From 5a2454fb6a6c8520bcacc6fbd5c0cace15868c5d Mon Sep 17 00:00:00 2001 From: md8n Date: Wed, 13 Dec 2023 09:24:50 +0900 Subject: [PATCH 01/19] File Merge --- CLI/Merge/MergeCommand.cs | 2 +- GCodeClean/Merge/MergeFile.cs | 210 ++++++++++++++++++++++++++++ GCodeClean/Processing/MergeFile.cs | 63 --------- GCodeClean/Structure/Line.cs | 216 ++++++++++++++--------------- GCodeClean/Structure/Token.cs | 47 ++++--- 5 files changed, 347 insertions(+), 191 deletions(-) create mode 100644 GCodeClean/Merge/MergeFile.cs delete mode 100644 GCodeClean/Processing/MergeFile.cs diff --git a/CLI/Merge/MergeCommand.cs b/CLI/Merge/MergeCommand.cs index 3988d8d..b864931 100644 --- a/CLI/Merge/MergeCommand.cs +++ b/CLI/Merge/MergeCommand.cs @@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis; -using GCodeClean.Processing; +using GCodeClean.Merge; using Spectre.Console; using Spectre.Console.Cli; diff --git a/GCodeClean/Merge/MergeFile.cs b/GCodeClean/Merge/MergeFile.cs new file mode 100644 index 0000000..ed5ab30 --- /dev/null +++ b/GCodeClean/Merge/MergeFile.cs @@ -0,0 +1,210 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using GCodeClean.Processing; +using GCodeClean.Structure; + +using Spectre.Console; + +namespace GCodeClean.Merge +{ + public static partial class Merge + { + private static readonly char[] separator = ['_']; + + public readonly record struct Node(string Tool, Int16 Id, Coord Start, Coord End); + public record struct Edge(Int16 PrevId, Int16 NextId, decimal Distance, Int16 Weighting) { + public Int16 Weighting { get; set; } = Weighting; + }; + + public static string ToSimpleString(this Edge edge) => $"{edge.PrevId}<->{edge.NextId}"; + + /// + /// Scan through the file for 'travelling' comments and build a list of them + /// + /// + /// + public static List GetNodes(this string inputFolder) { + var fileEntries = Directory.GetFiles(inputFolder); + Array.Sort(fileEntries); + List nodes = []; + foreach (var filePath in fileEntries) { + var fileNameParts = Path.GetFileNameWithoutExtension(filePath).Split(separator); + var tool = fileNameParts[0]; + var id = Int16.Parse(fileNameParts[1]); + var startCoords = fileNameParts[2].Replace("X", "").Split("Y").Select(c => decimal.Parse(c)).ToArray(); + var endCoords = fileNameParts[3].Replace("X", "").Split("Y").Select(c => decimal.Parse(c)).ToArray(); + var start = new Coord(startCoords[0], startCoords[1]); + var end = new Coord(endCoords[0], endCoords[1]); + nodes.Add(new Node(tool, id, start, end)); + } + + return nodes; + } + + /// + /// Identify primary pairings of cutting paths, where the end of one cutting path is the same as the start of one other cutting path. + /// These pairings will not be changed in future passes unless a loop is identified + /// + /// + /// + private static List GetPrimaryEdges(this List nodes) { + List primaryEdges = []; + foreach (var (tool, id, start, end) in nodes) { + var matchingNodes = nodes.FindAll(n => n.Tool == tool && n.Id != id && n.Start.X == end.X && n.Start.Y == end.Y); + if (matchingNodes.Count == 1) { + primaryEdges.Add(new Edge(id, matchingNodes[0].Id, 0M, 0)); + } + } + + return primaryEdges; + } + + private static List CheckForLoops(this List edges) { + List> nodeLists = []; + for(var ix = 0; ix < edges.Count; ix++) { + var edge = edges[ix]; + var matchingNodeLists = nodeLists.Where(nl => nl[^1] == edge.PrevId).ToList(); + if (matchingNodeLists.Count == 0) { + matchingNodeLists.Add([edge.PrevId, edge.NextId]); + continue; + } + if (matchingNodeLists.Count == 1) { + if (matchingNodeLists[0][0] == edge.NextId) { + // Loop detected + edge.Weighting = 100; // Do not use this + continue; + } + matchingNodeLists[0].Add(edge.NextId); + } + if (matchingNodeLists.Count > 1) { + throw new ArgumentOutOfRangeException("edges", "How did you get two chains of edges with the same end node ID?"); + } + } + + return edges; + } + + /// + /// Identify primary pairings of cutting paths, where the end of one cutting path is the same as the start of one other cutting path. + /// These pairings will not be changed in future passes + /// + /// + /// + private static List GetPrimaryPairings(this List nodes) + { + List primaryPairings = []; + foreach (var (tool, id, start, end) in nodes) { + var matchingNodes = nodes.FindAll(n => n.Tool == tool && n.Id != id && n.Start.X == end.X && n.Start.Y == end.Y); + if (matchingNodes.Count == 1) { + primaryPairings.Add(new Edge(id, matchingNodes[0].Id, 0M, 0)); + } + } + + return primaryPairings; + } + + public static List EndPairedNodeIds(this List edgePairs, int minWeighting) { + return edgePairs.Where(ep => ep.Weighting <= minWeighting).Select(ep => ep.PrevId).ToList(); + } + + public static List StartPairedNodeIds(this List edgePairs, int minWeighting) { + return edgePairs.Where(ep => ep.Weighting <= minWeighting).Select(ep => ep.NextId).ToList(); + } + + public static List MergeCycle(this List sourceEdges, List nodes, Int16 minWeighting) { + var firstPassEdges = sourceEdges.CheckForLoops(); + // Just the edges that we haven't marked as closing a loop + var pairedEdges = firstPassEdges + .Where(pe => pe.Weighting < 100) + .Select(pe => new Edge(pe.PrevId, pe.NextId, pe.Distance, pe.Weighting)) + .ToList(); + + // Fill in the nodes we have marked as closing a loop - so we don't repeat them + List knownLoopPairs = firstPassEdges + .Where(pe => pe.Weighting >= 100) + .Select(pe => new Edge(pe.PrevId, pe.NextId, pe.Distance, pe.Weighting)) + .ToList(); + + // And make sure we do not include anything we already consider as good enough + var pairedEndNodeIds = pairedEdges.EndPairedNodeIds(minWeighting); + var pairedStartNodeIds = pairedEdges.StartPairedNodeIds(minWeighting); + var pairedNodeIds = pairedEndNodeIds.Intersect(pairedStartNodeIds).ToList(); + var nfpNodes = nodes.Where(n => !pairedNodeIds.Contains(n.Id)); + List travellingPairs = []; + foreach (var (tool, id, start, end) in nfpNodes) { + if (pairedEndNodeIds.Contains(id)) { + continue; + } + var newPairEdges = from etNode in nfpNodes + .Where(nfpn => nfpn.Tool == tool && nfpn.Id != id && !pairedStartNodeIds.Contains(nfpn.Id)) + .Where(nfpn => !pairedEdges.Exists(pe => pe.PrevId == nfpn.Id && pe.NextId == id)) + .Where(nfpn => !knownLoopPairs.Exists(pe => pe.PrevId == id && pe.NextId == nfpn.Id)) + select (new Edge(id, etNode.Id, (end, etNode.Start).Distance(), 10)); + // Take the 10 closest matches for each node + travellingPairs.AddRange(newPairEdges.OrderBy(tp => tp.Distance).Take(10)); + } + List tpStartEdgeShortest = []; + foreach (var nodePrevId in travellingPairs.Select(tp => tp.PrevId).Distinct()) { + var startEdge = travellingPairs.Where(tp => tp.PrevId == nodePrevId).OrderBy(tp => tp.Distance).First(); + startEdge.Weighting = (short)(minWeighting + 1); + tpStartEdgeShortest.Add(startEdge); + } + List tpEndEdgeShortest = []; + foreach (var nodeNextId in travellingPairs.Select(tp => tp.NextId).Distinct()) { + var endEdge = travellingPairs.Where(tp => tp.NextId == nodeNextId).OrderBy(tp => tp.Distance).First(); + endEdge.Weighting = (short)(minWeighting + 1); + tpEndEdgeShortest.Add(endEdge); + } + List tpEdgeShortest = []; + foreach (var edge in tpStartEdgeShortest.OrderBy(tpsns => tpsns.PrevId)) { + var matchEdges = tpEndEdgeShortest.Where(tp => tp.PrevId == edge.PrevId && tp.NextId == edge.NextId && tp.Distance == edge.Distance); + if (matchEdges.Any()) { + tpEdgeShortest.Add(edge); + } + } + return [.. pairedEdges, .. tpEdgeShortest]; + } + + public static void MergeFile(this string inputFolder) + { + if (!Directory.Exists(inputFolder)) + { + AnsiConsole.MarkupLine($"No such folder found. Nothing to see here, move along."); + return; + } + + var nodes = inputFolder.GetNodes(); + var tools = nodes.Select(n => n.Tool).Distinct().ToList(); + var primaryEdges = nodes.GetPrimaryEdges(); + + List pairedEdges; + + Int16 minWeighting = 0; + do { + pairedEdges = primaryEdges.MergeCycle(nodes, minWeighting); + primaryEdges = pairedEdges; + } while (pairedEdges.Count < (nodes.Count - tools.Count) && minWeighting++ < 10); + + AnsiConsole.MarkupLine($"Pairings that were good:"); + foreach (var pair in pairedEdges.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting)).OrderBy(tps => tps.PrevId)) { + AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); + } + + AnsiConsole.MarkupLine($"Total distinct tools: {tools.Count}"); + AnsiConsole.MarkupLine($"Total nodes: {nodes.Count}"); + AnsiConsole.MarkupLine($"Total edges: {pairedEdges.Count}"); + + + //foreach (var pair in primaryPairs) { + // AnsiConsole.MarkupLine($"Node primary pairs: [bold yellow]{string.Join(',', pair)}[/]"); + //} + //AnsiConsole.MarkupLine($"Count primary pairs: [bold yellow]{primaryPairs.Count}[/]"); + } + } +} diff --git a/GCodeClean/Processing/MergeFile.cs b/GCodeClean/Processing/MergeFile.cs deleted file mode 100644 index 4b87e3f..0000000 --- a/GCodeClean/Processing/MergeFile.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -using GCodeClean.Structure; - -using Spectre.Console; - -namespace GCodeClean.Processing -{ - public static partial class Merge { - private static readonly char[] separator = ['_']; - - /// - /// Scan through the file for 'travelling' comments and build a list of them - /// - /// - /// - public static List<(string tool, int id, Coord start, Coord end)> GetNodes(this string inputFolder) { - var fileEntries = Directory.GetFiles(inputFolder); - Array.Sort(fileEntries); - List<(string tool, int id, Coord start, Coord end)> nodes = []; - foreach (var filePath in fileEntries) { - string[] fileNameParts = Path.GetFileNameWithoutExtension(filePath).Split(separator); - var tool = fileNameParts[0]; - var id = int.Parse(fileNameParts[1]); - var startCoords = fileNameParts[2].Replace("X", "").Split("Y").Select(c => decimal.Parse(c)).ToArray(); - var endCoords = fileNameParts[3].Replace("X", "").Split("Y").Select(c => decimal.Parse(c)).ToArray(); - var start = new Coord(startCoords[0], startCoords[1]); - var end = new Coord(endCoords[0], endCoords[1]); - nodes.Add((tool, id, start, end)); - } - - return nodes; - } - - public static void MergeFile(this string inputFolder) { - if (!Directory.Exists(inputFolder)) { - AnsiConsole.MarkupLine($"No such folder found. Nothing to see here, move along."); - return; - } - - var nodes = GetNodes(inputFolder); - List<(int idA, int idB)> primaryPairs = []; - - foreach (var (tool, id, start, end) in nodes) { - var matchingNodes = nodes.FindAll(n => n.tool == tool && n.id != id && n.start.X == end.X && n.start.Y == end.Y); - if (matchingNodes.Count == 1) { - primaryPairs.Add((id, matchingNodes[0].id)); - } - } - - foreach (var pair in primaryPairs) { - AnsiConsole.MarkupLine($"Node primary pairs: [bold yellow]{pair}[/]"); - } - AnsiConsole.MarkupLine($"Count primary pairs: [bold yellow]{primaryPairs.Count}[/]"); - } - } -} diff --git a/GCodeClean/Structure/Line.cs b/GCodeClean/Structure/Line.cs index 05e85e5..f5e96bc 100644 --- a/GCodeClean/Structure/Line.cs +++ b/GCodeClean/Structure/Line.cs @@ -1,6 +1,7 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. +using System; using System.Collections.Generic; using System.Linq; @@ -8,28 +9,21 @@ namespace GCodeClean.Structure { - public class Line { + public sealed class Line : IEquatable { private string _source; private List _tokens; /// - /// Get/Set the current list of all Tokens, get includes any line number token + /// Get/Set the current list of all Tokens, get includes any line number token. + /// Will reset the statuses for this line /// public List AllTokens { get { - // Always manipulate the returned list of tokens to put any line number first - // Even though we are doing this below in the set - var lineNumberToken = _tokens.Where(t => t.IsLineNumber).Take(1); - var allOtherTokens = _tokens.Where(t => !t.IsLineNumber); -#pragma warning disable S2365 // Properties should not make collection or array copies - return lineNumberToken.Concat(allOtherTokens).ToList(); -#pragma warning restore S2365 // Properties should not make collection or array copies + return _tokens; } set { - var lineNumberToken = value.Where(t => t.IsLineNumber).Take(1); - var allOtherTokens = value.Where(t => !t.IsLineNumber); - _tokens = lineNumberToken.Concat(allOtherTokens).ToList(); + SetTokens(value); } } @@ -44,44 +38,70 @@ public List Tokens { } } - public string Source { - get => _source; - set { - _source = value; + /// + /// Set the private member _tokens to the supplied value, ensuring that the order of tokens is correct + /// Then set the status values + /// + /// + private void SetTokens(List tokens) { + _tokens = tokens; + SetTokens(); + } - IsValid = true; - IsFileTerminator = false; - HasBlockDelete = false; - HasLineNumber = false; + /// + /// Set the private member _tokens, ensuring that the order of tokens is correct + /// Then set the status values + /// + private void SetTokens() { + var blockDeleteToken = _tokens.Where(t => t.IsBlockDelete).Take(1); + var lineNumberToken = _tokens.Where(t => t.IsLineNumber).Take(1); + var allCommentTokens = _tokens.Where(t => t.IsComment); + var allOtherTokens = _tokens.Where(t => !(t.IsLineNumber || t.IsBlockDelete || t.IsComment)); - _tokens ??= []; + _tokens = [.. blockDeleteToken, .. lineNumberToken, .. allOtherTokens, .. allCommentTokens]; - if (string.IsNullOrWhiteSpace(_source)) { - IsEmptyOrWhiteSpace = true; - return; - } + SetStatuses(); + } - AllTokens = _source.Tokenise().Select(s => new Token(s)).ToList(); + /// + /// Sets all of the status values from the current list of tokens + /// + private void SetStatuses() { + IsFileTerminator = false; + HasBlockDelete = false; + HasLineNumber = false; - if (Tokens.Exists(t => t.IsFileTerminator)) { - // Check the file terminator character is the only thing on the line - IsFileTerminator = true; - IsValid = Tokens.Count == 1; - return; - } + IsEmptyOrWhiteSpace = _tokens.TrueForAll(t => t.Source.Trim().Length == 0); + if (IsEmptyOrWhiteSpace) { + IsValid = true; + return; + } - if (Tokens.Exists(t => t.IsBlockDelete)) { - // Check the block delete character is the first character on the line - HasBlockDelete = true; - IsValid = Tokens[0].IsBlockDelete; - } + IsValid = _tokens.TrueForAll(t => t.IsValid); - if (!AllTokens.Exists(t => t.IsLineNumber)) { - return; - } + if (_tokens.Exists(t => t.IsFileTerminator)) { + // Check the file terminator character is the only thing on the line + IsFileTerminator = true; + IsValid = Tokens.Count == 1; + return; + } - // We have a line number - HasLineNumber = true; + HasBlockDelete = _tokens.Exists(t => t.IsBlockDelete); + if (HasBlockDelete && !_tokens[0].IsBlockDelete) { + // Assuming SetTokens has been called this will never be invoked, however ... + IsValid = false; + } + + HasLineNumber = _tokens.Exists(t => t.IsLineNumber); + } + + public string Source { + get => _source; + set { + _source = value; + _tokens ??= []; + + SetTokens(_source.Tokenise().Select(s => new Token(s)).ToList()); } } @@ -95,23 +115,23 @@ public string Source { public bool HasLineNumber { get; private set; } - public bool HasTokens(char[] codes) => AllTokens.Exists(t => codes.Contains(t.Code)); + public bool HasTokens(char[] codes) => _tokens.Exists(t => codes.Contains(t.Code)); public bool HasTokens(IEnumerable tokens) { var parsedTokens = tokens.Select(t => new Token(t)); return HasTokens(parsedTokens); } - public bool HasTokens(IEnumerable tokens) => AllTokens.Exists(tokens.Contains); + public bool HasTokens(IEnumerable tokens) => _tokens.Exists(tokens.Contains); - public bool HasToken(char code) => AllTokens.Exists(t => t.Code == code); + public bool HasToken(char code) => _tokens.Exists(t => t.Code == code); public bool HasToken(string token) { var parsedToken = new Token(token); return HasToken(parsedToken); } - public bool HasToken(Token token) => AllTokens.Exists(t => t == token); + public bool HasToken(Token token) => _tokens.Exists(t => t == token); /// /// Roughly equivalent to `IsNullOrWhiteSpace` this returns true if there are: @@ -120,7 +140,7 @@ public bool HasToken(string token) { /// * only one or more comments /// public bool IsNotCommandCodeOrArguments() { - return AllTokens.Count == 0 || AllTokens.TrueForAll(t => t.IsFileTerminator) || AllTokens.TrueForAll(t => t.IsComment); + return _tokens.Count == 0 || _tokens.TrueForAll(t => t.IsFileTerminator) || _tokens.TrueForAll(t => t.IsComment); } /// @@ -165,13 +185,7 @@ public Line(string source) { public Line(Line line) { _source = line.ToString(); - _tokens = line.AllTokens.Select(t => new Token(t)).ToList(); - - IsValid = line.IsValid; - IsFileTerminator = line.IsFileTerminator; - IsEmptyOrWhiteSpace = line.IsEmptyOrWhiteSpace; - HasBlockDelete = line.HasBlockDelete; - HasLineNumber = line.HasLineNumber; + AllTokens = line.AllTokens.Select(t => new Token(t)).ToList(); } /// @@ -182,12 +196,7 @@ public Line(Token token) { _source = token.ToString(); _tokens = [new Token(token)]; - - IsValid = _tokens[0].IsValid; - IsFileTerminator = _tokens[0].IsFileTerminator; - IsEmptyOrWhiteSpace = _tokens[0].Source.Trim().Length == 0; - HasBlockDelete = _tokens[0].IsBlockDelete; - HasLineNumber = _tokens[0].IsLineNumber; + SetStatuses(); } /// @@ -197,16 +206,7 @@ public Line(Token token) public Line(IEnumerable tokens) { _source = string.Join(' ', tokens); - _tokens = tokens.Select(t => new Token(t)).ToList(); - - IsValid = _tokens.TrueForAll(t => t.IsValid); - IsFileTerminator = _tokens[0].IsFileTerminator; - IsEmptyOrWhiteSpace = _tokens.TrueForAll(t => t.Source.Trim().Length == 0); - HasBlockDelete = _tokens.Exists(t => t.IsBlockDelete); - if (HasBlockDelete && !_tokens[0].IsBlockDelete) { - IsValid = false; - } - HasLineNumber = _tokens.Exists(t => t.IsLineNumber); + SetTokens(tokens.Select(t => new Token(t)).ToList()); } #endregion @@ -218,21 +218,19 @@ public void ClearTokens() public void PrependToken(Token token) { _tokens.Insert(0, token); - - // This little hack handles situations where we have an existing line number - // If the supplied token is a line number, then the existing line number will be eliminated - // If the supplied token is not a line number, then it will end up immediately after any line number - _tokens = AllTokens; + SetTokens(); } public void AppendToken(Token token) { _tokens.Add(token); + SetTokens(); } public void AppendTokens(IEnumerable tokens) { _tokens.AddRange(tokens); + SetTokens(); } public List RemoveTokens(List codes) { @@ -246,6 +244,7 @@ public List RemoveTokens(List codes) { removedTokens.Add(_tokens[ix]); _tokens.RemoveAt(ix); } + SetTokens(); return removedTokens; } @@ -261,6 +260,7 @@ public List RemoveTokens(List tokens) { removedTokens.Add(_tokens[ix]); _tokens.RemoveAt(ix); } + SetTokens(); return removedTokens; } @@ -276,6 +276,7 @@ public List RemoveToken(char code) { removedTokens.Add(_tokens[ix]); _tokens.RemoveAt(ix); } + SetTokens(); return removedTokens; } @@ -291,6 +292,7 @@ public List RemoveToken(Token token) { removedTokens.Add(_tokens[ix]); _tokens.RemoveAt(ix); } + SetTokens(); return removedTokens; } @@ -307,6 +309,7 @@ public List ReplaceToken(Token searchToken, Token replaceToken) { _tokens.RemoveAt(ix); _tokens.Insert(ix, replaceToken); } + SetTokens(); return removedTokens; } @@ -375,63 +378,60 @@ public static implicit operator Coord(Line line) return coords; } + public override bool Equals(object obj) => this.Equals(obj as Line); + /// - /// Compare two lines for equality. + /// Compare this line and another for equality. /// Note that line numbers are not included in the comparison /// - /// - /// + /// /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Blocker Code Smell", "S3875:\"operator==\" should not be overloaded on reference types", Justification = "")] - public static bool operator ==(Line a, Line b) { - if (a is null || b is null) { - return a is null && b is null; + public bool Equals(Line line) { + if (line is null) { + return false; } - if (a.Tokens.Count != b.Tokens.Count) { + if (Object.ReferenceEquals(this, line)) { + return true; + } + + if (Tokens.Count != line.Tokens.Count) { return false; } - return !b.Tokens.Where((t, ix) => a.Tokens[ix] != t).Any(); + return !line.Tokens.Where((t, ix) => Tokens[ix] != t).Any(); } - public static bool operator !=(Line a, Line b) { - return !(a == b); - } + public override int GetHashCode() => Tokens.Select(t => t.GetHashCode()).GetHashCode(); - public override bool Equals(object obj) - { - // Compare run-time types. - return GetType() == obj?.GetType() && this == (Line)obj; - } + /// + /// Compare two lines for equality. + /// Note that line numbers are not included in the comparison + /// + /// + /// + /// + public static bool operator ==(Line a, Line b) { + if (a is null) { + return b is null; + } - public override int GetHashCode() - { - return Tokens.Select(t => t.GetHashCode()).GetHashCode(); + return a.Equals(b); } + + public static bool operator !=(Line a, Line b) => !(a == b); /// /// Return the line as a formatted string, with any block delete and line number first and any comments last /// /// - public override string ToString() - { - var allTokensOrdered = _tokens.Where(t => t.IsBlockDelete).Take(1) - .Concat(_tokens.Where(t => t.IsLineNumber)) - .Concat(_tokens.Where(t => !(t.IsBlockDelete || t.IsLineNumber || t.IsComment))) - .Concat(_tokens.Where(t => t.IsComment)); - return string.Join(" ", allTokensOrdered).Trim(); - } + public override string ToString() => string.Join(" ", _tokens).Trim(); /// /// Return the line as a formatted string, but without any line number or comment /// /// - public string ToSimpleString() { - var allTokensOrdered = _tokens.Where(t => t.IsBlockDelete).Take(1) - .Concat(_tokens.Where(t => !(t.IsBlockDelete || t.IsLineNumber || t.IsComment))); - return string.Join(" ", allTokensOrdered).Trim(); - } + public string ToSimpleString() => string.Join(" ", _tokens.Where(t => !(t.IsLineNumber || t.IsComment))).Trim(); public string ToXYCoord() { var xyz = (Coord)this; diff --git a/GCodeClean/Structure/Token.cs b/GCodeClean/Structure/Token.cs index ab0da3c..e9fbd47 100644 --- a/GCodeClean/Structure/Token.cs +++ b/GCodeClean/Structure/Token.cs @@ -6,7 +6,7 @@ namespace GCodeClean.Structure { - public class Token + public sealed class Token : IEquatable { private string _source; private char _code; @@ -224,34 +224,31 @@ public Token ToComment() { return this; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Blocker Code Smell", "S3875:\"operator==\" should not be overloaded on reference types", Justification = "")] - public static bool operator ==(Token a, Token b) { - if (a is null || b is null) { - return a is null && b is null; - } + public override bool Equals(object obj) => this.Equals(obj as Token); - if (a.Code != b.Code) { + public bool Equals(Token token) { + if (token is null) { return false; } - if (a.IsComment) { - return a.Source == b.Source; + if (Object.ReferenceEquals(this, token)) { + return true; } - if (a.IsFileTerminator || a.IsBlockDelete) { - return true; + if (Code != token.Code) { + return false; } - return a.Number == b.Number; - } + if (IsComment) { + return Source == token.Source; + } - public static bool operator !=(Token a, Token b) { - return !(a == b); - } + if (IsFileTerminator || IsBlockDelete) { + // true because Code comparison is already true + return true; + } - public override bool Equals(object obj) { - // Compare run-time types. - return GetType() == obj?.GetType() && this == (Token) obj; + return Number == token.Number; } public override int GetHashCode() { @@ -262,6 +259,18 @@ public override int GetHashCode() { return (Code, Number).GetHashCode(); } + public static bool operator ==(Token a, Token b) { + if (a is null) { + return b is null; + } + + return a.Equals(b); + } + + public static bool operator !=(Token a, Token b) { + return !(a == b); + } + public override string ToString() { if (IsFileTerminator || IsBlockDelete || IsComment || !IsValid) { return Source; From ad9a5aea41b0b53d6aadb7ac4952717de622053f Mon Sep 17 00:00:00 2001 From: md8n Date: Tue, 19 Dec 2023 09:00:33 +0900 Subject: [PATCH 02/19] First technique tried --- GCodeClean.Tests/Merge.Tests.cs | 130 +++++++++++++ GCodeClean/Merge/MergeFile.cs | 250 ++++++++++++++---------- GCodeClean/Merge/Objects.cs | 13 ++ GCodeClean/Merge/Utility.cs | 326 ++++++++++++++++++++++++++++++++ 4 files changed, 619 insertions(+), 100 deletions(-) create mode 100644 GCodeClean.Tests/Merge.Tests.cs create mode 100644 GCodeClean/Merge/Objects.cs create mode 100644 GCodeClean/Merge/Utility.cs diff --git a/GCodeClean.Tests/Merge.Tests.cs b/GCodeClean.Tests/Merge.Tests.cs new file mode 100644 index 0000000..1edd117 --- /dev/null +++ b/GCodeClean.Tests/Merge.Tests.cs @@ -0,0 +1,130 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com) and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System.Collections.Generic; +using System.Linq; + +using Xunit; +using Xunit.Abstractions; + +using GCodeClean.Merge; + + +namespace GCodeClean.Tests { + public class MergeTest(ITestOutputHelper testOutputHelper) { + [Fact] + public void TestCheckForLoopsFirstPairings() { + List sourceEdges = [ + new Edge(0, 1, 0, 0), + new Edge(1, 2, 98.1337309440541M, 4), + new Edge(2, 0, 94.0106166876912M, 4), + new Edge(3, 4, 14.2090530296709M, 1), + new Edge(4, 5, 28.7462839511475M, 2), + new Edge(5, 6, 0, 0), + new Edge(6, 7, 22.3672135278403M, 1), + new Edge(7, 3, 47.4712088007036M, 3), + new Edge(8, 9, 0, 0), + new Edge(9, 10, 28.7073332268952M, 1), + new Edge(10, 11, 0, 0), + new Edge(11, 12, 0, 0), + new Edge(12, 8, 30.127148371527M, 1), + new Edge(13, 14, 0, 0), + new Edge(14, 15, 25.1760346361376M, 1), + new Edge(15, 16, 47.586326292329M, 2), + new Edge(16, 17, 0, 0), + new Edge(17, 18, 14.2227357776203M, 1), + new Edge(18, 19, 25.4259792338466M, 1), + new Edge(19, 13, 155.649088105263M, 4), + ]; + var pairedEdges = sourceEdges.CheckForLoops(); + + Assert.True(sourceEdges.Count == 20); + Assert.True(pairedEdges[16].Weighting == 100); // new Edge(2, 0, 94.0106166876912M, 4), + Assert.True(pairedEdges[17].Weighting == 100); // new Edge(7, 3, 47.4712088007036M, 3), + Assert.True(pairedEdges[18].Weighting == 100); // new Edge(12, 8, 30.127148371527M, 1), + Assert.True(pairedEdges.Count(pe => pe.Weighting == 100) == 4); + } + + [Fact] + public void TestCheckForLoopsProblemPairings() { + List sourceEdges = [ + new Edge(3, 2, 35.630983048465M, 1), + new Edge(4, 3, 1.79479943169146M, 1), + new Edge(3, 4, 14.2090530296709M, 1), + new Edge(3, 5, 24.6702927627542M, 1), + new Edge(6, 7, 22.3672135278403M, 1), + new Edge(12, 8, 30.127148371527M, 1), + new Edge(9, 10, 28.7073332268952M, 1), + new Edge(14, 15, 25.1760346361376M, 1), + new Edge(15, 13, 0.219672483483936M, 1), + new Edge(15, 16, 47.586326292329M, 1), + new Edge(2, 0, 94.0106166876912M, 1), + new Edge(17, 18, 14.2227357776203M, 1), + new Edge(18, 19, 25.4259792338466M, 1), + ]; + var pairedEdges = sourceEdges.CheckForLoops(); + + Assert.True(sourceEdges.Count == 13); + Assert.True(pairedEdges.Count == 13); + Assert.True(pairedEdges.Count(pe => pe.Weighting == 100) == 3); + } + + [Fact] + public void TestFilterEdgePairs() { + List sourceEdges = [ + new Edge(3, 2, 35.630983048465M, 1), + new Edge(4, 3, 1.79479943169146M, 1), + new Edge(3, 4, 14.2090530296709M, 1), + new Edge(3, 5, 24.6702927627542M, 1), + new Edge(6, 7, 22.3672135278403M, 1), + new Edge(12, 8, 30.127148371527M, 1), + new Edge(9, 10, 28.7073332268952M, 1), + new Edge(14, 15, 25.1760346361376M, 1), + new Edge(15, 13, 0.219672483483936M, 1), + new Edge(15, 16, 47.586326292329M, 1), + new Edge(2, 0, 94.0106166876912M, 1), + new Edge(17, 18, 14.2227357776203M, 1), + new Edge(18, 19, 25.4259792338466M, 1), + ]; + var filteredEdges = sourceEdges.FilterEdgePairs(); + + Assert.True(sourceEdges.Count == 13); + Assert.True(filteredEdges.Count == 10); + Assert.False(filteredEdges.Exists(fe => fe.Weighting == 100)); + } + + [Fact] + public void TestFilterEdgePairsWithCurrentPairs() { + List sourceEdges = [ + new Edge(2, 0, 94.0106166876912M, 1), + new Edge(3, 4, 14.2090530296709M, 1), + new Edge(3, 5, 24.6702927627542M, 1), + new Edge(4, 3, 1.79479943169146M, 1), + new Edge(6, 7, 22.3672135278403M, 1), + new Edge(9, 10, 28.7073332268952M, 1), + new Edge(12, 8, 30.127148371527M, 1), + new Edge(14, 15, 25.1760346361376M, 1), + new Edge(15, 13, 0.219672483483936M, 1), + new Edge(15, 16, 47.586326292329M, 1), + new Edge(17, 18, 14.2227357776203M, 1), + new Edge(18, 19, 25.4259792338466M, 1), + ]; + List currentEdges = [ + new Edge(0, 1, 0, 0), + new Edge(5, 6, 0, 0), + new Edge(8, 9, 0, 0), + new Edge(10, 11, 0, 0), + new Edge(11, 12, 0, 0), + new Edge(13, 14, 0, 0), + new Edge(16, 17, 0, 0), + ]; + + var pairedEdges = sourceEdges.FilterEdgePairsWithCurrentPairs(currentEdges); + + Assert.True(sourceEdges.Count == 12); + Assert.True(currentEdges.Count == 7); + Assert.True(pairedEdges.Count == 8); + Assert.False(pairedEdges.Exists(pe => pe.Weighting == 100)); + } + } +} diff --git a/GCodeClean/Merge/MergeFile.cs b/GCodeClean/Merge/MergeFile.cs index ed5ab30..9932b0c 100644 --- a/GCodeClean/Merge/MergeFile.cs +++ b/GCodeClean/Merge/MergeFile.cs @@ -12,16 +12,11 @@ using Spectre.Console; namespace GCodeClean.Merge -{ - public static partial class Merge +{ + public static class Merge { private static readonly char[] separator = ['_']; - public readonly record struct Node(string Tool, Int16 Id, Coord Start, Coord End); - public record struct Edge(Int16 PrevId, Int16 NextId, decimal Distance, Int16 Weighting) { - public Int16 Weighting { get; set; } = Weighting; - }; - public static string ToSimpleString(this Edge edge) => $"{edge.PrevId}<->{edge.NextId}"; /// @@ -48,73 +43,38 @@ public static List GetNodes(this string inputFolder) { } /// - /// Identify primary pairings of cutting paths, where the end of one cutting path is the same as the start of one other cutting path. - /// These pairings will not be changed in future passes unless a loop is identified + /// Find all nodes that are not pointed to as the Previous node by an edge /// + /// /// /// - private static List GetPrimaryEdges(this List nodes) { - List primaryEdges = []; - foreach (var (tool, id, start, end) in nodes) { - var matchingNodes = nodes.FindAll(n => n.Tool == tool && n.Id != id && n.Start.X == end.X && n.Start.Y == end.Y); - if (matchingNodes.Count == 1) { - primaryEdges.Add(new Edge(id, matchingNodes[0].Id, 0M, 0)); - } - } - - return primaryEdges; + public static List UnpairedPrevNodes(this List edgePairs, List nodes) { + return nodes.Where(n => !edgePairs.Exists(ep => ep.PrevId == n.Id)).ToList(); } - private static List CheckForLoops(this List edges) { - List> nodeLists = []; - for(var ix = 0; ix < edges.Count; ix++) { - var edge = edges[ix]; - var matchingNodeLists = nodeLists.Where(nl => nl[^1] == edge.PrevId).ToList(); - if (matchingNodeLists.Count == 0) { - matchingNodeLists.Add([edge.PrevId, edge.NextId]); - continue; - } - if (matchingNodeLists.Count == 1) { - if (matchingNodeLists[0][0] == edge.NextId) { - // Loop detected - edge.Weighting = 100; // Do not use this - continue; - } - matchingNodeLists[0].Add(edge.NextId); - } - if (matchingNodeLists.Count > 1) { - throw new ArgumentOutOfRangeException("edges", "How did you get two chains of edges with the same end node ID?"); - } - } - - return edges; + public static List UnpairedNextNodes(this List edgePairs, List nodes) { + return nodes.Where(n => !edgePairs.Exists(ep => ep.NextId == n.Id)).ToList(); } - /// - /// Identify primary pairings of cutting paths, where the end of one cutting path is the same as the start of one other cutting path. - /// These pairings will not be changed in future passes - /// - /// - /// - private static List GetPrimaryPairings(this List nodes) - { - List primaryPairings = []; - foreach (var (tool, id, start, end) in nodes) { - var matchingNodes = nodes.FindAll(n => n.Tool == tool && n.Id != id && n.Start.X == end.X && n.Start.Y == end.Y); - if (matchingNodes.Count == 1) { - primaryPairings.Add(new Edge(id, matchingNodes[0].Id, 0M, 0)); - } + public static List BuildTravellingPairs(this List knownLoopForkPairs, List unpairedPrevNodes, List unpairedNextNodes) { + List travellingPairs = []; + foreach (var upn in unpairedPrevNodes) { + // Match other nodes (not self) that use the same tool + // and not a known loop forming edge pair + // and take the top 10 + var newPairEdges = unpairedNextNodes + .Where(unn => unn.Id != upn.Id + && unn.Tool == upn.Tool + && !knownLoopForkPairs.Exists(pe => pe.PrevId == upn.Id && pe.NextId == unn.Id) + ) + .Select(unn => new Edge(upn.Id, unn.Id, (upn.End, unn.Start).Distance(), 10)) + .OrderBy(e => e.Distance) + .Take(10); + // Then remove those where the inverse distance is less + travellingPairs.AddRange(newPairEdges); } - return primaryPairings; - } - - public static List EndPairedNodeIds(this List edgePairs, int minWeighting) { - return edgePairs.Where(ep => ep.Weighting <= minWeighting).Select(ep => ep.PrevId).ToList(); - } - - public static List StartPairedNodeIds(this List edgePairs, int minWeighting) { - return edgePairs.Where(ep => ep.Weighting <= minWeighting).Select(ep => ep.NextId).ToList(); + return travellingPairs; } public static List MergeCycle(this List sourceEdges, List nodes, Int16 minWeighting) { @@ -125,50 +85,84 @@ public static List MergeCycle(this List sourceEdges, List node .Select(pe => new Edge(pe.PrevId, pe.NextId, pe.Distance, pe.Weighting)) .ToList(); - // Fill in the nodes we have marked as closing a loop - so we don't repeat them - List knownLoopPairs = firstPassEdges + // Fill in the nodes we have marked as closing a loop or making a fork - so we don't repeat them + List knownLoopForkPairs = firstPassEdges .Where(pe => pe.Weighting >= 100) .Select(pe => new Edge(pe.PrevId, pe.NextId, pe.Distance, pe.Weighting)) .ToList(); + // And add those that are the inverse of all current pairings - as these would equal loops +#pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters + knownLoopForkPairs.AddRange(pairedEdges.Select(pe => new Edge(pe.NextId, pe.PrevId, pe.Distance, 100))); +#pragma warning restore S2234 // Arguments should be passed in the same order as the method parameters - // And make sure we do not include anything we already consider as good enough - var pairedEndNodeIds = pairedEdges.EndPairedNodeIds(minWeighting); - var pairedStartNodeIds = pairedEdges.StartPairedNodeIds(minWeighting); - var pairedNodeIds = pairedEndNodeIds.Intersect(pairedStartNodeIds).ToList(); - var nfpNodes = nodes.Where(n => !pairedNodeIds.Contains(n.Id)); - List travellingPairs = []; - foreach (var (tool, id, start, end) in nfpNodes) { - if (pairedEndNodeIds.Contains(id)) { - continue; - } - var newPairEdges = from etNode in nfpNodes - .Where(nfpn => nfpn.Tool == tool && nfpn.Id != id && !pairedStartNodeIds.Contains(nfpn.Id)) - .Where(nfpn => !pairedEdges.Exists(pe => pe.PrevId == nfpn.Id && pe.NextId == id)) - .Where(nfpn => !knownLoopPairs.Exists(pe => pe.PrevId == id && pe.NextId == nfpn.Id)) - select (new Edge(id, etNode.Id, (end, etNode.Start).Distance(), 10)); - // Take the 10 closest matches for each node - travellingPairs.AddRange(newPairEdges.OrderBy(tp => tp.Distance).Take(10)); - } + var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); + var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes); + List travellingPairs = knownLoopForkPairs.BuildTravellingPairs(unpairedPrevNodes, unpairedNextNodes); + + // Focus on the PrevId of the TravellingPair edges List tpStartEdgeShortest = []; foreach (var nodePrevId in travellingPairs.Select(tp => tp.PrevId).Distinct()) { var startEdge = travellingPairs.Where(tp => tp.PrevId == nodePrevId).OrderBy(tp => tp.Distance).First(); startEdge.Weighting = (short)(minWeighting + 1); tpStartEdgeShortest.Add(startEdge); } + tpStartEdgeShortest = tpStartEdgeShortest.FilterEdgePairsWithCurrentPairs(pairedEdges); + + // Focus on the NextId of the TravellingPair edges List tpEndEdgeShortest = []; foreach (var nodeNextId in travellingPairs.Select(tp => tp.NextId).Distinct()) { var endEdge = travellingPairs.Where(tp => tp.NextId == nodeNextId).OrderBy(tp => tp.Distance).First(); endEdge.Weighting = (short)(minWeighting + 1); tpEndEdgeShortest.Add(endEdge); } - List tpEdgeShortest = []; - foreach (var edge in tpStartEdgeShortest.OrderBy(tpsns => tpsns.PrevId)) { - var matchEdges = tpEndEdgeShortest.Where(tp => tp.PrevId == edge.PrevId && tp.NextId == edge.NextId && tp.Distance == edge.Distance); - if (matchEdges.Any()) { - tpEdgeShortest.Add(edge); - } + tpEndEdgeShortest = tpEndEdgeShortest.FilterEdgePairsWithCurrentPairs(pairedEdges); + + var tpEdgeShortest = tpStartEdgeShortest.IntersectEdges(tpEndEdgeShortest); + + if (tpEdgeShortest.Count > 0) { + pairedEdges = [.. pairedEdges, .. tpEdgeShortest]; + } else { + pairedEdges = [.. pairedEdges, .. tpStartEdgeShortest, .. tpEndEdgeShortest]; + } + + pairedEdges = pairedEdges.CheckForLoops().Where(pe => pe.Weighting < 100).ToList(); + + //AnsiConsole.MarkupLine("Pairs:"); + //foreach (var pair in pairedEdges.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { + // AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); + //} + + // Find all outstanding nodes, and try to 'inject' them between two paired nodes + unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); + unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes); + var unpairedNodes = unpairedPrevNodes.Intersect(unpairedNextNodes).ToList(); + + // If we have unpaired Prev or Next, but no completely unpaired nodes (likely) + // then we'll try to 'pop' some unpaired Prev or Next, and see what we can get + //if (unpairedNodes.Count == 0) { + // var poppableEdges = pairedEdges.Where(pe => pe.Weighting > 0); + // var unpairedNextNodeIds = unpairedNextNodes.Select(n => n.Id); + // var edgePrevToPop = poppableEdges.Where(pe => unpairedNextNodeIds.Contains(pe.PrevId)).OrderByDescending(pe => pe.Distance).First(); + // pairedEdges.Remove(pairedEdges.GetEdge(edgePrevToPop.PrevId, edgePrevToPop.NextId)); + // unpairedNodes.Add(nodes.GetNode(edgePrevToPop.PrevId)); + //} + + foreach (var node in unpairedNodes) { + var injectables = pairedEdges + .Where(pe => pe.Weighting > 0 && pe.Weighting < 100) + .Select(pe => (pe.PrevId, pe.NextId, pe.Distance, node.Id, injPrevDist: (nodes.GetNode(pe.PrevId).End, node.Start).Distance(), injNextDist: (node.End, nodes.GetNode(pe.NextId).Start).Distance())) + .OrderBy(inj => inj.injPrevDist + inj.injNextDist - inj.Distance) + .Take(10).ToList(); + var (PrevId, NextId, _, Id, injPrevDist, injNextDist) = injectables.FirstOrDefault(); + + var swappedEdge = pairedEdges.GetEdge(PrevId, NextId); + pairedEdges.Remove(swappedEdge); + pairedEdges.Add(new Edge(PrevId, Id, injPrevDist, swappedEdge.Weighting)); + pairedEdges.Add(new Edge(Id, NextId, injNextDist, swappedEdge.Weighting)); + pairedEdges = pairedEdges.CheckForLoops().Where(pe => pe.Weighting < 100).ToList(); } - return [.. pairedEdges, .. tpEdgeShortest]; + + return pairedEdges; } public static void MergeFile(this string inputFolder) @@ -179,27 +173,83 @@ public static void MergeFile(this string inputFolder) return; } - var nodes = inputFolder.GetNodes(); + var nodes = inputFolder.GetNodes().Take(20).ToList(); var tools = nodes.Select(n => n.Tool).Distinct().ToList(); - var primaryEdges = nodes.GetPrimaryEdges(); + //AnsiConsole.MarkupLine("Original nodes:"); + //for (var ix = 0; ix < nodes.Count; ix++) { + // AnsiConsole.MarkupLine($"[bold yellow]{nodes[ix].Start.X}, {nodes[ix].Start.Y}, {nodes[ix].End.X}, {nodes[ix].End.Y}[/]"); + //} - List pairedEdges; + if (tools.Count > 1) { + AnsiConsole.MarkupLine("[bold red]Currently only one tool per merge is supported[/]"); + return; + } - Int16 minWeighting = 0; + var currentDistance = nodes.TotalDistance(nodes.Select(n => n.Id).ToList()); + + var pairedEdges = nodes.GetPrimaryEdges(); + List prevStartIds; + List prevEndIds; + List startIds; + List endIds; + short minCycles = 0; + short minWeighting = 0; do { - pairedEdges = primaryEdges.MergeCycle(nodes, minWeighting); - primaryEdges = pairedEdges; - } while (pairedEdges.Count < (nodes.Count - tools.Count) && minWeighting++ < 10); + (startIds, endIds) = pairedEdges.GetStartsAndEnds(); + do { + prevStartIds = startIds; + prevEndIds = endIds; + pairedEdges = pairedEdges.MergeCycle(nodes, minWeighting); + (startIds, endIds) = pairedEdges.GetStartsAndEnds(); + } while ( + !prevStartIds.ToHashSet().SetEquals(startIds) && + !prevEndIds.ToHashSet().SetEquals(endIds) && + pairedEdges.Count < (nodes.Count - tools.Count) && + minWeighting++ < 100 + ); + pairedEdges.DivideAndCheck(nodes); + if (startIds.Count == 1) { + break; + } + List empty = []; + List travellingPairs = empty.BuildTravellingPairs(nodes.Where(n => endIds.Contains(n.Id)).ToList(), nodes.Where(n => startIds.Contains(n.Id)).ToList()); + pairedEdges = [.. pairedEdges, ..travellingPairs.Select(tp => new Edge(tp.PrevId, tp.NextId, tp.Distance, minWeighting))]; + pairedEdges = pairedEdges.CheckForLoops().Where(pe => pe.Weighting < 100).ToList(); + + } while (prevStartIds.Count > 1 && minCycles++ < 10); AnsiConsole.MarkupLine($"Pairings that were good:"); - foreach (var pair in pairedEdges.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting)).OrderBy(tps => tps.PrevId)) { + foreach (var pair in pairedEdges.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); } + var newDistance = nodes.TotalDistance(pairedEdges.GetNodeIds()); + AnsiConsole.MarkupLine($"Total distinct tools: {tools.Count}"); AnsiConsole.MarkupLine($"Total nodes: {nodes.Count}"); AnsiConsole.MarkupLine($"Total edges: {pairedEdges.Count}"); + AnsiConsole.MarkupLine($"Starting node Ids: {string.Join(',', startIds)}"); + AnsiConsole.MarkupLine($"Ending node Ids: {string.Join(',', endIds)}"); + + AnsiConsole.MarkupLine($"Current travelling distance: {currentDistance}"); + AnsiConsole.MarkupLine($"New travelling distance: {newDistance}"); + // List<(string tool, List nodeIds)> cutList = []; + // foreach(var toolStartId in toolStartIds) { + // var tool = nodes.First(n => n.Id == toolStartId).Tool; + // var pairedEdge = pairedEdges.Find(pe => pe.PrevId == toolStartId); + // List nodeIds = [pairedEdge.PrevId]; + //#pragma warning disable S2583 + //#pragma warning disable CS8073 + // do { + // var nextId = pairedEdge.NextId; + // nodeIds.Add(nextId); + // pairedEdge = pairedEdges.Find(pe => pe.PrevId == nextId); + // } while (pairedEdge != null); + //#pragma warning restore CS8073 + //#pragma warning restore S2583 + // cutList.Add( (tool, nodeIds) ); + // } //foreach (var pair in primaryPairs) { // AnsiConsole.MarkupLine($"Node primary pairs: [bold yellow]{string.Join(',', pair)}[/]"); diff --git a/GCodeClean/Merge/Objects.cs b/GCodeClean/Merge/Objects.cs new file mode 100644 index 0000000..f3d3a7b --- /dev/null +++ b/GCodeClean/Merge/Objects.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System; +using GCodeClean.Structure; + +namespace GCodeClean.Merge +{ + public readonly record struct Node(string Tool, short Id, Coord Start, Coord End); + public record struct Edge(short PrevId, short NextId, decimal Distance, short Weighting) { + public Int16 Weighting { get; set; } = Weighting; + }; +} diff --git a/GCodeClean/Merge/Utility.cs b/GCodeClean/Merge/Utility.cs new file mode 100644 index 0000000..9019a8c --- /dev/null +++ b/GCodeClean/Merge/Utility.cs @@ -0,0 +1,326 @@ +// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using GCodeClean.Processing; +using Spectre.Console; + +namespace GCodeClean.Merge +{ + public static class Utility + { + public static Node GetNode(this IEnumerable nodes, short id) { + return nodes.First(n => n.Id == id); + } + + public static Edge GetEdge(this IEnumerable edges, short prevId, short nextId) { + return edges.First(n => n.PrevId == prevId && n.NextId == nextId); + } + + public static decimal TotalDistance(this List nodes, List nodeIds) { + var distance = 0M; + for (var ix = 0; ix < nodeIds.Count - 1; ix++) { + var prevNode = nodes[nodeIds[ix]]; + var nextNode = nodes[nodeIds[ix + 1]]; + distance += (prevNode.End, nextNode.Start).Distance(); + } + return distance; + } + + /// + /// Converts a list of edges (must be contiguous chain) into a list of node Ids + /// + /// + /// + public static List GetNodeIds(this List edges) { + List nodeIds = [edges[0].PrevId]; + nodeIds.AddRange(edges.Select(e => e.NextId)); + return nodeIds; + } + + /// + /// Identify primary pairings of cutting paths, where the end of one cutting path is the same as the start of one other cutting path. + /// These pairings will not be changed in future passes unless a loop is identified + /// + /// + /// + public static List GetPrimaryEdges(this List nodes) { + List primaryEdges = []; + foreach (var (tool, id, start, end) in nodes) { + var matchingNodes = nodes.FindAll(n => n.Tool == tool && n.Id != id && n.Start.X == end.X && n.Start.Y == end.Y); + if (matchingNodes.Count == 1) { + primaryEdges.Add(new Edge(id, matchingNodes[0].Id, 0M, 0)); + } + } + + return primaryEdges; + } + + /// + /// If a title has been provided do a dump to the console of the supplied list of edge pairs + /// + /// + /// + public static void DebugEdgePairs(this List edgePairs, string title = "") { + if (title == "") { + return; + } + AnsiConsole.MarkupLine(title); + foreach (var pair in edgePairs.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting)).OrderBy(tps => tps.PrevId)) { + AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); + } + } + + public static bool HasProcessableEdges(this List edges) { + return edges.Exists(e => e.Weighting < 100); + } + + public static List IntersectEdges(this List edges, IEnumerable otherEdges) { + return edges.IntersectBy(otherEdges.Select(oe => (oe.PrevId, oe.NextId)), e => (e.PrevId, e.NextId)).ToList(); + } + + public static List RemoveDuplicates(this IEnumerable edges) { + List dedupEdges = []; + + foreach (var edge in edges) { + if (dedupEdges.Exists(de => de.PrevId == edge.PrevId && de.NextId == edge.NextId)) { + continue; + } + dedupEdges.Add(edge); + } + + return dedupEdges; + } + + /// + /// Takes the supplied list of edges and determines if there are any loops or forks + /// + /// + /// + public static List CheckForLoops(this List edges) { + edges = edges.RemoveDuplicates(); + if (!edges.HasProcessableEdges()) { + return []; + } + List> nodeLists = []; + + // Get the max weighting value less than 100, it's the edges with this value that we will sort before checking for loops + var maxWeighting = edges.Where(e => e.Weighting < 100).OrderByDescending(e => e.Weighting).First().Weighting; + + // Deep copy the edges first before starting, we want them in a specific order, and because the reordering at the end is destructive + var testEdges = edges.Where(e => e.Weighting < maxWeighting).Select(e => new Edge(e.PrevId, e.NextId, e.Distance, e.Weighting)).ToList(); + testEdges.AddRange(edges.Where(e => e.Weighting == maxWeighting).OrderBy(e => e.Distance).Select(e => new Edge(e.PrevId, e.NextId, e.Distance, e.Weighting))); + testEdges.AddRange(edges.Where(e => e.Weighting >= 100)); + + for (var ix = 0; ix < testEdges.Count; ix++) { + var edge = testEdges[ix]; + if (edge.Weighting >= 100) { + // Already excluded + continue; + } + + var longerNodeLists = nodeLists.Where(nl => nl.Count > 2).Select(nl => nl[1..^1]).ToList(); + if (longerNodeLists.Count > 0) { + var matchingNodeListFork = longerNodeLists.Find(nl => nl.Contains(edge.PrevId) || nl.Contains(edge.NextId)); + + if (matchingNodeListFork != null) { + // Fork detected - within a nodelist + edge.Weighting = 100; // Do not use this + testEdges[ix] = edge; + continue; + } + } + + var matchingNodeListPreceeding = nodeLists.Find(nl => nl[^1] == edge.PrevId); + var matchingNodeListSucceeding = nodeLists.Find(nl => nl[0] == edge.NextId); + + if (matchingNodeListPreceeding == null && matchingNodeListSucceeding == null) { + nodeLists.Add([edge.PrevId, edge.NextId]); + continue; + } + + if (matchingNodeListPreceeding != null && matchingNodeListPreceeding.Contains(edge.NextId)) { + // Loop detected + edge.Weighting = 100; // Do not use this + testEdges[ix] = edge; + } + if (matchingNodeListSucceeding != null && matchingNodeListSucceeding.Contains(edge.PrevId)) { + // Loop detected + edge.Weighting = 100; // Do not use this + testEdges[ix] = edge; + } + + if (matchingNodeListPreceeding != null && matchingNodeListSucceeding != null && edge.Weighting < 100) { + // Merge two nodelists into one - unless its a loop + matchingNodeListPreceeding.AddRange(matchingNodeListSucceeding); + nodeLists.Remove(matchingNodeListSucceeding); + continue; + } + + if (matchingNodeListPreceeding != null && !matchingNodeListPreceeding.Contains(edge.NextId)) { + matchingNodeListPreceeding.Add(edge.NextId); + } + if (matchingNodeListSucceeding != null && !matchingNodeListSucceeding.Contains(edge.PrevId)) { + matchingNodeListSucceeding.Insert(matchingNodeListSucceeding.IndexOf(edge.NextId), edge.PrevId); + } + } + + List orderedEdges = []; + foreach (List nodeList in nodeLists) { + for (var ix = 0; ix < nodeList.Count - 1; ix++) { + var edge = testEdges.Find(e => e.PrevId == nodeList[ix] && e.NextId == nodeList[ix + 1]); + orderedEdges.Add(edge); + testEdges.Remove(edge); + } + } + orderedEdges.AddRange(testEdges); + + return orderedEdges; + } + + public static List OrderIds(this List nodeIds) { + List orderedIds = [nodeIds[0], .. nodeIds[1..^1].OrderBy(x => x), nodeIds[^1]]; + return orderedIds; + } + + public static bool IsShorter(this List edges, List originalNodes) { + var originalNodeIds = originalNodes.Select(n => n.Id).ToList(); + var newNodeIds = edges.GetNodeIds(); + var currentDistance = originalNodes.TotalDistance(originalNodeIds); + var newDistance = originalNodes.TotalDistance(newNodeIds); + return currentDistance >= newDistance; + } + + + /// + /// Divide the supplied list of edges to lists of continguous nodes (that aren't in original order) + /// and determine if that list of edges should be reverted to original order + /// + /// + /// + /// + public static List DivideAndCheck(this List edges, List originalNodes) { + // If we have a list of nodes that covers a continguous set of original nodes + // Then we'll compare to see if we've actually saved anything, + // and if not we'll revert to original + if (edges.IsShorter(originalNodes)) { + // Newer is better, so leave it alone + return edges; + } + + var originalNodeIdsMatch = String.Join(",", originalNodes.Select(n => n.Id.ToString())); + // Take a simple binary chop through the list of edges, and shuffle it a bit until + // things match up + var newNodeIds = edges.GetNodeIds(); + int chop = newNodeIds.Count / 2; + + List newFirstHalfIds; + List newSecondHalfIds; + List newFirstHalfIdsOrdered; + List newSecondHalfIdsOrdered; + string newFirstHalf; + string newSecondHalf; + + do { + newFirstHalfIds = newNodeIds[0..chop]; + newSecondHalfIds = newNodeIds[chop..]; + + newFirstHalfIdsOrdered = newFirstHalfIds.OrderIds(); + newSecondHalfIdsOrdered = newSecondHalfIds.OrderIds(); + newFirstHalf = String.Join(",", newFirstHalfIdsOrdered.Select(n => n.ToString())); + newSecondHalf = String.Join(",", newSecondHalfIdsOrdered.Select(n => n.ToString())); + + AnsiConsole.MarkupLine($"[bold yellow]{originalNodeIdsMatch}[/]"); + AnsiConsole.MarkupLine($"[bold yellow]{newFirstHalf} / {newSecondHalf}[/]"); + + chop++; + } while (!originalNodeIdsMatch.Contains(newFirstHalf)); + + var newFirstHalfEdges = newFirstHalfIds[..^1].Select(fId => edges.First(e => e.PrevId == fId)).ToList(); + var origFirstHalfNodes = newFirstHalfIdsOrdered.Select(fId => originalNodes.First(n => n.Id == fId)).ToList(); + + if (!newFirstHalfEdges.IsShorter(origFirstHalfNodes)) { + newFirstHalfEdges = newFirstHalfEdges.DivideAndCheck(origFirstHalfNodes); + } + + var newSecondHalfEdges = newSecondHalfIds[..^1].Select(fId => edges.First(e => e.PrevId == fId)).ToList(); + var origSecondHalfNodes = newSecondHalfIdsOrdered.Select(fId => originalNodes.First(n => n.Id == fId)).ToList(); + + if (!newSecondHalfEdges.IsShorter(origSecondHalfNodes)) { + newSecondHalfEdges = newSecondHalfEdges.DivideAndCheck(origSecondHalfNodes); + } + + edges = [..newFirstHalfEdges,..newSecondHalfEdges]; + + return edges; + + + // var originalNodeIds = String.Join(",", originalNodes.Select(n => n.Id.ToString())); + + // orignalNodeIds has a zero length when we do not want to do reversions + //if (originalNodeIds.Length > 0) { + // for (var ix = 0; ix < nodeLists.Count; ix++) { + // var nodeList = nodeLists[ix]; + // var orderedNodeList = nodeList.OrderBy(x => x).ToList(); + // var newNodeIds = String.Join(",", nodeList.Select(n => n.ToString())); + // var newOrderedNodeIds = String.Join(",", orderedNodeList.Select(n => n.ToString())); + // if (newNodeIds == newOrderedNodeIds) { + // // Nothing to see here, move along + // continue; + // } + + // if (originalNodeIds.Contains(newOrderedNodeIds)) { + // var currentDistance = originalNodes.TotalDistance(orderedNodeList); + // var newDistance = originalNodes.TotalDistance(nodeList); + // if (currentDistance < newDistance) { + // // revert + // nodeLists[ix] = orderedNodeList; + // } + // } + // } + //} + } + + /// + /// Filter supplied edge pairs for anything with a weighting of 100 or more + /// + /// + /// A new list of filtered edge pairs + public static List FilterEdgePairs(this List edges) { + if (!edges.HasProcessableEdges()) { + return []; + } + return edges.CheckForLoops() + .Where(ep => ep.Weighting < 100) + .Select(ep => new Edge(ep.PrevId, ep.NextId, ep.Distance, ep.Weighting)) + .ToList(); + } + + /// + /// Filter the supplied set of edge pairs after combining them with a current set of edge pairs + /// + /// + /// + /// A new list of filtered edge pairs + public static List FilterEdgePairsWithCurrentPairs(this List edges, List currentPairs) { + if (!edges.HasProcessableEdges()) { + return []; + } + var firstFilteredEP = edges.FilterEdgePairs(); + List tempEdges = [.. currentPairs, .. firstFilteredEP]; + tempEdges = tempEdges.FilterEdgePairs(); + + return firstFilteredEP.Where(ff => tempEdges.Exists(te => te.PrevId == ff.PrevId && te.NextId == ff.NextId)).ToList(); + } + + public static (List startIds, List endIds) GetStartsAndEnds(this List edges) { + var starts = edges.Select(pe => pe.PrevId).ToList(); + var ends = edges.Select(pe => pe.NextId).ToList(); + // Find the starting node Ids - one for each tool - if the tool is used for more than one cutting path + return (starts.Where(si => !ends.Contains(si)).ToList(), ends.Where(ei => !starts.Contains(ei)).ToList()); + } + } +} From 2fea951d37a269982f37164d7dba1b09ad2fe368 Mon Sep 17 00:00:00 2001 From: md8n Date: Wed, 20 Dec 2023 13:08:18 +0900 Subject: [PATCH 03/19] Looks like a contender --- GCodeClean.Tests/Merge.Tests.cs | 24 ++++ GCodeClean/Merge/MergeFile.cs | 226 ++++++++++++++++---------------- GCodeClean/Merge/Utility.cs | 117 +++-------------- 3 files changed, 157 insertions(+), 210 deletions(-) diff --git a/GCodeClean.Tests/Merge.Tests.cs b/GCodeClean.Tests/Merge.Tests.cs index 1edd117..0205d48 100644 --- a/GCodeClean.Tests/Merge.Tests.cs +++ b/GCodeClean.Tests/Merge.Tests.cs @@ -69,6 +69,30 @@ public void TestCheckForLoopsProblemPairings() { Assert.True(pairedEdges.Count(pe => pe.Weighting == 100) == 3); } + [Fact] + public void TestCheckForLoopsLateFork() { + List sourceEdges = [ + new Edge(14, 15, 25.1760346361376M, 10), + new Edge(15, 13, 0.219672483483936M, 10), + new Edge(4, 3, 1.79479943169146M, 10), + new Edge(17, 18, 14.2227357776203M, 10), + new Edge(18, 19, 25.4259792338466M, 10), + new Edge(6, 7, 22.3672135278403M, 10), + new Edge(7, 5, 39.6719488429797M, 10), + new Edge(9, 10, 28.7073332268952M, 10), + new Edge(12, 8, 30.127148371527M, 10), + new Edge(1, 2, 98.1337309440541M, 10), + new Edge(2, 3, 50.7771087105203M, 10), + new Edge(3, 4, 14.2090530296709M, 10), + new Edge(19, 18, 76.3650198061914M, 10), + ]; + var pairedEdges = sourceEdges.CheckForLoops(); + + Assert.True(sourceEdges.Count == 13); + Assert.True(pairedEdges.Count == 13); + Assert.True(pairedEdges.Count(pe => pe.Weighting == 100) == 3); + } + [Fact] public void TestFilterEdgePairs() { List sourceEdges = [ diff --git a/GCodeClean/Merge/MergeFile.cs b/GCodeClean/Merge/MergeFile.cs index 9932b0c..8f9642c 100644 --- a/GCodeClean/Merge/MergeFile.cs +++ b/GCodeClean/Merge/MergeFile.cs @@ -56,20 +56,20 @@ public static List UnpairedNextNodes(this List edgePairs, List return nodes.Where(n => !edgePairs.Exists(ep => ep.NextId == n.Id)).ToList(); } - public static List BuildTravellingPairs(this List knownLoopForkPairs, List unpairedPrevNodes, List unpairedNextNodes) { + public static List BuildTravellingPairs(this List knownLoopForkPairs, List unpairedPrevNodes, List unpairedNextNodes, short weighting, int topCount = 10) { List travellingPairs = []; foreach (var upn in unpairedPrevNodes) { // Match other nodes (not self) that use the same tool // and not a known loop forming edge pair - // and take the top 10 + // and take the top 'count' of the results (default 10) var newPairEdges = unpairedNextNodes .Where(unn => unn.Id != upn.Id && unn.Tool == upn.Tool && !knownLoopForkPairs.Exists(pe => pe.PrevId == upn.Id && pe.NextId == unn.Id) ) - .Select(unn => new Edge(upn.Id, unn.Id, (upn.End, unn.Start).Distance(), 10)) + .Select(unn => new Edge(upn.Id, unn.Id, (upn.End, unn.Start).Distance(), weighting)) .OrderBy(e => e.Distance) - .Take(10); + .Take(topCount); // Then remove those where the inverse distance is less travellingPairs.AddRange(newPairEdges); } @@ -77,92 +77,105 @@ public static List BuildTravellingPairs(this List knownLoopForkPairs return travellingPairs; } - public static List MergeCycle(this List sourceEdges, List nodes, Int16 minWeighting) { - var firstPassEdges = sourceEdges.CheckForLoops(); - // Just the edges that we haven't marked as closing a loop - var pairedEdges = firstPassEdges - .Where(pe => pe.Weighting < 100) - .Select(pe => new Edge(pe.PrevId, pe.NextId, pe.Distance, pe.Weighting)) - .ToList(); - - // Fill in the nodes we have marked as closing a loop or making a fork - so we don't repeat them - List knownLoopForkPairs = firstPassEdges - .Where(pe => pe.Weighting >= 100) - .Select(pe => new Edge(pe.PrevId, pe.NextId, pe.Distance, pe.Weighting)) - .ToList(); - // And add those that are the inverse of all current pairings - as these would equal loops + public static List PairSeedingToInjPairings(this List pairedEdges, List nodes, short weighting) { + AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Peer Seeding"); #pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters - knownLoopForkPairs.AddRange(pairedEdges.Select(pe => new Edge(pe.NextId, pe.PrevId, pe.Distance, 100))); + List alreadyPaired = pairedEdges.Select(pe => new Edge(pe.NextId, pe.PrevId, pe.Distance, 100)).ToList(); #pragma warning restore S2234 // Arguments should be passed in the same order as the method parameters - var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes); - List travellingPairs = knownLoopForkPairs.BuildTravellingPairs(unpairedPrevNodes, unpairedNextNodes); - - // Focus on the PrevId of the TravellingPair edges - List tpStartEdgeShortest = []; - foreach (var nodePrevId in travellingPairs.Select(tp => tp.PrevId).Distinct()) { - var startEdge = travellingPairs.Where(tp => tp.PrevId == nodePrevId).OrderBy(tp => tp.Distance).First(); - startEdge.Weighting = (short)(minWeighting + 1); - tpStartEdgeShortest.Add(startEdge); + List seedPairings = [.. alreadyPaired.BuildTravellingPairs(unpairedPrevNodes, unpairedNextNodes, weighting, 1).OrderBy(tp => tp.Distance)]; + seedPairings = seedPairings.Where(sp => sp.Weighting < 100).ToList().FilterEdgePairsWithCurrentPairs(pairedEdges); + + if (seedPairings.Count == 0) { + return pairedEdges; } - tpStartEdgeShortest = tpStartEdgeShortest.FilterEdgePairsWithCurrentPairs(pairedEdges); - - // Focus on the NextId of the TravellingPair edges - List tpEndEdgeShortest = []; - foreach (var nodeNextId in travellingPairs.Select(tp => tp.NextId).Distinct()) { - var endEdge = travellingPairs.Where(tp => tp.NextId == nodeNextId).OrderBy(tp => tp.Distance).First(); - endEdge.Weighting = (short)(minWeighting + 1); - tpEndEdgeShortest.Add(endEdge); + + if (pairedEdges.Count == 0) { + // No zero length pairings, so choose the shortest edge pairing that there is, as the seed + seedPairings = [seedPairings[0]]; } - tpEndEdgeShortest = tpEndEdgeShortest.FilterEdgePairsWithCurrentPairs(pairedEdges); - var tpEdgeShortest = tpStartEdgeShortest.IntersectEdges(tpEndEdgeShortest); + var unpairedNodes = unpairedPrevNodes.IntersectNodes(unpairedNextNodes); - if (tpEdgeShortest.Count > 0) { - pairedEdges = [.. pairedEdges, .. tpEdgeShortest]; - } else { - pairedEdges = [.. pairedEdges, .. tpStartEdgeShortest, .. tpEndEdgeShortest]; + List injPairings = []; + for (var ix = 0; ix < seedPairings.Count; ix++) { + var seedPairing = seedPairings[ix]; + if (seedPairing.Weighting >= 100) { + continue; + } + var prevNode = nodes.GetNode(seedPairing.PrevId); + var nextNode = nodes.GetNode(seedPairing.NextId); + var fun = unpairedNodes.Where(unn => unn.Id != prevNode.Id && unn.Id != nextNode.Id); + var altPrevEdges = fun + .Select(unn => new Edge(prevNode.Id, unn.Id, (prevNode.End, unn.Start).Distance(), 10)) + .OrderBy(e => e.NextId) + .Take(10) + .ToList(); + var altNextEdges = fun + .Select(upn => new Edge(upn.Id, nextNode.Id, (upn.End, nextNode.Start).Distance(), 10)) + .OrderBy(e => e.PrevId) + .Take(10) + .ToList(); + if (altPrevEdges.Count == 0 || altNextEdges.Count == 0) { + continue; + } + List<(Edge ap, Edge an, decimal distance)> altInjEdges = []; + foreach (var ap in altPrevEdges) { + var an = altNextEdges.Find(an => an.PrevId == ap.NextId); +#pragma warning disable CS8073 + if (an == null) { + continue; + } +#pragma warning restore CS8073 + altInjEdges.Add((ap, an, ap.Distance + an.Distance)); + } + altInjEdges = [.. altInjEdges.OrderBy(a => a.distance)]; + var triplet = altInjEdges[0]; + if (triplet.distance - seedPairing.Distance < seedPairing.Distance) { + List tripPair = [triplet.ap, triplet.an]; + tripPair = tripPair.FilterEdgePairsWithCurrentPairs([..pairedEdges, ..seedPairings]); + if (tripPair.Count == 2) { + seedPairing.Weighting = 100; + seedPairings[ix] = seedPairing; + unpairedNodes.Remove(unpairedNodes.GetNode(triplet.ap.NextId)); + injPairings.AddRange([triplet.ap, triplet.an]); + } + } + } + AnsiConsole.MarkupLine($"Injection Pairings:"); + foreach (var pair in injPairings.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { + AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); } - - pairedEdges = pairedEdges.CheckForLoops().Where(pe => pe.Weighting < 100).ToList(); - //AnsiConsole.MarkupLine("Pairs:"); - //foreach (var pair in pairedEdges.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { - // AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); - //} + pairedEdges = [.. pairedEdges, .. seedPairings, .. injPairings]; + return pairedEdges.CheckForLoops(); + } - // Find all outstanding nodes, and try to 'inject' them between two paired nodes - unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); - unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes); - var unpairedNodes = unpairedPrevNodes.Intersect(unpairedNextNodes).ToList(); - - // If we have unpaired Prev or Next, but no completely unpaired nodes (likely) - // then we'll try to 'pop' some unpaired Prev or Next, and see what we can get - //if (unpairedNodes.Count == 0) { - // var poppableEdges = pairedEdges.Where(pe => pe.Weighting > 0); - // var unpairedNextNodeIds = unpairedNextNodes.Select(n => n.Id); - // var edgePrevToPop = poppableEdges.Where(pe => unpairedNextNodeIds.Contains(pe.PrevId)).OrderByDescending(pe => pe.Distance).First(); - // pairedEdges.Remove(pairedEdges.GetEdge(edgePrevToPop.PrevId, edgePrevToPop.NextId)); - // unpairedNodes.Add(nodes.GetNode(edgePrevToPop.PrevId)); - //} + public static List BuildResidualPairs(this List pairedEdges, List nodes, short weighting) { + AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Residual pairs"); + var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes).Select(n => n.Id); + var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes).Select(n => n.Id); + + List empty = []; + List residualPairs = empty.BuildTravellingPairs(nodes.Where(n => unpairedPrevNodes.Contains(n.Id)).ToList(), nodes.Where(n => unpairedNextNodes.Contains(n.Id)).ToList(), weighting); - foreach (var node in unpairedNodes) { - var injectables = pairedEdges - .Where(pe => pe.Weighting > 0 && pe.Weighting < 100) - .Select(pe => (pe.PrevId, pe.NextId, pe.Distance, node.Id, injPrevDist: (nodes.GetNode(pe.PrevId).End, node.Start).Distance(), injNextDist: (node.End, nodes.GetNode(pe.NextId).Start).Distance())) - .OrderBy(inj => inj.injPrevDist + inj.injNextDist - inj.Distance) - .Take(10).ToList(); - var (PrevId, NextId, _, Id, injPrevDist, injNextDist) = injectables.FirstOrDefault(); - - var swappedEdge = pairedEdges.GetEdge(PrevId, NextId); - pairedEdges.Remove(swappedEdge); - pairedEdges.Add(new Edge(PrevId, Id, injPrevDist, swappedEdge.Weighting)); - pairedEdges.Add(new Edge(Id, NextId, injNextDist, swappedEdge.Weighting)); - pairedEdges = pairedEdges.CheckForLoops().Where(pe => pe.Weighting < 100).ToList(); + for (var ix = residualPairs.Count - 1; ix >= 0; ix--) { + List residualPrimary = [residualPairs[ix]]; + List residualTest = residualPrimary.FilterEdgePairsWithCurrentPairs(pairedEdges); + if (residualTest.Count == 0) { + residualPairs.Remove(residualPrimary[0]); + } + } + + List finalPairs = []; + while (residualPairs.Count > 1) { + var residualPrimary = residualPairs.OrderByDescending(rp => rp.NextId).ThenBy(rp => rp.Distance).First(); + residualPairs = residualPairs.Where(rp => rp.PrevId != residualPrimary.PrevId && rp.NextId != residualPrimary.NextId).ToList(); + finalPairs.Add(residualPrimary); } - return pairedEdges; + return finalPairs; } public static void MergeFile(this string inputFolder) @@ -173,12 +186,8 @@ public static void MergeFile(this string inputFolder) return; } - var nodes = inputFolder.GetNodes().Take(20).ToList(); + var nodes = inputFolder.GetNodes().ToList(); var tools = nodes.Select(n => n.Tool).Distinct().ToList(); - //AnsiConsole.MarkupLine("Original nodes:"); - //for (var ix = 0; ix < nodes.Count; ix++) { - // AnsiConsole.MarkupLine($"[bold yellow]{nodes[ix].Start.X}, {nodes[ix].Start.Y}, {nodes[ix].End.X}, {nodes[ix].End.Y}[/]"); - //} if (tools.Count > 1) { AnsiConsole.MarkupLine("[bold red]Currently only one tool per merge is supported[/]"); @@ -188,52 +197,49 @@ public static void MergeFile(this string inputFolder) var currentDistance = nodes.TotalDistance(nodes.Select(n => n.Id).ToList()); var pairedEdges = nodes.GetPrimaryEdges(); - List prevStartIds; - List prevEndIds; + List startIds; List endIds; - short minCycles = 0; - short minWeighting = 0; + int prevCount = 0; + int postCount = 0; + short weighting = 1; + do { - (startIds, endIds) = pairedEdges.GetStartsAndEnds(); - do { - prevStartIds = startIds; - prevEndIds = endIds; - pairedEdges = pairedEdges.MergeCycle(nodes, minWeighting); - (startIds, endIds) = pairedEdges.GetStartsAndEnds(); - } while ( - !prevStartIds.ToHashSet().SetEquals(startIds) && - !prevEndIds.ToHashSet().SetEquals(endIds) && - pairedEdges.Count < (nodes.Count - tools.Count) && - minWeighting++ < 100 - ); - pairedEdges.DivideAndCheck(nodes); - if (startIds.Count == 1) { - break; - } - List empty = []; - List travellingPairs = empty.BuildTravellingPairs(nodes.Where(n => endIds.Contains(n.Id)).ToList(), nodes.Where(n => startIds.Contains(n.Id)).ToList()); - pairedEdges = [.. pairedEdges, ..travellingPairs.Select(tp => new Edge(tp.PrevId, tp.NextId, tp.Distance, minWeighting))]; - pairedEdges = pairedEdges.CheckForLoops().Where(pe => pe.Weighting < 100).ToList(); + prevCount = pairedEdges.Count; + pairedEdges = pairedEdges.PairSeedingToInjPairings(nodes, weighting++); + postCount = pairedEdges.Count; + } while (prevCount < postCount); - } while (prevStartIds.Count > 1 && minCycles++ < 10); + var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); + while (unpairedPrevNodes.Count > 1) { + weighting++; + List residualPairs = pairedEdges.BuildResidualPairs(nodes, weighting); - AnsiConsole.MarkupLine($"Pairings that were good:"); - foreach (var pair in pairedEdges.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { - AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); + pairedEdges = [.. pairedEdges, .. residualPairs]; + pairedEdges = pairedEdges.CheckForLoops(); + pairedEdges = pairedEdges.Where(pe => pe.Weighting < 100).ToList(); + + unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); } - var newDistance = nodes.TotalDistance(pairedEdges.GetNodeIds()); + //AnsiConsole.MarkupLine($"Pairings that were good:"); + //foreach (var pair in pairedEdges.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { + // AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); + //} + var nodeIdList = pairedEdges.GetNodeIds(); + var newDistance = nodes.TotalDistance(nodeIdList); AnsiConsole.MarkupLine($"Total distinct tools: {tools.Count}"); AnsiConsole.MarkupLine($"Total nodes: {nodes.Count}"); AnsiConsole.MarkupLine($"Total edges: {pairedEdges.Count}"); + (startIds, endIds) = pairedEdges.GetStartsAndEnds(); AnsiConsole.MarkupLine($"Starting node Ids: {string.Join(',', startIds)}"); AnsiConsole.MarkupLine($"Ending node Ids: {string.Join(',', endIds)}"); AnsiConsole.MarkupLine($"Current travelling distance: {currentDistance}"); AnsiConsole.MarkupLine($"New travelling distance: {newDistance}"); + // List<(string tool, List nodeIds)> cutList = []; // foreach(var toolStartId in toolStartIds) { // var tool = nodes.First(n => n.Id == toolStartId).Tool; diff --git a/GCodeClean/Merge/Utility.cs b/GCodeClean/Merge/Utility.cs index 9019a8c..236d0f1 100644 --- a/GCodeClean/Merge/Utility.cs +++ b/GCodeClean/Merge/Utility.cs @@ -1,13 +1,14 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. -using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; -using GCodeClean.Processing; + using Spectre.Console; +using GCodeClean.Processing; + + namespace GCodeClean.Merge { public static class Utility @@ -78,6 +79,10 @@ public static bool HasProcessableEdges(this List edges) { return edges.Exists(e => e.Weighting < 100); } + public static List IntersectNodes(this List nodes, IEnumerable otherNodes) { + return nodes.IntersectBy(otherNodes.Select(on => on.Id), e => e.Id).ToList(); + } + public static List IntersectEdges(this List edges, IEnumerable otherEdges) { return edges.IntersectBy(otherEdges.Select(oe => (oe.PrevId, oe.NextId)), e => (e.PrevId, e.NextId)).ToList(); } @@ -122,15 +127,16 @@ public static List CheckForLoops(this List edges) { continue; } - var longerNodeLists = nodeLists.Where(nl => nl.Count > 2).Select(nl => nl[1..^1]).ToList(); - if (longerNodeLists.Count > 0) { - var matchingNodeListFork = longerNodeLists.Find(nl => nl.Contains(edge.PrevId) || nl.Contains(edge.NextId)); - - if (matchingNodeListFork != null) { + foreach (var nodeList in nodeLists) { + if (nodeList[0..^1].Contains(edge.PrevId)) { + // Fork detected - within a nodelist + edge.Weighting = 100; // Do not use this + testEdges[ix] = edge; + } + if (nodeList[1..].Contains(edge.NextId)) { // Fork detected - within a nodelist edge.Weighting = 100; // Do not use this testEdges[ix] = edge; - continue; } } @@ -195,95 +201,6 @@ public static bool IsShorter(this List edges, List originalNodes) { } - /// - /// Divide the supplied list of edges to lists of continguous nodes (that aren't in original order) - /// and determine if that list of edges should be reverted to original order - /// - /// - /// - /// - public static List DivideAndCheck(this List edges, List originalNodes) { - // If we have a list of nodes that covers a continguous set of original nodes - // Then we'll compare to see if we've actually saved anything, - // and if not we'll revert to original - if (edges.IsShorter(originalNodes)) { - // Newer is better, so leave it alone - return edges; - } - - var originalNodeIdsMatch = String.Join(",", originalNodes.Select(n => n.Id.ToString())); - // Take a simple binary chop through the list of edges, and shuffle it a bit until - // things match up - var newNodeIds = edges.GetNodeIds(); - int chop = newNodeIds.Count / 2; - - List newFirstHalfIds; - List newSecondHalfIds; - List newFirstHalfIdsOrdered; - List newSecondHalfIdsOrdered; - string newFirstHalf; - string newSecondHalf; - - do { - newFirstHalfIds = newNodeIds[0..chop]; - newSecondHalfIds = newNodeIds[chop..]; - - newFirstHalfIdsOrdered = newFirstHalfIds.OrderIds(); - newSecondHalfIdsOrdered = newSecondHalfIds.OrderIds(); - newFirstHalf = String.Join(",", newFirstHalfIdsOrdered.Select(n => n.ToString())); - newSecondHalf = String.Join(",", newSecondHalfIdsOrdered.Select(n => n.ToString())); - - AnsiConsole.MarkupLine($"[bold yellow]{originalNodeIdsMatch}[/]"); - AnsiConsole.MarkupLine($"[bold yellow]{newFirstHalf} / {newSecondHalf}[/]"); - - chop++; - } while (!originalNodeIdsMatch.Contains(newFirstHalf)); - - var newFirstHalfEdges = newFirstHalfIds[..^1].Select(fId => edges.First(e => e.PrevId == fId)).ToList(); - var origFirstHalfNodes = newFirstHalfIdsOrdered.Select(fId => originalNodes.First(n => n.Id == fId)).ToList(); - - if (!newFirstHalfEdges.IsShorter(origFirstHalfNodes)) { - newFirstHalfEdges = newFirstHalfEdges.DivideAndCheck(origFirstHalfNodes); - } - - var newSecondHalfEdges = newSecondHalfIds[..^1].Select(fId => edges.First(e => e.PrevId == fId)).ToList(); - var origSecondHalfNodes = newSecondHalfIdsOrdered.Select(fId => originalNodes.First(n => n.Id == fId)).ToList(); - - if (!newSecondHalfEdges.IsShorter(origSecondHalfNodes)) { - newSecondHalfEdges = newSecondHalfEdges.DivideAndCheck(origSecondHalfNodes); - } - - edges = [..newFirstHalfEdges,..newSecondHalfEdges]; - - return edges; - - - // var originalNodeIds = String.Join(",", originalNodes.Select(n => n.Id.ToString())); - - // orignalNodeIds has a zero length when we do not want to do reversions - //if (originalNodeIds.Length > 0) { - // for (var ix = 0; ix < nodeLists.Count; ix++) { - // var nodeList = nodeLists[ix]; - // var orderedNodeList = nodeList.OrderBy(x => x).ToList(); - // var newNodeIds = String.Join(",", nodeList.Select(n => n.ToString())); - // var newOrderedNodeIds = String.Join(",", orderedNodeList.Select(n => n.ToString())); - // if (newNodeIds == newOrderedNodeIds) { - // // Nothing to see here, move along - // continue; - // } - - // if (originalNodeIds.Contains(newOrderedNodeIds)) { - // var currentDistance = originalNodes.TotalDistance(orderedNodeList); - // var newDistance = originalNodes.TotalDistance(nodeList); - // if (currentDistance < newDistance) { - // // revert - // nodeLists[ix] = orderedNodeList; - // } - // } - // } - //} - } - /// /// Filter supplied edge pairs for anything with a weighting of 100 or more /// @@ -317,8 +234,8 @@ public static List FilterEdgePairsWithCurrentPairs(this List edges, } public static (List startIds, List endIds) GetStartsAndEnds(this List edges) { - var starts = edges.Select(pe => pe.PrevId).ToList(); - var ends = edges.Select(pe => pe.NextId).ToList(); + var starts = edges.Where(e => e.Weighting < 100).Select(pe => pe.PrevId).ToList(); + var ends = edges.Where(e => e.Weighting < 100).Select(pe => pe.NextId).ToList(); // Find the starting node Ids - one for each tool - if the tool is used for more than one cutting path return (starts.Where(si => !ends.Contains(si)).ToList(), ends.Where(ei => !starts.Contains(ei)).ToList()); } From fc057113c914633494f4625881566f4ae5c32078 Mon Sep 17 00:00:00 2001 From: md8n Date: Wed, 20 Dec 2023 13:24:10 +0900 Subject: [PATCH 04/19] Housekeeping --- GCodeClean/Merge/MergeFile.cs | 52 +++++++++++++++++++---------------- GCodeClean/Merge/Utility.cs | 49 ++++++--------------------------- 2 files changed, 37 insertions(+), 64 deletions(-) diff --git a/GCodeClean/Merge/MergeFile.cs b/GCodeClean/Merge/MergeFile.cs index 8f9642c..df15941 100644 --- a/GCodeClean/Merge/MergeFile.cs +++ b/GCodeClean/Merge/MergeFile.cs @@ -77,27 +77,7 @@ public static List BuildTravellingPairs(this List knownLoopForkPairs return travellingPairs; } - public static List PairSeedingToInjPairings(this List pairedEdges, List nodes, short weighting) { - AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Peer Seeding"); -#pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters - List alreadyPaired = pairedEdges.Select(pe => new Edge(pe.NextId, pe.PrevId, pe.Distance, 100)).ToList(); -#pragma warning restore S2234 // Arguments should be passed in the same order as the method parameters - var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); - var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes); - List seedPairings = [.. alreadyPaired.BuildTravellingPairs(unpairedPrevNodes, unpairedNextNodes, weighting, 1).OrderBy(tp => tp.Distance)]; - seedPairings = seedPairings.Where(sp => sp.Weighting < 100).ToList().FilterEdgePairsWithCurrentPairs(pairedEdges); - - if (seedPairings.Count == 0) { - return pairedEdges; - } - - if (pairedEdges.Count == 0) { - // No zero length pairings, so choose the shortest edge pairing that there is, as the seed - seedPairings = [seedPairings[0]]; - } - - var unpairedNodes = unpairedPrevNodes.IntersectNodes(unpairedNextNodes); - + public static List GetInjectablePairings(this List pairedEdges, List seedPairings, List nodes, List unpairedNodes) { List injPairings = []; for (var ix = 0; ix < seedPairings.Count; ix++) { var seedPairing = seedPairings[ix]; @@ -124,9 +104,11 @@ public static List PairSeedingToInjPairings(this List pairedEdges, L foreach (var ap in altPrevEdges) { var an = altNextEdges.Find(an => an.PrevId == ap.NextId); #pragma warning disable CS8073 +#pragma warning disable S2589 if (an == null) { continue; } +#pragma warning restore S2589 #pragma warning restore CS8073 altInjEdges.Add((ap, an, ap.Distance + an.Distance)); } @@ -134,7 +116,7 @@ public static List PairSeedingToInjPairings(this List pairedEdges, L var triplet = altInjEdges[0]; if (triplet.distance - seedPairing.Distance < seedPairing.Distance) { List tripPair = [triplet.ap, triplet.an]; - tripPair = tripPair.FilterEdgePairsWithCurrentPairs([..pairedEdges, ..seedPairings]); + tripPair = tripPair.FilterEdgePairsWithCurrentPairs([.. pairedEdges, .. seedPairings]); if (tripPair.Count == 2) { seedPairing.Weighting = 100; seedPairings[ix] = seedPairing; @@ -147,8 +129,32 @@ public static List PairSeedingToInjPairings(this List pairedEdges, L foreach (var pair in injPairings.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); } + return injPairings; + } + + public static List PairSeedingToInjPairings(this List pairedEdges, List nodes, short weighting) { + AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Peer Seeding"); +#pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters + List alreadyPaired = pairedEdges.Select(pe => new Edge(pe.NextId, pe.PrevId, pe.Distance, 100)).ToList(); +#pragma warning restore S2234 // Arguments should be passed in the same order as the method parameters + var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); + var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes); + List seedPairings = [.. alreadyPaired.BuildTravellingPairs(unpairedPrevNodes, unpairedNextNodes, weighting, 1).OrderBy(tp => tp.Distance)]; + seedPairings = seedPairings.Where(sp => sp.Weighting < 100).ToList().FilterEdgePairsWithCurrentPairs(pairedEdges); + + if (seedPairings.Count == 0) { + return pairedEdges; + } + + if (pairedEdges.Count == 0) { + // No zero length pairings, so choose the shortest edge pairing that there is, as the seed + seedPairings = [seedPairings[0]]; + } - pairedEdges = [.. pairedEdges, .. seedPairings, .. injPairings]; + //List injPairings = []; + //var unpairedNodes = unpairedPrevNodes.IntersectNodes(unpairedNextNodes); + //injPairings = pairedEdges.GetInjectablePairings(seedPairings, nodes, unpairedNodes); + pairedEdges = [.. pairedEdges, .. seedPairings]; //, .. injPairings]; return pairedEdges.CheckForLoops(); } diff --git a/GCodeClean/Merge/Utility.cs b/GCodeClean/Merge/Utility.cs index 236d0f1..9aa1427 100644 --- a/GCodeClean/Merge/Utility.cs +++ b/GCodeClean/Merge/Utility.cs @@ -60,22 +60,7 @@ public static List GetPrimaryEdges(this List nodes) { return primaryEdges; } - /// - /// If a title has been provided do a dump to the console of the supplied list of edge pairs - /// - /// - /// - public static void DebugEdgePairs(this List edgePairs, string title = "") { - if (title == "") { - return; - } - AnsiConsole.MarkupLine(title); - foreach (var pair in edgePairs.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting)).OrderBy(tps => tps.PrevId)) { - AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); - } - } - - public static bool HasProcessableEdges(this List edges) { + private static bool HasProcessableEdges(this List edges) { return edges.Exists(e => e.Weighting < 100); } @@ -83,10 +68,6 @@ public static List IntersectNodes(this List nodes, IEnumerable return nodes.IntersectBy(otherNodes.Select(on => on.Id), e => e.Id).ToList(); } - public static List IntersectEdges(this List edges, IEnumerable otherEdges) { - return edges.IntersectBy(otherEdges.Select(oe => (oe.PrevId, oe.NextId)), e => (e.PrevId, e.NextId)).ToList(); - } - public static List RemoveDuplicates(this IEnumerable edges) { List dedupEdges = []; @@ -187,33 +168,19 @@ public static List CheckForLoops(this List edges) { return orderedEdges; } - public static List OrderIds(this List nodeIds) { - List orderedIds = [nodeIds[0], .. nodeIds[1..^1].OrderBy(x => x), nodeIds[^1]]; - return orderedIds; - } - - public static bool IsShorter(this List edges, List originalNodes) { - var originalNodeIds = originalNodes.Select(n => n.Id).ToList(); - var newNodeIds = edges.GetNodeIds(); - var currentDistance = originalNodes.TotalDistance(originalNodeIds); - var newDistance = originalNodes.TotalDistance(newNodeIds); - return currentDistance >= newDistance; - } - - /// /// Filter supplied edge pairs for anything with a weighting of 100 or more /// /// /// A new list of filtered edge pairs public static List FilterEdgePairs(this List edges) { - if (!edges.HasProcessableEdges()) { - return []; - } - return edges.CheckForLoops() - .Where(ep => ep.Weighting < 100) - .Select(ep => new Edge(ep.PrevId, ep.NextId, ep.Distance, ep.Weighting)) - .ToList(); + if (!edges.HasProcessableEdges()) { + return []; + } + return edges.CheckForLoops() + .Where(ep => ep.Weighting < 100) + .Select(ep => new Edge(ep.PrevId, ep.NextId, ep.Distance, ep.Weighting)) + .ToList(); } /// From dcb782c108b6cb27df0955caf52bf4363fe5bd58 Mon Sep 17 00:00:00 2001 From: md8n Date: Tue, 26 Dec 2023 12:15:39 +0900 Subject: [PATCH 05/19] Better seed pairing --- GCodeClean/Merge/Utility.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/GCodeClean/Merge/Utility.cs b/GCodeClean/Merge/Utility.cs index 9aa1427..8659ee0 100644 --- a/GCodeClean/Merge/Utility.cs +++ b/GCodeClean/Merge/Utility.cs @@ -17,8 +17,9 @@ public static Node GetNode(this IEnumerable nodes, short id) { return nodes.First(n => n.Id == id); } - public static Edge GetEdge(this IEnumerable edges, short prevId, short nextId) { - return edges.First(n => n.PrevId == prevId && n.NextId == nextId); + public static Edge? GetEdge(this IEnumerable edges, short prevId, short nextId) { + var foundEdge = edges.FirstOrDefault(n => n.PrevId == prevId && n.NextId == nextId); + return (foundEdge.PrevId == 0 && foundEdge.NextId == 0 && foundEdge.Distance == 0) ? null : foundEdge; } public static decimal TotalDistance(this List nodes, List nodeIds) { @@ -52,7 +53,13 @@ public static List GetPrimaryEdges(this List nodes) { List primaryEdges = []; foreach (var (tool, id, start, end) in nodes) { var matchingNodes = nodes.FindAll(n => n.Tool == tool && n.Id != id && n.Start.X == end.X && n.Start.Y == end.Y); - if (matchingNodes.Count == 1) { + if (matchingNodes.Count > 1) { + // This may be some kind of 'peck-drilling' operation, whatever it is + // simply take the first node + // where the start and end are the same + matchingNodes = matchingNodes.Where(mn => mn.Start.X == mn.End.X && mn.Start.Y == mn.End.Y).Take(1).ToList(); + } + if (matchingNodes.Count == 1 && primaryEdges.GetEdge(matchingNodes[0].Id, id) == null) { primaryEdges.Add(new Edge(id, matchingNodes[0].Id, 0M, 0)); } } From 84f501d7acb1a7d6248462a069416e19e4c80f4e Mon Sep 17 00:00:00 2001 From: md8n Date: Tue, 26 Dec 2023 19:23:47 +0900 Subject: [PATCH 06/19] A LOT better --- GCodeClean/Merge/MergeFile.cs | 56 +++++++++--- GCodeClean/Merge/Utility.cs | 155 +++++++++++++++++++++++++++++----- 2 files changed, 174 insertions(+), 37 deletions(-) diff --git a/GCodeClean/Merge/MergeFile.cs b/GCodeClean/Merge/MergeFile.cs index df15941..5b49140 100644 --- a/GCodeClean/Merge/MergeFile.cs +++ b/GCodeClean/Merge/MergeFile.cs @@ -70,7 +70,6 @@ public static List BuildTravellingPairs(this List knownLoopForkPairs .Select(unn => new Edge(upn.Id, unn.Id, (upn.End, unn.Start).Distance(), weighting)) .OrderBy(e => e.Distance) .Take(topCount); - // Then remove those where the inverse distance is less travellingPairs.AddRange(newPairEdges); } @@ -103,13 +102,9 @@ public static List GetInjectablePairings(this List pairedEdges, List List<(Edge ap, Edge an, decimal distance)> altInjEdges = []; foreach (var ap in altPrevEdges) { var an = altNextEdges.Find(an => an.PrevId == ap.NextId); -#pragma warning disable CS8073 -#pragma warning disable S2589 - if (an == null) { + if (an.PrevId == 0 && an.NextId == 0 && an.Distance == 0) { continue; } -#pragma warning restore S2589 -#pragma warning restore CS8073 altInjEdges.Add((ap, an, ap.Distance + an.Distance)); } altInjEdges = [.. altInjEdges.OrderBy(a => a.distance)]; @@ -135,6 +130,7 @@ public static List GetInjectablePairings(this List pairedEdges, List public static List PairSeedingToInjPairings(this List pairedEdges, List nodes, short weighting) { AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Peer Seeding"); #pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters + // Invert existing pairings, and mark as 'do not use' weighting = 100 List alreadyPaired = pairedEdges.Select(pe => new Edge(pe.NextId, pe.PrevId, pe.Distance, 100)).ToList(); #pragma warning restore S2234 // Arguments should be passed in the same order as the method parameters var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); @@ -151,11 +147,11 @@ public static List PairSeedingToInjPairings(this List pairedEdges, L seedPairings = [seedPairings[0]]; } - //List injPairings = []; - //var unpairedNodes = unpairedPrevNodes.IntersectNodes(unpairedNextNodes); - //injPairings = pairedEdges.GetInjectablePairings(seedPairings, nodes, unpairedNodes); - pairedEdges = [.. pairedEdges, .. seedPairings]; //, .. injPairings]; - return pairedEdges.CheckForLoops(); + List injPairings; + var unpairedNodes = unpairedPrevNodes.IntersectNodes(unpairedNextNodes); + injPairings = pairedEdges.GetInjectablePairings(seedPairings, nodes, unpairedNodes); + pairedEdges = [.. pairedEdges, .. seedPairings, .. injPairings]; + return pairedEdges.CheckForLoops().Where(sp => sp.Weighting < 100).ToList(); } public static List BuildResidualPairs(this List pairedEdges, List nodes, short weighting) { @@ -164,7 +160,10 @@ public static List BuildResidualPairs(this List pairedEdges, List n.Id); List empty = []; - List residualPairs = empty.BuildTravellingPairs(nodes.Where(n => unpairedPrevNodes.Contains(n.Id)).ToList(), nodes.Where(n => unpairedNextNodes.Contains(n.Id)).ToList(), weighting); + List residualPairs = empty.BuildTravellingPairs( + nodes.Where(n => unpairedPrevNodes.Contains(n.Id)).ToList(), + nodes.Where(n => unpairedNextNodes.Contains(n.Id)).ToList(), + weighting); for (var ix = residualPairs.Count - 1; ix >= 0; ix--) { List residualPrimary = [residualPairs[ix]]; @@ -174,9 +173,15 @@ public static List BuildResidualPairs(this List pairedEdges, List (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { + // AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); + //} + List finalPairs = []; while (residualPairs.Count > 1) { - var residualPrimary = residualPairs.OrderByDescending(rp => rp.NextId).ThenBy(rp => rp.Distance).First(); + var firstNextId = residualPairs.OrderByDescending(rp => rp.NextId).First().NextId; + var residualPrimary = residualPairs.Where(rp => rp.NextId == firstNextId).OrderBy(rp => rp.Distance).First(); residualPairs = residualPairs.Where(rp => rp.PrevId != residualPrimary.PrevId && rp.NextId != residualPrimary.NextId).ToList(); finalPairs.Add(residualPrimary); } @@ -192,7 +197,9 @@ public static void MergeFile(this string inputFolder) return; } - var nodes = inputFolder.GetNodes().ToList(); + //var offset = 0; + //var take = 210; + var nodes = inputFolder.GetNodes().ToList(); // .Skip(offset).Take(take) var tools = nodes.Select(n => n.Tool).Distinct().ToList(); if (tools.Count > 1) { @@ -200,6 +207,11 @@ public static void MergeFile(this string inputFolder) return; } + //AnsiConsole.MarkupLine($"Nodes:"); + //foreach (var node in nodes.Select(n => (n.Id, n.Start, n.End))) { + // AnsiConsole.MarkupLine($"[bold yellow]{node}[/]"); + //} + var currentDistance = nodes.TotalDistance(nodes.Select(n => n.Id).ToList()); var pairedEdges = nodes.GetPrimaryEdges(); @@ -210,6 +222,12 @@ public static void MergeFile(this string inputFolder) int postCount = 0; short weighting = 1; + do { + prevCount = pairedEdges.Count; + pairedEdges = pairedEdges.GetSecondaryEdges(nodes, weighting++); + postCount = pairedEdges.Count; + } while (prevCount < postCount); + do { prevCount = pairedEdges.Count; pairedEdges = pairedEdges.PairSeedingToInjPairings(nodes, weighting++); @@ -228,6 +246,16 @@ public static void MergeFile(this string inputFolder) unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); } + // Make a final decision about rotating the whole list + var firstNode = nodes.GetNode(pairedEdges[0].PrevId); + var lastNode = nodes.GetNode(pairedEdges[^1].NextId); + var maxEdge = pairedEdges.OrderByDescending(pe => pe.Distance).FirstOrDefault(); + var lastToFirstEdge = new Edge(lastNode.Id, firstNode.Id, (lastNode.End, firstNode.Start).Distance(), weighting); + if (lastToFirstEdge.Distance < maxEdge.Distance) { + var maxEdgeIx = pairedEdges.IndexOf(maxEdge); + pairedEdges = [..pairedEdges[(maxEdgeIx+1)..], lastToFirstEdge, ..pairedEdges[0..maxEdgeIx]]; + } + //AnsiConsole.MarkupLine($"Pairings that were good:"); //foreach (var pair in pairedEdges.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { // AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); diff --git a/GCodeClean/Merge/Utility.cs b/GCodeClean/Merge/Utility.cs index 8659ee0..7a91cb4 100644 --- a/GCodeClean/Merge/Utility.cs +++ b/GCodeClean/Merge/Utility.cs @@ -1,6 +1,7 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. +using System; using System.Collections.Generic; using System.Linq; @@ -22,11 +23,21 @@ public static Node GetNode(this IEnumerable nodes, short id) { return (foundEdge.PrevId == 0 && foundEdge.NextId == 0 && foundEdge.Distance == 0) ? null : foundEdge; } + public static Edge? GetEdgeByPrevId(this IEnumerable edges, short prevId) { + var foundEdge = edges.FirstOrDefault(n => n.PrevId == prevId); + return (foundEdge.PrevId == 0 && foundEdge.NextId == 0 && foundEdge.Distance == 0) ? null : foundEdge; + } + + public static Edge? GetEdgeByNextId(this IEnumerable edges, short nextId) { + var foundEdge = edges.FirstOrDefault(n => n.NextId == nextId); + return (foundEdge.PrevId == 0 && foundEdge.NextId == 0 && foundEdge.Distance == 0) ? null : foundEdge; + } + public static decimal TotalDistance(this List nodes, List nodeIds) { var distance = 0M; for (var ix = 0; ix < nodeIds.Count - 1; ix++) { - var prevNode = nodes[nodeIds[ix]]; - var nextNode = nodes[nodeIds[ix + 1]]; + var prevNode = nodes.GetNode(nodeIds[ix]); + var nextNode = nodes.GetNode(nodeIds[ix + 1]); distance += (prevNode.End, nextNode.Start).Distance(); } return distance; @@ -43,6 +54,25 @@ public static List GetNodeIds(this List edges) { return nodeIds; } + /// + /// Converts a list of node Ids, into a linked list of edges + /// + /// + /// + public static List GetEdges(this List nodeIds, List edges) { + List nodeListEdges = []; + for (var jx = 0; jx < nodeIds.Count - 1; jx++) { + var nlEdge = edges.GetEdge(nodeIds[jx], nodeIds[jx + 1]); + if (nlEdge == null) { + // There's no way this could happen excluding some weird programmer error + continue; + } + nodeListEdges.Add((Edge)nlEdge); + } + + return nodeListEdges; + } + /// /// Identify primary pairings of cutting paths, where the end of one cutting path is the same as the start of one other cutting path. /// These pairings will not be changed in future passes unless a loop is identified @@ -50,6 +80,8 @@ public static List GetNodeIds(this List edges) { /// /// public static List GetPrimaryEdges(this List nodes) { + AnsiConsole.MarkupLine($"Pass [bold yellow]0[/]: Primary Edges"); + List primaryEdges = []; foreach (var (tool, id, start, end) in nodes) { var matchingNodes = nodes.FindAll(n => n.Tool == tool && n.Id != id && n.Start.X == end.X && n.Start.Y == end.Y); @@ -67,6 +99,38 @@ public static List GetPrimaryEdges(this List nodes) { return primaryEdges; } + /// + /// Identify secondary pairings of cutting paths, where the end of one cutting path is the same as the start of one other cutting path. + /// These pairings will not be changed in future passes unless a loop is identified + /// + /// + /// + /// + /// + public static List GetSecondaryEdges(this List pairedEdges, List nodes, short weighting) { + AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Secondary Edges"); +#pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters + // Invert existing pairings, and mark as 'do not use' weighting = 100 + List alreadyPaired = pairedEdges.Select(pe => new Edge(pe.NextId, pe.PrevId, pe.Distance, 100)).ToList(); +#pragma warning restore S2234 // Arguments should be passed in the same order as the method parameters + var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); + var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes); + List seedPairings = [.. alreadyPaired.BuildTravellingPairs(unpairedPrevNodes, unpairedNextNodes, weighting, 1).Where(sp => sp.Distance == 0)]; + seedPairings = seedPairings.Where(sp => sp.Weighting < 100).ToList().FilterEdgePairsWithCurrentPairs(pairedEdges); + + if (seedPairings.Count == 0) { + return pairedEdges; + } + + if (pairedEdges.Count == 0) { + // No zero length pairings, so choose the shortest edge pairing that there is, as the seed + seedPairings = [seedPairings[0]]; + } + + pairedEdges = [.. pairedEdges, .. seedPairings]; + return pairedEdges.CheckForLoops().Where(sp => sp.Weighting < 100).ToList(); + } + private static bool HasProcessableEdges(this List edges) { return edges.Exists(e => e.Weighting < 100); } @@ -116,15 +180,48 @@ public static List CheckForLoops(this List edges) { } foreach (var nodeList in nodeLists) { + var hasFork = false; if (nodeList[0..^1].Contains(edge.PrevId)) { - // Fork detected - within a nodelist - edge.Weighting = 100; // Do not use this - testEdges[ix] = edge; + hasFork = true; } if (nodeList[1..].Contains(edge.NextId)) { + hasFork = true; + } + if (hasFork) { // Fork detected - within a nodelist - edge.Weighting = 100; // Do not use this - testEdges[ix] = edge; + List nodeListEdges = nodeList.GetEdges(testEdges); + // Check for inversions + if (nodeListEdges.GetEdge(edge.NextId, edge.PrevId) != null) { + // A simple inversion + edge.Weighting = 100; // Do not use this + testEdges[ix] = edge; + } else { + var altEdge = (Edge)(nodeListEdges.GetEdgeByPrevId(edge.PrevId) ?? nodeListEdges.GetEdgeByNextId(edge.NextId)); + if (altEdge.Distance <= edge.Distance) { + edge.Weighting = 100; // Do not use this + testEdges[ix] = edge; + } else { + // Make a decision about the fork + var altEdgeIx = nodeListEdges.IndexOf(altEdge); + if (altEdgeIx != 0 && altEdgeIx != nodeListEdges.Count - 1) { + // If it is not effectively at the start or end of the nodelist, we'll reject it + edge.Weighting = 100; // Do not use this + testEdges[ix] = edge; + } else { + var altTestEdge = (Edge)testEdges.GetEdge(altEdge.PrevId, altEdge.NextId); + var altTestEdgeIx = testEdges.IndexOf(altTestEdge); + altTestEdge.Weighting = 100; + testEdges[altTestEdgeIx] = altTestEdge; + nodeListEdges[altEdgeIx] = edge; + if (altEdgeIx == 0) { + nodeList[0] = edge.PrevId; + } else { + nodeList[altEdgeIx] = edge.NextId; + } + } + } + } + continue; } } @@ -136,22 +233,34 @@ public static List CheckForLoops(this List edges) { continue; } - if (matchingNodeListPreceeding != null && matchingNodeListPreceeding.Contains(edge.NextId)) { - // Loop detected - edge.Weighting = 100; // Do not use this - testEdges[ix] = edge; - } - if (matchingNodeListSucceeding != null && matchingNodeListSucceeding.Contains(edge.PrevId)) { - // Loop detected - edge.Weighting = 100; // Do not use this - testEdges[ix] = edge; - } - - if (matchingNodeListPreceeding != null && matchingNodeListSucceeding != null && edge.Weighting < 100) { - // Merge two nodelists into one - unless its a loop - matchingNodeListPreceeding.AddRange(matchingNodeListSucceeding); - nodeLists.Remove(matchingNodeListSucceeding); - continue; + if (matchingNodeListPreceeding != null) { + if (matchingNodeListPreceeding[0] == edge.NextId) { + // Loop detected - pop the longest edge + // We could alternatively check if preceeding and succeeding are the same nodelist + LinkedList nodeListEdges = new LinkedList(matchingNodeListPreceeding.GetEdges(testEdges)); + var distances = nodeListEdges.Select(nle => nle.Distance).Distinct().ToList(); + if (distances.Max() <= edge.Distance) { + edge.Weighting = 100; // Do not use this + testEdges[ix] = edge; + } else { + // Find and pop the longest edge + nodeListEdges.AddLast(edge); + var popEdge = nodeListEdges.OrderByDescending(nle => nle.Distance).First(); + if (popEdge == edge) { + edge.Weighting = 100; // Do not use this + testEdges[ix] = edge; + } else { + // we need to rotate the list + popEdge.Weighting = 100; + throw new Exception("Need to rotate, dunno how"); + } + } + } else if (matchingNodeListSucceeding != null && matchingNodeListPreceeding != matchingNodeListSucceeding) { + // Merge two nodelists into one - unless its a loop + matchingNodeListPreceeding.AddRange(matchingNodeListSucceeding); + nodeLists.Remove(matchingNodeListSucceeding); + continue; + } } if (matchingNodeListPreceeding != null && !matchingNodeListPreceeding.Contains(edge.NextId)) { From 71177271b0aa5cdc2d298b1f88309c41d447e2b6 Mon Sep 17 00:00:00 2001 From: md8n Date: Wed, 27 Dec 2023 07:48:04 +0900 Subject: [PATCH 07/19] and nodelist rotation --- GCodeClean/Merge/MergeFile.cs | 3 +-- GCodeClean/Merge/Utility.cs | 11 ++++++++++- SECURITY.md | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/GCodeClean/Merge/MergeFile.cs b/GCodeClean/Merge/MergeFile.cs index 5b49140..8a29b06 100644 --- a/GCodeClean/Merge/MergeFile.cs +++ b/GCodeClean/Merge/MergeFile.cs @@ -236,8 +236,7 @@ public static void MergeFile(this string inputFolder) var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); while (unpairedPrevNodes.Count > 1) { - weighting++; - List residualPairs = pairedEdges.BuildResidualPairs(nodes, weighting); + List residualPairs = pairedEdges.BuildResidualPairs(nodes, weighting++); pairedEdges = [.. pairedEdges, .. residualPairs]; pairedEdges = pairedEdges.CheckForLoops(); diff --git a/GCodeClean/Merge/Utility.cs b/GCodeClean/Merge/Utility.cs index 7a91cb4..7d21899 100644 --- a/GCodeClean/Merge/Utility.cs +++ b/GCodeClean/Merge/Utility.cs @@ -251,8 +251,17 @@ public static List CheckForLoops(this List edges) { testEdges[ix] = edge; } else { // we need to rotate the list + while(nodeListEdges.First.Value != popEdge) { + var firstEdge = nodeListEdges.First; + nodeListEdges.RemoveFirst(); + nodeListEdges.AddLast(firstEdge); + } + var popEdgeIx = testEdges.IndexOf(popEdge); popEdge.Weighting = 100; - throw new Exception("Need to rotate, dunno how"); + testEdges[popEdgeIx] = popEdge; + nodeListEdges.RemoveFirst(); + var nodeListIx = nodeLists.IndexOf(matchingNodeListPreceeding); + nodeLists[nodeListIx] = nodeListEdges.ToList().GetNodeIds(); } } } else if (matchingNodeListSucceeding != null && matchingNodeListPreceeding != matchingNodeListSucceeding) { diff --git a/SECURITY.md b/SECURITY.md index 4691db2..1ba41c7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | | ------- | ------------------ | -| 1.2.3 | :white_check_mark: | +| 1.3.0 | :white_check_mark: | ## Reporting a Vulnerability From 4ae1c246a964e0e68879cbdc2f1ad4d60adfbb75 Mon Sep 17 00:00:00 2001 From: md8n Date: Wed, 27 Dec 2023 09:46:07 +0900 Subject: [PATCH 08/19] Refactor out algorithm better --- GCodeClean/Merge/{Utility.cs => Algorithm.cs} | 207 +++++++++------- GCodeClean/Merge/Edges.cs | 53 ++++ GCodeClean/Merge/MergeFile.cs | 234 +----------------- GCodeClean/Merge/NodeFileIO.cs | 48 ++++ GCodeClean/Merge/Nodes.cs | 32 +++ GCodeClean/Merge/NodesAndEdges.cs | 94 +++++++ GCodeClean/Merge/Objects.cs | 1 + GCodeClean/Merge/Utilities.cs | 75 ++++++ 8 files changed, 425 insertions(+), 319 deletions(-) rename GCodeClean/Merge/{Utility.cs => Algorithm.cs} (70%) create mode 100644 GCodeClean/Merge/Edges.cs create mode 100644 GCodeClean/Merge/NodeFileIO.cs create mode 100644 GCodeClean/Merge/Nodes.cs create mode 100644 GCodeClean/Merge/NodesAndEdges.cs create mode 100644 GCodeClean/Merge/Utilities.cs diff --git a/GCodeClean/Merge/Utility.cs b/GCodeClean/Merge/Algorithm.cs similarity index 70% rename from GCodeClean/Merge/Utility.cs rename to GCodeClean/Merge/Algorithm.cs index 7d21899..4724966 100644 --- a/GCodeClean/Merge/Utility.cs +++ b/GCodeClean/Merge/Algorithm.cs @@ -1,78 +1,18 @@ -// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; using System.Linq; -using Spectre.Console; - using GCodeClean.Processing; +using Spectre.Console; namespace GCodeClean.Merge -{ - public static class Utility +{ + public static class Algorithm { - public static Node GetNode(this IEnumerable nodes, short id) { - return nodes.First(n => n.Id == id); - } - - public static Edge? GetEdge(this IEnumerable edges, short prevId, short nextId) { - var foundEdge = edges.FirstOrDefault(n => n.PrevId == prevId && n.NextId == nextId); - return (foundEdge.PrevId == 0 && foundEdge.NextId == 0 && foundEdge.Distance == 0) ? null : foundEdge; - } - - public static Edge? GetEdgeByPrevId(this IEnumerable edges, short prevId) { - var foundEdge = edges.FirstOrDefault(n => n.PrevId == prevId); - return (foundEdge.PrevId == 0 && foundEdge.NextId == 0 && foundEdge.Distance == 0) ? null : foundEdge; - } - - public static Edge? GetEdgeByNextId(this IEnumerable edges, short nextId) { - var foundEdge = edges.FirstOrDefault(n => n.NextId == nextId); - return (foundEdge.PrevId == 0 && foundEdge.NextId == 0 && foundEdge.Distance == 0) ? null : foundEdge; - } - - public static decimal TotalDistance(this List nodes, List nodeIds) { - var distance = 0M; - for (var ix = 0; ix < nodeIds.Count - 1; ix++) { - var prevNode = nodes.GetNode(nodeIds[ix]); - var nextNode = nodes.GetNode(nodeIds[ix + 1]); - distance += (prevNode.End, nextNode.Start).Distance(); - } - return distance; - } - - /// - /// Converts a list of edges (must be contiguous chain) into a list of node Ids - /// - /// - /// - public static List GetNodeIds(this List edges) { - List nodeIds = [edges[0].PrevId]; - nodeIds.AddRange(edges.Select(e => e.NextId)); - return nodeIds; - } - - /// - /// Converts a list of node Ids, into a linked list of edges - /// - /// - /// - public static List GetEdges(this List nodeIds, List edges) { - List nodeListEdges = []; - for (var jx = 0; jx < nodeIds.Count - 1; jx++) { - var nlEdge = edges.GetEdge(nodeIds[jx], nodeIds[jx + 1]); - if (nlEdge == null) { - // There's no way this could happen excluding some weird programmer error - continue; - } - nodeListEdges.Add((Edge)nlEdge); - } - - return nodeListEdges; - } - /// /// Identify primary pairings of cutting paths, where the end of one cutting path is the same as the start of one other cutting path. /// These pairings will not be changed in future passes unless a loop is identified @@ -109,13 +49,71 @@ public static List GetPrimaryEdges(this List nodes) { /// public static List GetSecondaryEdges(this List pairedEdges, List nodes, short weighting) { AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Secondary Edges"); + List seedPairings = [.. pairedEdges.GetResidualSeedPairings(nodes, weighting).Where(sp => sp.Distance == 0)]; + return seedPairings.GetFilteredSeedPairings(pairedEdges); + } + + public static List PairSeedingToInjPairings(this List pairedEdges, List nodes, short weighting) { + AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Peer Seeding"); + List seedPairings = [.. pairedEdges.GetResidualSeedPairings(nodes, weighting).OrderBy(tp => tp.Distance)]; + + /* Injecting nodes into other existing edge pairings, not found to be useful */ + //List injPairings; + //var unpairedNodes = unpairedPrevNodes.IntersectNodes(unpairedNextNodes); + //injPairings = pairedEdges.GetInjectablePairings(seedPairings, nodes, unpairedNodes); + //pairedEdges = [.. pairedEdges, .. seedPairings, .. injPairings]; + + return seedPairings.GetFilteredSeedPairings(pairedEdges); + } + + public static List BuildResidualPairs(this List pairedEdges, List nodes, short weighting) { + AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Residual pairs"); + var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes).Select(n => n.Id); + var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes).Select(n => n.Id); + + List empty = []; + List residualPairs = empty.BuildTravellingPairs( + nodes.Where(n => unpairedPrevNodes.Contains(n.Id)).ToList(), + nodes.Where(n => unpairedNextNodes.Contains(n.Id)).ToList(), + weighting); + + for (var ix = residualPairs.Count - 1; ix >= 0; ix--) { + List residualPrimary = [residualPairs[ix]]; + List residualTest = residualPrimary.FilterEdgePairsWithCurrentPairs(pairedEdges); + if (residualTest.Count == 0) { + residualPairs.Remove(residualPrimary[0]); + } + } + + //AnsiConsole.MarkupLine($"Residual Pairings that were good:"); + //foreach (var pair in residualPairs.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { + // AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); + //} + + List finalPairs = []; + while (residualPairs.Count > 1) { + var firstNextId = residualPairs.OrderByDescending(rp => rp.NextId).First().NextId; + var residualPrimary = residualPairs.Where(rp => rp.NextId == firstNextId).OrderBy(rp => rp.Distance).First(); + residualPairs = residualPairs.Where(rp => rp.PrevId != residualPrimary.PrevId && rp.NextId != residualPrimary.NextId).ToList(); + finalPairs.Add(residualPrimary); + } + + return finalPairs; + } + + public static List GetResidualSeedPairings(this List pairedEdges, List nodes, short weighting) { #pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters // Invert existing pairings, and mark as 'do not use' weighting = 100 List alreadyPaired = pairedEdges.Select(pe => new Edge(pe.NextId, pe.PrevId, pe.Distance, 100)).ToList(); #pragma warning restore S2234 // Arguments should be passed in the same order as the method parameters var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes); - List seedPairings = [.. alreadyPaired.BuildTravellingPairs(unpairedPrevNodes, unpairedNextNodes, weighting, 1).Where(sp => sp.Distance == 0)]; + List seedPairings = [.. alreadyPaired.BuildTravellingPairs(unpairedPrevNodes, unpairedNextNodes, weighting, 1)]; + + return seedPairings; + } + + public static List GetFilteredSeedPairings(this List seedPairings, List pairedEdges) { seedPairings = seedPairings.Where(sp => sp.Weighting < 100).ToList().FilterEdgePairsWithCurrentPairs(pairedEdges); if (seedPairings.Count == 0) { @@ -131,27 +129,6 @@ public static List GetSecondaryEdges(this List pairedEdges, List sp.Weighting < 100).ToList(); } - private static bool HasProcessableEdges(this List edges) { - return edges.Exists(e => e.Weighting < 100); - } - - public static List IntersectNodes(this List nodes, IEnumerable otherNodes) { - return nodes.IntersectBy(otherNodes.Select(on => on.Id), e => e.Id).ToList(); - } - - public static List RemoveDuplicates(this IEnumerable edges) { - List dedupEdges = []; - - foreach (var edge in edges) { - if (dedupEdges.Exists(de => de.PrevId == edge.PrevId && de.NextId == edge.NextId)) { - continue; - } - dedupEdges.Add(edge); - } - - return dedupEdges; - } - /// /// Takes the supplied list of edges and determines if there are any loops or forks /// @@ -251,7 +228,7 @@ public static List CheckForLoops(this List edges) { testEdges[ix] = edge; } else { // we need to rotate the list - while(nodeListEdges.First.Value != popEdge) { + while (nodeListEdges.First.Value != popEdge) { var firstEdge = nodeListEdges.First; nodeListEdges.RemoveFirst(); nodeListEdges.AddLast(firstEdge); @@ -325,11 +302,57 @@ public static List FilterEdgePairsWithCurrentPairs(this List edges, return firstFilteredEP.Where(ff => tempEdges.Exists(te => te.PrevId == ff.PrevId && te.NextId == ff.NextId)).ToList(); } - public static (List startIds, List endIds) GetStartsAndEnds(this List edges) { - var starts = edges.Where(e => e.Weighting < 100).Select(pe => pe.PrevId).ToList(); - var ends = edges.Where(e => e.Weighting < 100).Select(pe => pe.NextId).ToList(); - // Find the starting node Ids - one for each tool - if the tool is used for more than one cutting path - return (starts.Where(si => !ends.Contains(si)).ToList(), ends.Where(ei => !starts.Contains(ei)).ToList()); + /// + /// Reorder the list of nodes to achieve a shorter travelling distance + /// + /// + /// + public static List TravellingReorder(this List nodes) + { + var pairedEdges = nodes.GetPrimaryEdges(); + + int prevCount = 0; + int postCount = 0; + short weighting = 1; + + do { + prevCount = pairedEdges.Count; + pairedEdges = pairedEdges.GetSecondaryEdges(nodes, weighting++); + postCount = pairedEdges.Count; + } while (prevCount < postCount); + + do { + prevCount = pairedEdges.Count; + pairedEdges = pairedEdges.PairSeedingToInjPairings(nodes, weighting++); + postCount = pairedEdges.Count; + } while (prevCount < postCount); + + var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); + while (unpairedPrevNodes.Count > 1) { + List residualPairs = pairedEdges.BuildResidualPairs(nodes, weighting++); + + pairedEdges = [.. pairedEdges, .. residualPairs]; + pairedEdges = pairedEdges.CheckForLoops().Where(pe => pe.Weighting < 100).ToList(); + + unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); + } + + // Make a final decision about rotating the whole list + var firstNode = nodes.GetNode(pairedEdges[0].PrevId); + var lastNode = nodes.GetNode(pairedEdges[^1].NextId); + var maxEdge = pairedEdges.OrderByDescending(pe => pe.Distance).FirstOrDefault(); + var lastToFirstEdge = new Edge(lastNode.Id, firstNode.Id, (lastNode.End, firstNode.Start).Distance(), weighting); + if (lastToFirstEdge.Distance < maxEdge.Distance) { + var maxEdgeIx = pairedEdges.IndexOf(maxEdge); + pairedEdges = [..pairedEdges[(maxEdgeIx+1)..], lastToFirstEdge, ..pairedEdges[0..maxEdgeIx]]; + } + + //AnsiConsole.MarkupLine($"Pairings that were good:"); + //foreach (var pair in pairedEdges.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { + // AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); + //} + + return pairedEdges; } } } diff --git a/GCodeClean/Merge/Edges.cs b/GCodeClean/Merge/Edges.cs new file mode 100644 index 0000000..eca3cfb --- /dev/null +++ b/GCodeClean/Merge/Edges.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System.Collections.Generic; +using System.Linq; + +using Spectre.Console; + + +namespace GCodeClean.Merge +{ + public static class Edges + { + public static Edge? GetEdge(this IEnumerable edges, short prevId, short nextId) { + var foundEdge = edges.FirstOrDefault(n => n.PrevId == prevId && n.NextId == nextId); + return (foundEdge.PrevId == 0 && foundEdge.NextId == 0 && foundEdge.Distance == 0) ? null : foundEdge; + } + + public static Edge? GetEdgeByPrevId(this IEnumerable edges, short prevId) { + var foundEdge = edges.FirstOrDefault(n => n.PrevId == prevId); + return (foundEdge.PrevId == 0 && foundEdge.NextId == 0 && foundEdge.Distance == 0) ? null : foundEdge; + } + + public static Edge? GetEdgeByNextId(this IEnumerable edges, short nextId) { + var foundEdge = edges.FirstOrDefault(n => n.NextId == nextId); + return (foundEdge.PrevId == 0 && foundEdge.NextId == 0 && foundEdge.Distance == 0) ? null : foundEdge; + } + + public static bool HasProcessableEdges(this List edges) { + return edges.Exists(e => e.Weighting < 100); + } + + public static List RemoveDuplicates(this IEnumerable edges) { + List dedupEdges = []; + + foreach (var edge in edges) { + if (dedupEdges.Exists(de => de.PrevId == edge.PrevId && de.NextId == edge.NextId)) { + continue; + } + dedupEdges.Add(edge); + } + + return dedupEdges; + } + + public static (List startIds, List endIds) GetStartsAndEnds(this List edges) { + var starts = edges.Where(e => e.Weighting < 100).Select(pe => pe.PrevId).ToList(); + var ends = edges.Where(e => e.Weighting < 100).Select(pe => pe.NextId).ToList(); + // Find the starting node Ids - one for each tool - if the tool is used for more than one cutting path + return (starts.Where(si => !ends.Contains(si)).ToList(), ends.Where(ei => !starts.Contains(ei)).ToList()); + } + } +} diff --git a/GCodeClean/Merge/MergeFile.cs b/GCodeClean/Merge/MergeFile.cs index 8a29b06..e06d3ad 100644 --- a/GCodeClean/Merge/MergeFile.cs +++ b/GCodeClean/Merge/MergeFile.cs @@ -1,205 +1,24 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. -using System; -using System.Collections.Generic; -using System.IO; using System.Linq; -using GCodeClean.Processing; -using GCodeClean.Structure; - using Spectre.Console; + namespace GCodeClean.Merge { public static class Merge { - private static readonly char[] separator = ['_']; - - public static string ToSimpleString(this Edge edge) => $"{edge.PrevId}<->{edge.NextId}"; - - /// - /// Scan through the file for 'travelling' comments and build a list of them - /// - /// - /// - public static List GetNodes(this string inputFolder) { - var fileEntries = Directory.GetFiles(inputFolder); - Array.Sort(fileEntries); - List nodes = []; - foreach (var filePath in fileEntries) { - var fileNameParts = Path.GetFileNameWithoutExtension(filePath).Split(separator); - var tool = fileNameParts[0]; - var id = Int16.Parse(fileNameParts[1]); - var startCoords = fileNameParts[2].Replace("X", "").Split("Y").Select(c => decimal.Parse(c)).ToArray(); - var endCoords = fileNameParts[3].Replace("X", "").Split("Y").Select(c => decimal.Parse(c)).ToArray(); - var start = new Coord(startCoords[0], startCoords[1]); - var end = new Coord(endCoords[0], endCoords[1]); - nodes.Add(new Node(tool, id, start, end)); - } - - return nodes; - } - - /// - /// Find all nodes that are not pointed to as the Previous node by an edge - /// - /// - /// - /// - public static List UnpairedPrevNodes(this List edgePairs, List nodes) { - return nodes.Where(n => !edgePairs.Exists(ep => ep.PrevId == n.Id)).ToList(); - } - - public static List UnpairedNextNodes(this List edgePairs, List nodes) { - return nodes.Where(n => !edgePairs.Exists(ep => ep.NextId == n.Id)).ToList(); - } - - public static List BuildTravellingPairs(this List knownLoopForkPairs, List unpairedPrevNodes, List unpairedNextNodes, short weighting, int topCount = 10) { - List travellingPairs = []; - foreach (var upn in unpairedPrevNodes) { - // Match other nodes (not self) that use the same tool - // and not a known loop forming edge pair - // and take the top 'count' of the results (default 10) - var newPairEdges = unpairedNextNodes - .Where(unn => unn.Id != upn.Id - && unn.Tool == upn.Tool - && !knownLoopForkPairs.Exists(pe => pe.PrevId == upn.Id && pe.NextId == unn.Id) - ) - .Select(unn => new Edge(upn.Id, unn.Id, (upn.End, unn.Start).Distance(), weighting)) - .OrderBy(e => e.Distance) - .Take(topCount); - travellingPairs.AddRange(newPairEdges); - } - - return travellingPairs; - } - - public static List GetInjectablePairings(this List pairedEdges, List seedPairings, List nodes, List unpairedNodes) { - List injPairings = []; - for (var ix = 0; ix < seedPairings.Count; ix++) { - var seedPairing = seedPairings[ix]; - if (seedPairing.Weighting >= 100) { - continue; - } - var prevNode = nodes.GetNode(seedPairing.PrevId); - var nextNode = nodes.GetNode(seedPairing.NextId); - var fun = unpairedNodes.Where(unn => unn.Id != prevNode.Id && unn.Id != nextNode.Id); - var altPrevEdges = fun - .Select(unn => new Edge(prevNode.Id, unn.Id, (prevNode.End, unn.Start).Distance(), 10)) - .OrderBy(e => e.NextId) - .Take(10) - .ToList(); - var altNextEdges = fun - .Select(upn => new Edge(upn.Id, nextNode.Id, (upn.End, nextNode.Start).Distance(), 10)) - .OrderBy(e => e.PrevId) - .Take(10) - .ToList(); - if (altPrevEdges.Count == 0 || altNextEdges.Count == 0) { - continue; - } - List<(Edge ap, Edge an, decimal distance)> altInjEdges = []; - foreach (var ap in altPrevEdges) { - var an = altNextEdges.Find(an => an.PrevId == ap.NextId); - if (an.PrevId == 0 && an.NextId == 0 && an.Distance == 0) { - continue; - } - altInjEdges.Add((ap, an, ap.Distance + an.Distance)); - } - altInjEdges = [.. altInjEdges.OrderBy(a => a.distance)]; - var triplet = altInjEdges[0]; - if (triplet.distance - seedPairing.Distance < seedPairing.Distance) { - List tripPair = [triplet.ap, triplet.an]; - tripPair = tripPair.FilterEdgePairsWithCurrentPairs([.. pairedEdges, .. seedPairings]); - if (tripPair.Count == 2) { - seedPairing.Weighting = 100; - seedPairings[ix] = seedPairing; - unpairedNodes.Remove(unpairedNodes.GetNode(triplet.ap.NextId)); - injPairings.AddRange([triplet.ap, triplet.an]); - } - } - } - AnsiConsole.MarkupLine($"Injection Pairings:"); - foreach (var pair in injPairings.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { - AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); - } - return injPairings; - } - - public static List PairSeedingToInjPairings(this List pairedEdges, List nodes, short weighting) { - AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Peer Seeding"); -#pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters - // Invert existing pairings, and mark as 'do not use' weighting = 100 - List alreadyPaired = pairedEdges.Select(pe => new Edge(pe.NextId, pe.PrevId, pe.Distance, 100)).ToList(); -#pragma warning restore S2234 // Arguments should be passed in the same order as the method parameters - var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); - var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes); - List seedPairings = [.. alreadyPaired.BuildTravellingPairs(unpairedPrevNodes, unpairedNextNodes, weighting, 1).OrderBy(tp => tp.Distance)]; - seedPairings = seedPairings.Where(sp => sp.Weighting < 100).ToList().FilterEdgePairsWithCurrentPairs(pairedEdges); - - if (seedPairings.Count == 0) { - return pairedEdges; - } - - if (pairedEdges.Count == 0) { - // No zero length pairings, so choose the shortest edge pairing that there is, as the seed - seedPairings = [seedPairings[0]]; - } - - List injPairings; - var unpairedNodes = unpairedPrevNodes.IntersectNodes(unpairedNextNodes); - injPairings = pairedEdges.GetInjectablePairings(seedPairings, nodes, unpairedNodes); - pairedEdges = [.. pairedEdges, .. seedPairings, .. injPairings]; - return pairedEdges.CheckForLoops().Where(sp => sp.Weighting < 100).ToList(); - } - - public static List BuildResidualPairs(this List pairedEdges, List nodes, short weighting) { - AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Residual pairs"); - var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes).Select(n => n.Id); - var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes).Select(n => n.Id); - - List empty = []; - List residualPairs = empty.BuildTravellingPairs( - nodes.Where(n => unpairedPrevNodes.Contains(n.Id)).ToList(), - nodes.Where(n => unpairedNextNodes.Contains(n.Id)).ToList(), - weighting); - - for (var ix = residualPairs.Count - 1; ix >= 0; ix--) { - List residualPrimary = [residualPairs[ix]]; - List residualTest = residualPrimary.FilterEdgePairsWithCurrentPairs(pairedEdges); - if (residualTest.Count == 0) { - residualPairs.Remove(residualPrimary[0]); - } - } - - //AnsiConsole.MarkupLine($"Residual Pairings that were good:"); - //foreach (var pair in residualPairs.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { - // AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); - //} - - List finalPairs = []; - while (residualPairs.Count > 1) { - var firstNextId = residualPairs.OrderByDescending(rp => rp.NextId).First().NextId; - var residualPrimary = residualPairs.Where(rp => rp.NextId == firstNextId).OrderBy(rp => rp.Distance).First(); - residualPairs = residualPairs.Where(rp => rp.PrevId != residualPrimary.PrevId && rp.NextId != residualPrimary.NextId).ToList(); - finalPairs.Add(residualPrimary); - } - - return finalPairs; - } - public static void MergeFile(this string inputFolder) { - if (!Directory.Exists(inputFolder)) + if (!inputFolder.FolderExists()) { AnsiConsole.MarkupLine($"No such folder found. Nothing to see here, move along."); return; } - //var offset = 0; - //var take = 210; - var nodes = inputFolder.GetNodes().ToList(); // .Skip(offset).Take(take) + var nodes = inputFolder.GetNodes().ToList(); var tools = nodes.Select(n => n.Tool).Distinct().ToList(); if (tools.Count > 1) { @@ -214,46 +33,7 @@ public static void MergeFile(this string inputFolder) var currentDistance = nodes.TotalDistance(nodes.Select(n => n.Id).ToList()); - var pairedEdges = nodes.GetPrimaryEdges(); - - List startIds; - List endIds; - int prevCount = 0; - int postCount = 0; - short weighting = 1; - - do { - prevCount = pairedEdges.Count; - pairedEdges = pairedEdges.GetSecondaryEdges(nodes, weighting++); - postCount = pairedEdges.Count; - } while (prevCount < postCount); - - do { - prevCount = pairedEdges.Count; - pairedEdges = pairedEdges.PairSeedingToInjPairings(nodes, weighting++); - postCount = pairedEdges.Count; - } while (prevCount < postCount); - - var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); - while (unpairedPrevNodes.Count > 1) { - List residualPairs = pairedEdges.BuildResidualPairs(nodes, weighting++); - - pairedEdges = [.. pairedEdges, .. residualPairs]; - pairedEdges = pairedEdges.CheckForLoops(); - pairedEdges = pairedEdges.Where(pe => pe.Weighting < 100).ToList(); - - unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes); - } - - // Make a final decision about rotating the whole list - var firstNode = nodes.GetNode(pairedEdges[0].PrevId); - var lastNode = nodes.GetNode(pairedEdges[^1].NextId); - var maxEdge = pairedEdges.OrderByDescending(pe => pe.Distance).FirstOrDefault(); - var lastToFirstEdge = new Edge(lastNode.Id, firstNode.Id, (lastNode.End, firstNode.Start).Distance(), weighting); - if (lastToFirstEdge.Distance < maxEdge.Distance) { - var maxEdgeIx = pairedEdges.IndexOf(maxEdge); - pairedEdges = [..pairedEdges[(maxEdgeIx+1)..], lastToFirstEdge, ..pairedEdges[0..maxEdgeIx]]; - } + var pairedEdges = nodes.TravellingReorder(); //AnsiConsole.MarkupLine($"Pairings that were good:"); //foreach (var pair in pairedEdges.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { @@ -266,9 +46,9 @@ public static void MergeFile(this string inputFolder) AnsiConsole.MarkupLine($"Total nodes: {nodes.Count}"); AnsiConsole.MarkupLine($"Total edges: {pairedEdges.Count}"); - (startIds, endIds) = pairedEdges.GetStartsAndEnds(); - AnsiConsole.MarkupLine($"Starting node Ids: {string.Join(',', startIds)}"); - AnsiConsole.MarkupLine($"Ending node Ids: {string.Join(',', endIds)}"); + var (startIds, endIds) = pairedEdges.GetStartsAndEnds(); + AnsiConsole.MarkupLine($"Starting node Id: {string.Join(',', startIds)}"); + AnsiConsole.MarkupLine($"Ending node Id: {string.Join(',', endIds)}"); AnsiConsole.MarkupLine($"Current travelling distance: {currentDistance}"); AnsiConsole.MarkupLine($"New travelling distance: {newDistance}"); diff --git a/GCodeClean/Merge/NodeFileIO.cs b/GCodeClean/Merge/NodeFileIO.cs new file mode 100644 index 0000000..478fe2b --- /dev/null +++ b/GCodeClean/Merge/NodeFileIO.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using GCodeClean.Structure; + +using Spectre.Console; + +namespace GCodeClean.Merge +{ + public static class NodeFileIO + { + private static readonly char[] separator = ['_']; + + public static string ToSimpleString(this Edge edge) => $"{edge.PrevId}<->{edge.NextId}"; + + public static bool FolderExists(this string inputFolder) { + return Directory.Exists(inputFolder); + } + + /// + /// Scan through the file for 'travelling' comments and build a list of them + /// + /// + /// + public static List GetNodes(this string inputFolder) { + var fileEntries = Directory.GetFiles(inputFolder); + Array.Sort(fileEntries); + List nodes = []; + foreach (var filePath in fileEntries) { + var fileNameParts = Path.GetFileNameWithoutExtension(filePath).Split(separator); + var tool = fileNameParts[0]; + var id = Int16.Parse(fileNameParts[1]); + var startCoords = fileNameParts[2].Replace("X", "").Split("Y").Select(c => decimal.Parse(c)).ToArray(); + var endCoords = fileNameParts[3].Replace("X", "").Split("Y").Select(c => decimal.Parse(c)).ToArray(); + var start = new Coord(startCoords[0], startCoords[1]); + var end = new Coord(endCoords[0], endCoords[1]); + nodes.Add(new Node(tool, id, start, end)); + } + + return nodes; + } + } +} diff --git a/GCodeClean/Merge/Nodes.cs b/GCodeClean/Merge/Nodes.cs new file mode 100644 index 0000000..3804f36 --- /dev/null +++ b/GCodeClean/Merge/Nodes.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System.Collections.Generic; +using System.Linq; + +using GCodeClean.Processing; + + +namespace GCodeClean.Merge +{ + public static class Nodes + { + public static Node GetNode(this IEnumerable nodes, short id) { + return nodes.First(n => n.Id == id); + } + + public static decimal TotalDistance(this List nodes, List nodeIds) { + var distance = 0M; + for (var ix = 0; ix < nodeIds.Count - 1; ix++) { + var prevNode = nodes.GetNode(nodeIds[ix]); + var nextNode = nodes.GetNode(nodeIds[ix + 1]); + distance += (prevNode.End, nextNode.Start).Distance(); + } + return distance; + } + + public static List IntersectNodes(this List nodes, IEnumerable otherNodes) { + return nodes.IntersectBy(otherNodes.Select(on => on.Id), e => e.Id).ToList(); + } + } +} diff --git a/GCodeClean/Merge/NodesAndEdges.cs b/GCodeClean/Merge/NodesAndEdges.cs new file mode 100644 index 0000000..fe28bfb --- /dev/null +++ b/GCodeClean/Merge/NodesAndEdges.cs @@ -0,0 +1,94 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System.Collections.Generic; +using System.Linq; + +using Spectre.Console; + +using GCodeClean.Processing; + + +namespace GCodeClean.Merge +{ + public static class NodesAndEdges + { + /// + /// Converts a list of node Ids, into a linked list of edges + /// + /// + /// + public static List GetEdges(this List nodeIds, List edges) { + List nodeListEdges = []; + for (var jx = 0; jx < nodeIds.Count - 1; jx++) { + var nlEdge = edges.GetEdge(nodeIds[jx], nodeIds[jx + 1]); + if (nlEdge == null) { + // There's no way this could happen excluding some weird programmer error + continue; + } + nodeListEdges.Add((Edge)nlEdge); + } + + return nodeListEdges; + } + + /// + /// Converts a list of edges (must be contiguous chain) into a list of nodes + /// + /// + /// + public static List GetNodes(this List edges, List currentNodes) { + List nodeIds = edges.GetNodeIds(); + List newNodes = []; + foreach(var nodeId in nodeIds) { + newNodes.Add(currentNodes.GetNode(nodeId)); + } + return newNodes; + } + + /// + /// Converts a list of edges (must be contiguous chain) into a list of node Ids + /// + /// + /// + public static List GetNodeIds(this List edges) { + List nodeIds = [edges[0].PrevId]; + nodeIds.AddRange(edges.Select(e => e.NextId)); + return nodeIds; + } + + public static List BuildTravellingPairs(this List knownLoopForkPairs, List unpairedPrevNodes, List unpairedNextNodes, short weighting, int topCount = 10) { + List travellingPairs = []; + foreach (var upn in unpairedPrevNodes) { + // Match other nodes (not self) that use the same tool + // and not a known loop forming edge pair + // and take the top 'count' of the results (default 10) + var newPairEdges = unpairedNextNodes + .Where(unn => unn.Id != upn.Id + && unn.Tool == upn.Tool + && !knownLoopForkPairs.Exists(pe => pe.PrevId == upn.Id && pe.NextId == unn.Id) + ) + .Select(unn => new Edge(upn.Id, unn.Id, (upn.End, unn.Start).Distance(), weighting)) + .OrderBy(e => e.Distance) + .Take(topCount); + travellingPairs.AddRange(newPairEdges); + } + + return travellingPairs; + } + + /// + /// Find all nodes that are not pointed to as the Previous node by an edge + /// + /// + /// + /// + public static List UnpairedPrevNodes(this List edgePairs, List nodes) { + return nodes.Where(n => !edgePairs.Exists(ep => ep.PrevId == n.Id)).ToList(); + } + + public static List UnpairedNextNodes(this List edgePairs, List nodes) { + return nodes.Where(n => !edgePairs.Exists(ep => ep.NextId == n.Id)).ToList(); + } + } +} diff --git a/GCodeClean/Merge/Objects.cs b/GCodeClean/Merge/Objects.cs index f3d3a7b..a2e028f 100644 --- a/GCodeClean/Merge/Objects.cs +++ b/GCodeClean/Merge/Objects.cs @@ -7,6 +7,7 @@ namespace GCodeClean.Merge { public readonly record struct Node(string Tool, short Id, Coord Start, Coord End); + public record struct Edge(short PrevId, short NextId, decimal Distance, short Weighting) { public Int16 Weighting { get; set; } = Weighting; }; diff --git a/GCodeClean/Merge/Utilities.cs b/GCodeClean/Merge/Utilities.cs new file mode 100644 index 0000000..893df20 --- /dev/null +++ b/GCodeClean/Merge/Utilities.cs @@ -0,0 +1,75 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System.Collections.Generic; +using System.Linq; + +using GCodeClean.Processing; + +using Spectre.Console; + +namespace GCodeClean.Merge +{ + public static class Utilities + { + /// + /// 'Injects' unpaired nodes within existing edges + /// + /// Not found to be useful with the way the rest of this algorithm works + /// + /// + /// + /// + /// + public static List GetInjectablePairings(this List pairedEdges, List seedPairings, List nodes, List unpairedNodes) { + List injPairings = []; + for (var ix = 0; ix < seedPairings.Count; ix++) { + var seedPairing = seedPairings[ix]; + if (seedPairing.Weighting >= 100) { + continue; + } + var prevNode = nodes.GetNode(seedPairing.PrevId); + var nextNode = nodes.GetNode(seedPairing.NextId); + var fun = unpairedNodes.Where(unn => unn.Id != prevNode.Id && unn.Id != nextNode.Id); + var altPrevEdges = fun + .Select(unn => new Edge(prevNode.Id, unn.Id, (prevNode.End, unn.Start).Distance(), 10)) + .OrderBy(e => e.NextId) + .Take(10) + .ToList(); + var altNextEdges = fun + .Select(upn => new Edge(upn.Id, nextNode.Id, (upn.End, nextNode.Start).Distance(), 10)) + .OrderBy(e => e.PrevId) + .Take(10) + .ToList(); + if (altPrevEdges.Count == 0 || altNextEdges.Count == 0) { + continue; + } + List<(Edge ap, Edge an, decimal distance)> altInjEdges = []; + foreach (var ap in altPrevEdges) { + var an = altNextEdges.Find(an => an.PrevId == ap.NextId); + if (an.PrevId == 0 && an.NextId == 0 && an.Distance == 0) { + continue; + } + altInjEdges.Add((ap, an, ap.Distance + an.Distance)); + } + altInjEdges = [.. altInjEdges.OrderBy(a => a.distance)]; + var triplet = altInjEdges[0]; + if (triplet.distance - seedPairing.Distance < seedPairing.Distance) { + List tripPair = [triplet.ap, triplet.an]; + tripPair = tripPair.FilterEdgePairsWithCurrentPairs([.. pairedEdges, .. seedPairings]); + if (tripPair.Count == 2) { + seedPairing.Weighting = 100; + seedPairings[ix] = seedPairing; + unpairedNodes.Remove(unpairedNodes.GetNode(triplet.ap.NextId)); + injPairings.AddRange([triplet.ap, triplet.an]); + } + } + } + AnsiConsole.MarkupLine($"Injection Pairings:"); + foreach (var pair in injPairings.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { + AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); + } + return injPairings; + } + } +} From bf0fbe7a5d72384029e8f8a4a3653c1768905da7 Mon Sep 17 00:00:00 2001 From: md8n Date: Wed, 27 Dec 2023 11:10:11 +0900 Subject: [PATCH 09/19] WIP - refactor and merge files --- CLI/Split/SplitCommand.cs | 7 ++- GCodeClean.Tests/Merge.Tests.cs | 4 +- GCodeClean/Merge/Algorithm.cs | 6 +- GCodeClean/Merge/MergeFile.cs | 2 + GCodeClean/Merge/NodeFileIO.cs | 19 ++++-- GCodeClean/Merge/Nodes.cs | 1 + GCodeClean/Merge/NodesAndEdges.cs | 1 + GCodeClean/Merge/{Objects.cs => Structure.cs} | 9 +-- GCodeClean/Merge/{Utilities.cs => Utility.cs} | 8 ++- GCodeClean/Shared/Structure.cs | 11 ++++ .../SplitFile.cs => Shared/Utility.cs} | 52 +++------------- GCodeClean/Split/SplitFile.cs | 60 +++++++++++++++++++ GCodeClean/Structure/Coord.cs | 42 +++++++------ GCodeClean/Structure/Line.cs | 5 +- 14 files changed, 137 insertions(+), 90 deletions(-) rename GCodeClean/Merge/{Objects.cs => Structure.cs} (59%) rename GCodeClean/Merge/{Utilities.cs => Utility.cs} (98%) create mode 100644 GCodeClean/Shared/Structure.cs rename GCodeClean/{Processing/SplitFile.cs => Shared/Utility.cs} (61%) create mode 100644 GCodeClean/Split/SplitFile.cs diff --git a/CLI/Split/SplitCommand.cs b/CLI/Split/SplitCommand.cs index 5f0f6ba..6b6b4cb 100644 --- a/CLI/Split/SplitCommand.cs +++ b/CLI/Split/SplitCommand.cs @@ -4,12 +4,13 @@ using System.Diagnostics.CodeAnalysis; using System.IO; -using GCodeClean.IO; -using GCodeClean.Processing; - using Spectre.Console; using Spectre.Console.Cli; +using GCodeClean.IO; +using GCodeClean.Split; + + namespace GCodeCleanCLI.Split { public class SplitCommand : Command { diff --git a/GCodeClean.Tests/Merge.Tests.cs b/GCodeClean.Tests/Merge.Tests.cs index 0205d48..8ba4f8b 100644 --- a/GCodeClean.Tests/Merge.Tests.cs +++ b/GCodeClean.Tests/Merge.Tests.cs @@ -8,9 +8,11 @@ using Xunit.Abstractions; using GCodeClean.Merge; +using GCodeClean.Shared; -namespace GCodeClean.Tests { +namespace GCodeClean.Tests +{ public class MergeTest(ITestOutputHelper testOutputHelper) { [Fact] public void TestCheckForLoopsFirstPairings() { diff --git a/GCodeClean/Merge/Algorithm.cs b/GCodeClean/Merge/Algorithm.cs index 4724966..1c602d6 100644 --- a/GCodeClean/Merge/Algorithm.cs +++ b/GCodeClean/Merge/Algorithm.cs @@ -4,13 +4,13 @@ using System; using System.Collections.Generic; using System.Linq; +using Spectre.Console; using GCodeClean.Processing; - -using Spectre.Console; +using GCodeClean.Shared; namespace GCodeClean.Merge -{ +{ public static class Algorithm { /// diff --git a/GCodeClean/Merge/MergeFile.cs b/GCodeClean/Merge/MergeFile.cs index e06d3ad..184bea1 100644 --- a/GCodeClean/Merge/MergeFile.cs +++ b/GCodeClean/Merge/MergeFile.cs @@ -53,6 +53,8 @@ public static void MergeFile(this string inputFolder) AnsiConsole.MarkupLine($"Current travelling distance: {currentDistance}"); AnsiConsole.MarkupLine($"New travelling distance: {newDistance}"); + inputFolder.MergeNodes(pairedEdges.GetNodes(nodes)); + // List<(string tool, List nodeIds)> cutList = []; // foreach(var toolStartId in toolStartIds) { // var tool = nodes.First(n => n.Id == toolStartId).Tool; diff --git a/GCodeClean/Merge/NodeFileIO.cs b/GCodeClean/Merge/NodeFileIO.cs index 478fe2b..5a92eca 100644 --- a/GCodeClean/Merge/NodeFileIO.cs +++ b/GCodeClean/Merge/NodeFileIO.cs @@ -6,12 +6,14 @@ using System.IO; using System.Linq; +using Spectre.Console; + +using GCodeClean.Shared; using GCodeClean.Structure; -using Spectre.Console; namespace GCodeClean.Merge -{ +{ public static class NodeFileIO { private static readonly char[] separator = ['_']; @@ -23,9 +25,9 @@ public static bool FolderExists(this string inputFolder) { } /// - /// Scan through the file for 'travelling' comments and build a list of them + /// Scan through the folder's file names and build a list of nodes from them /// - /// + /// /// public static List GetNodes(this string inputFolder) { var fileEntries = Directory.GetFiles(inputFolder); @@ -44,5 +46,14 @@ public static List GetNodes(this string inputFolder) { return nodes; } + + public static void MergeNodes(this string inputFolder, List nodes) { + var mergeFileName = $"{inputFolder}-ts.nc"; + var idFtm = $"D{nodes[^1].Id.ToString().Length}"; + foreach (var node in nodes) { + var nodeFileName = node.NodeFileName(inputFolder, idFtm); + File.AppendAllText(mergeFileName, File.ReadAllText(nodeFileName)); + } + } } } diff --git a/GCodeClean/Merge/Nodes.cs b/GCodeClean/Merge/Nodes.cs index 3804f36..aa5297c 100644 --- a/GCodeClean/Merge/Nodes.cs +++ b/GCodeClean/Merge/Nodes.cs @@ -5,6 +5,7 @@ using System.Linq; using GCodeClean.Processing; +using GCodeClean.Shared; namespace GCodeClean.Merge diff --git a/GCodeClean/Merge/NodesAndEdges.cs b/GCodeClean/Merge/NodesAndEdges.cs index fe28bfb..e0a59bb 100644 --- a/GCodeClean/Merge/NodesAndEdges.cs +++ b/GCodeClean/Merge/NodesAndEdges.cs @@ -7,6 +7,7 @@ using Spectre.Console; using GCodeClean.Processing; +using GCodeClean.Shared; namespace GCodeClean.Merge diff --git a/GCodeClean/Merge/Objects.cs b/GCodeClean/Merge/Structure.cs similarity index 59% rename from GCodeClean/Merge/Objects.cs rename to GCodeClean/Merge/Structure.cs index a2e028f..84cc530 100644 --- a/GCodeClean/Merge/Objects.cs +++ b/GCodeClean/Merge/Structure.cs @@ -1,14 +1,9 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. -using System; -using GCodeClean.Structure; - namespace GCodeClean.Merge -{ - public readonly record struct Node(string Tool, short Id, Coord Start, Coord End); - +{ public record struct Edge(short PrevId, short NextId, decimal Distance, short Weighting) { - public Int16 Weighting { get; set; } = Weighting; + public short Weighting { get; set; } = Weighting; }; } diff --git a/GCodeClean/Merge/Utilities.cs b/GCodeClean/Merge/Utility.cs similarity index 98% rename from GCodeClean/Merge/Utilities.cs rename to GCodeClean/Merge/Utility.cs index 893df20..56810a0 100644 --- a/GCodeClean/Merge/Utilities.cs +++ b/GCodeClean/Merge/Utility.cs @@ -4,13 +4,15 @@ using System.Collections.Generic; using System.Linq; +using Spectre.Console; + using GCodeClean.Processing; +using GCodeClean.Shared; -using Spectre.Console; namespace GCodeClean.Merge -{ - public static class Utilities +{ + public static class Utility { /// /// 'Injects' unpaired nodes within existing edges diff --git a/GCodeClean/Shared/Structure.cs b/GCodeClean/Shared/Structure.cs new file mode 100644 index 0000000..cbb0657 --- /dev/null +++ b/GCodeClean/Shared/Structure.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +// Structures shared by Split and Merge + +using GCodeClean.Structure; + +namespace GCodeClean.Shared +{ + public readonly record struct Node(string Tool, short Id, Coord Start, Coord End); +} diff --git a/GCodeClean/Processing/SplitFile.cs b/GCodeClean/Shared/Utility.cs similarity index 61% rename from GCodeClean/Processing/SplitFile.cs rename to GCodeClean/Shared/Utility.cs index 2e9a09e..0c8b930 100644 --- a/GCodeClean/Processing/SplitFile.cs +++ b/GCodeClean/Shared/Utility.cs @@ -6,13 +6,13 @@ using System.IO; using System.Text.RegularExpressions; +using GCodeClean.Processing; using GCodeClean.Structure; -using Spectre.Console; -namespace GCodeClean.Processing +namespace GCodeClean.Shared { - public static partial class Split { + public static partial class Utility { /// /// Finds GCodeClean's special 'Travelling' comments /// @@ -90,55 +90,21 @@ public static List GetPostamble(this IEnumerable inputLines, str return postambleLines; } - public static void SplitFile(this IEnumerable inputLines, string outputFolder, List travellingComments, List preambleLines, List postambleLines) { - if (Directory.Exists(outputFolder)) { - Directory.Delete(outputFolder, true); - } - Directory.CreateDirectory(outputFolder); - - var (_, tLId, _, _) = travellingComments[^1].ParseTravelling(); - var idFtm = $"D{tLId.ToString().Length}"; - - string firstLine = ""; - - var iL = inputLines.GetEnumerator(); - - while (iL.MoveNext()) { - var line = iL.Current; - if (line == Default.PreambleCompleted) { - break; - } - } + public static string IdFormat(this short id) => $"D{id.ToString().Length}"; - foreach (var travelling in travellingComments) { - var (tool, id, start, end) = travelling.ParseTravelling(); - var filename = $"{outputFolder}{Path.DirectorySeparatorChar}{tool}_{id.ToString(idFtm)}_{start.ToXYCoord()}_{end.ToXYCoord()}_gcc.nc"; - AnsiConsole.MarkupLine($"Filename: [bold yellow]{filename}[/]"); - File.WriteAllLines(filename, preambleLines); - if (firstLine != "") { - File.AppendAllLines(filename, [firstLine]); - } - while (iL.MoveNext()) { - var line = iL.Current; - File.AppendAllLines(filename, [line]); - if (line.EndsWith(travelling)) { - firstLine = (new Line(line)).ToSimpleString(); - break; - } - } - File.AppendAllLines(filename, postambleLines); - } + public static string NodeFileName(this Node node, string folderName, string idFtm) { + return $"{folderName}{Path.DirectorySeparatorChar}{node.Tool}_{node.Id.ToString(idFtm)}_{node.Start.ToXYCoord()}_{node.End.ToXYCoord()}_gcc.nc"; } - private static (string tool, int id, Line start, Line end) ParseTravelling(this string travelling) { + public static Node ParseTravelling(this string travelling) { var tDetails = travelling.Replace("(||Travelling||", "").Replace("||)", "").Split("||"); var tTool = tDetails[0]; - var tId = Convert.ToInt32(tDetails[1]); + var tId = Convert.ToInt16(tDetails[1]); var tSE = tDetails[2].Split(">>", StringSplitOptions.RemoveEmptyEntries); var lStart = new Line(tSE[0]); var lEnd = new Line(tSE[1]); - return (tTool, tId, lStart, lEnd); + return new Node(tTool, tId, (Coord)lStart, (Coord)lEnd); } } } diff --git a/GCodeClean/Split/SplitFile.cs b/GCodeClean/Split/SplitFile.cs new file mode 100644 index 0000000..7335173 --- /dev/null +++ b/GCodeClean/Split/SplitFile.cs @@ -0,0 +1,60 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System.Collections.Generic; +using System.IO; + +using Spectre.Console; + +using GCodeClean.Processing; +using GCodeClean.Structure; +using GCodeClean.Shared; + + +namespace GCodeClean.Split +{ + public static partial class Split { + public static void SplitFile(this IEnumerable inputLines, string outputFolder, List travellingComments, List preambleLines, List postambleLines) { + if (Directory.Exists(outputFolder)) { + Directory.Delete(outputFolder, true); + } + Directory.CreateDirectory(outputFolder); + + var (_, tLId, _, _) = travellingComments[^1].ParseTravelling(); + var idFtm = tLId.IdFormat(); + + string firstLine = ""; + + var iL = inputLines.GetEnumerator(); + + while (iL.MoveNext()) { + var line = iL.Current; + if (line == Default.PreambleCompleted) { + break; + } + } + + foreach (var travelling in travellingComments) { + var filename = travelling.ParseTravelling().NodeFileName(outputFolder, idFtm); + AnsiConsole.MarkupLine($"Filename: [bold yellow]{filename}[/]"); + + File.WriteAllLines(filename, preambleLines); + + if (firstLine != "") { + File.AppendAllLines(filename, [firstLine]); + } + + while (iL.MoveNext()) { + var line = iL.Current; + File.AppendAllLines(filename, [line]); + if (line.EndsWith(travelling)) { + firstLine = (new Line(line)).ToSimpleString(); + break; + } + } + + File.AppendAllLines(filename, postambleLines); + } + } + } +} diff --git a/GCodeClean/Structure/Coord.cs b/GCodeClean/Structure/Coord.cs index 766431b..2c28e0b 100644 --- a/GCodeClean/Structure/Coord.cs +++ b/GCodeClean/Structure/Coord.cs @@ -197,28 +197,6 @@ public static decimal Distance(Coord coords1, Coord coords2) return coords3; } - public override string ToString() - { - var coords = new List(); - - if ((Set & CoordSet.X) == CoordSet.X) - { - coords.Add($"X:{X:0.####}"); - } - - if ((Set & CoordSet.Y) == CoordSet.Y) - { - coords.Add($"Y:{Y:0.####}"); - } - - if ((Set & CoordSet.Z) == CoordSet.Z) - { - coords.Add($"Z:{Z:0.####}"); - } - - return string.Join(',', coords); - } - /// /// Determines if all supplied coords are in the same orthogonal plane (X, Y or Z) /// @@ -258,5 +236,25 @@ public static CoordSet Ortho(List coords) return allX | allY | allZ; } + + public override string ToString() { + var coords = new List(); + + if ((Set & CoordSet.X) == CoordSet.X) { + coords.Add($"X:{X:0.####}"); + } + + if ((Set & CoordSet.Y) == CoordSet.Y) { + coords.Add($"Y:{Y:0.####}"); + } + + if ((Set & CoordSet.Z) == CoordSet.Z) { + coords.Add($"Z:{Z:0.####}"); + } + + return string.Join(',', coords); + } + + public string ToXYCoord() => $"X{this.X}Y{this.Y}"; } } \ No newline at end of file diff --git a/GCodeClean/Structure/Line.cs b/GCodeClean/Structure/Line.cs index f5e96bc..319f52c 100644 --- a/GCodeClean/Structure/Line.cs +++ b/GCodeClean/Structure/Line.cs @@ -433,9 +433,6 @@ public bool Equals(Line line) { /// public string ToSimpleString() => string.Join(" ", _tokens.Where(t => !(t.IsLineNumber || t.IsComment))).Trim(); - public string ToXYCoord() { - var xyz = (Coord)this; - return $"X{xyz.X}Y{xyz.Y}"; - } + public string ToXYCoord() => ((Coord)this).ToXYCoord(); } } From 46f4514e472b20afbb07a1fb0a0aed52c0d5ff66 Mon Sep 17 00:00:00 2001 From: md8n Date: Wed, 27 Dec 2023 11:11:35 +0900 Subject: [PATCH 10/19] builds OK --- CLI/Split/SplitCommand.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/CLI/Split/SplitCommand.cs b/CLI/Split/SplitCommand.cs index 6b6b4cb..801fefc 100644 --- a/CLI/Split/SplitCommand.cs +++ b/CLI/Split/SplitCommand.cs @@ -8,6 +8,7 @@ using Spectre.Console.Cli; using GCodeClean.IO; +using GCodeClean.Shared; using GCodeClean.Split; From 545bd67ff45e702e26f09e216b3b8813664cfe6a Mon Sep 17 00:00:00 2001 From: md8n Date: Wed, 27 Dec 2023 11:40:58 +0900 Subject: [PATCH 11/19] File Merge - travelling salesman --- GCodeClean/Merge/NodeFileIO.cs | 43 +++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/GCodeClean/Merge/NodeFileIO.cs b/GCodeClean/Merge/NodeFileIO.cs index 5a92eca..beccac3 100644 --- a/GCodeClean/Merge/NodeFileIO.cs +++ b/GCodeClean/Merge/NodeFileIO.cs @@ -10,6 +10,8 @@ using GCodeClean.Shared; using GCodeClean.Structure; +using GCodeClean.IO; +using GCodeClean.Processing; namespace GCodeClean.Merge @@ -50,10 +52,49 @@ public static List GetNodes(this string inputFolder) { public static void MergeNodes(this string inputFolder, List nodes) { var mergeFileName = $"{inputFolder}-ts.nc"; var idFtm = $"D{nodes[^1].Id.ToString().Length}"; + + var firstNodeFileName = nodes[0].NodeFileName(inputFolder, idFtm); + var firstNodeInputLines = firstNodeFileName.ReadFileLines(); + var preambleLines = firstNodeInputLines.GetPreamble(); + File.WriteAllLines(mergeFileName, preambleLines); + foreach (var node in nodes) { var nodeFileName = node.NodeFileName(inputFolder, idFtm); - File.AppendAllText(mergeFileName, File.ReadAllText(nodeFileName)); + var inputLines = nodeFileName.ReadFileLines(); + var travellingComments = inputLines.GetTravellingComments(); + + string firstLine = ""; + + var iL = inputLines.GetEnumerator(); + + while (iL.MoveNext()) { + var line = iL.Current; + if (line == Default.PreambleCompleted) { + break; + } + } + + foreach (var travelling in travellingComments) { + if (firstLine != "") { + File.AppendAllLines(mergeFileName, [firstLine]); + } + + while (iL.MoveNext()) { + var line = iL.Current; + File.AppendAllLines(mergeFileName, [line]); + if (line.EndsWith(travelling)) { + firstLine = (new Line(line)).ToSimpleString(); + break; + } + } + } } + + var lastNodeFileName = nodes[^1].NodeFileName(inputFolder, idFtm); + var lastNodeInputLines = lastNodeFileName.ReadFileLines(); + var lastTravellingComments = lastNodeInputLines.GetTravellingComments(); + var postambleLines = lastNodeInputLines.GetPostamble(lastTravellingComments[^1]); + File.AppendAllLines(mergeFileName, postambleLines); } } } From 8e800dd7020aca91530ec7b77c75ec1bad39af60 Mon Sep 17 00:00:00 2001 From: md8n Date: Wed, 27 Dec 2023 13:50:32 +0900 Subject: [PATCH 12/19] updated tests --- GCodeClean.Tests/Dedup.Tests.cs | 56 ++++++++++++++++++++++- GCodeClean.Tests/Workflow.Tests.cs | 72 +++++++++++++++++------------- 2 files changed, 97 insertions(+), 31 deletions(-) diff --git a/GCodeClean.Tests/Dedup.Tests.cs b/GCodeClean.Tests/Dedup.Tests.cs index 052b0cd..77e4319 100644 --- a/GCodeClean.Tests/Dedup.Tests.cs +++ b/GCodeClean.Tests/Dedup.Tests.cs @@ -20,7 +20,7 @@ private static async IAsyncEnumerable AsyncLines(IEnumerable lines) foreach (var line in lines) { await Task.Delay(1); - yield return line; + yield return new Line(line); } } @@ -207,5 +207,59 @@ public async Task DedupTravelling() { Assert.False(testLines.SequenceEqual(resultLines)); Assert.True(expectedLines.SequenceEqual(resultLines)); } + + [Fact] + public async Task DedupTravellingAgain() { + List sourceTextLines = [ + "G17", + "G90", + "G21", + "G00 Z1.5", + "G01 X54.178 Y136.211", + "G01 X54.178 Y136.211 Z-0.678", + "G01 X54.125 Y136.168 Z-0.613", + "G01 X54.033 Y136.095 Z-0.499", + "G00 Z1.5", + "G00 X69.089 Y128.892", + "G01 Z-0.661", + "G01 X68.995 Y128.814 Z-0.627", + "G01 X68.905 Y128.746 Z-0.597", + "G01 X68.813 Y128.684 Z-0.57", + "G00 Z0.5", + "M30", + ]; + var testLines = sourceTextLines.ConvertAll(l => new Line(l)); + var lines = AsyncLines(testLines); + + List expectedLines = [ + new Line("G17"), + new Line("G90"), + new Line("G21"), + + new Line("G0 Z0.5"), + new Line("G0 X54.178 Y136.211 Z0.5"), + new Line("G1 X54.178 Y136.211 Z-0.678"), + new Line("G1 X54.125 Y136.168 Z-0.613"), + new Line("G1 X54.033 Y136.095 Z-0.499"), + new Line("G0 X54.033 Y136.095 Z0.5"), + + new Line("G0 X69.089 Y128.892 Z0.5"), + new Line("G1 X69.089 Y128.892 Z-0.661"), + new Line("G1 X68.995 Y128.814 Z-0.627"), + new Line("G1 X68.905 Y128.746 Z-0.597"), + new Line("G1 X68.813 Y128.684 Z-0.57"), + new Line("G0 X68.813 Y128.684 Z0.5"), + + new Line("M30"), + ]; + + decimal zClamp = 0.5M; + var augmentLines = lines.Augment(); + var zClampedLines = augmentLines.ZClamp(zClamp); + + var resultLines = await zClampedLines.DedupTravelling().ToArrayAsync(); + Assert.False(testLines.SequenceEqual(resultLines)); + Assert.True(expectedLines.SequenceEqual(resultLines)); + } } } diff --git a/GCodeClean.Tests/Workflow.Tests.cs b/GCodeClean.Tests/Workflow.Tests.cs index 88fe4fd..9ec67e4 100644 --- a/GCodeClean.Tests/Workflow.Tests.cs +++ b/GCodeClean.Tests/Workflow.Tests.cs @@ -134,14 +134,17 @@ public async Task CleanLinesSecondPhase() { "G54", "M3", "G00 Z1.5", - "G00 X68.904 Y128.746 Z1.5", - "G01 X68.904 Y128.746 Z-1.194", - "G01 X68.995 Y128.814 Z-1.254", - "G01 X69.089 Y128.892 Z-1.322", + "G01 X54.178 Y136.211", + "G01 X54.178 Y136.211 Z-0.678", + "G01 X54.125 Y136.168 Z-0.613", + "G01 X54.033 Y136.095 Z-0.499", "G00 Z1.5", - "G00 X42.239 Y157.031", - "G01 Z-0.413", - "G01 X42.33 Y157.498 Z-0.468", + "G00 X69.089 Y128.892", + "G01 Z-0.661", + "G01 X68.995 Y128.814 Z-0.627", + "G01 X68.905 Y128.746 Z-0.597", + "G01 X68.813 Y128.684 Z-0.57", + "G00 Z0.5", ]; var sourceLineLines = sourceTextLines.ConvertAll(l => new Line(l)); @@ -159,15 +162,18 @@ public async Task CleanLinesSecondPhase() { new Line("M3"), new Line(Default.PreambleCompleted), new Line(""), + new Line("G0 Z0.5"), - new Line("G0 X68.904 Y128.746 Z0.5"), - new Line("G1 X68.904 Y128.746 Z-1.194"), - new Line("G1 X69.089 Y128.892 Z-1.322"), - new Line("G0 X69.089 Y128.892 Z0.5 (||Travelling||notset||0||>>G0 X68.904 Y128.746 Z0.5>>G0 X69.089 Y128.892 Z0.5>>||)"), - new Line("G0 X42.239 Y157.031 Z0.5"), - new Line("G1 X42.239 Y157.031 Z-0.413"), - new Line("G1 X42.33 Y157.498 Z-0.468"), - new Line("G0 Z0.5 (||Travelling||notset||1||>>G0 X42.239 Y157.031 Z0.5>>G1 X42.33 Y157.498 Z-0.468>>||)"), + new Line("G0 X54.178 Y136.211 Z0.5"), + new Line("G1 X54.178 Y136.211 Z-0.678"), + new Line("G1 X54.033 Y136.095 Z-0.499"), + new Line("G0 X54.033 Y136.095 Z0.5 (||Travelling||notset||0||>>G0 X54.178 Y136.211 Z0.5>>G0 X54.033 Y136.095 Z0.5>>||)"), + + new Line("G0 X69.089 Y128.892 Z0.5"), + new Line("G1 X69.089 Y128.892 Z-0.661"), + new Line("G1 X68.813 Y128.684 Z-0.57"), + new Line("G0 X68.813 Y128.684 Z0.5 (||Travelling||notset||1||>>G0 X69.089 Y128.892 Z0.5>>G0 X68.813 Y128.684 Z0.5>>||)"), + new Line(Default.PostAmbleCompleted), new Line("M30"), ]; @@ -225,14 +231,17 @@ public async Task CleanLinesThirdPhase() { "G54", "M3", "G00 Z1.5", - "G00 X68.904 Y128.746 Z1.5", - "G01 X68.904 Y128.746 Z-1.194", - "G01 X68.995 Y128.814 Z-1.254", - "G01 X69.089 Y128.892 Z-1.322", + "G01 X54.178 Y136.211", + "G01 X54.178 Y136.211 Z-0.678", + "G01 X54.125 Y136.168 Z-0.613", + "G01 X54.033 Y136.095 Z-0.499", "G00 Z1.5", - "G00 X42.239 Y157.031", - "G01 Z-0.413", - "G01 X42.33 Y157.498 Z-0.468", + "G00 X69.089 Y128.892", + "G01 Z-0.661", + "G01 X68.995 Y128.814 Z-0.627", + "G01 X68.905 Y128.746 Z-0.597", + "G01 X68.813 Y128.684 Z-0.57", + "G00 Z0.5", ]; var sourceLineLines = sourceTextLines.ConvertAll(l => new Line(l)); var sourceLines = sourceTextLines.ToAsyncEnumerable(); @@ -250,14 +259,17 @@ public async Task CleanLinesThirdPhase() { new Line(Default.PreambleCompleted), new Line(""), new Line("G0 Z0.5"), - new Line("G0 X68.904 Y128.746"), - new Line("G1 X68.904 Y128.746 Z-1.194"), - new Line("G1 X69.089 Y128.892 Z-1.322"), - new Line("G0 X69.089 Y128.892 Z0.5 (||Travelling||notset||0||>>G0 X68.904 Y128.746 Z0.5>>G0 X69.089 Y128.892 Z0.5>>||)"), - new Line("G0 X42.239 Y157.031"), - new Line("G1 X42.239 Y157.031 Z-0.413"), - new Line("G1 X42.33 Y157.498 Z-0.468"), - new Line("G0 Z0.5 (||Travelling||notset||1||>>G0 X42.239 Y157.031 Z0.5>>G1 X42.33 Y157.498 Z-0.468>>||)"), + + new Line("G0 X54.178 Y136.211"), + new Line("G1 X54.178 Y136.211 Z-0.678"), + new Line("G1 X54.033 Y136.095 Z-0.499"), + new Line("G0 X54.033 Y136.095 Z0.5 (||Travelling||notset||0||>>G0 X54.178 Y136.211 Z0.5>>G0 X54.033 Y136.095 Z0.5>>||)"), + + new Line("G0 X69.089 Y128.892"), + new Line("G1 X69.089 Y128.892 Z-0.661"), + new Line("G1 X68.813 Y128.684 Z-0.57"), + new Line("G0 X68.813 Y128.684 Z0.5 (||Travelling||notset||1||>>G0 X69.089 Y128.892 Z0.5>>G0 X68.813 Y128.684 Z0.5>>||)"), + new Line(Default.PostAmbleCompleted), new Line("M30"), ]; From f238b2da09ca318a787e068f4ef7f8dabc85e065 Mon Sep 17 00:00:00 2001 From: md8n Date: Thu, 28 Dec 2023 09:29:50 +0900 Subject: [PATCH 13/19] Improved Split and Merge --- GCodeClean/Merge/NodeFileIO.cs | 19 ++++++++++--------- GCodeClean/Shared/Utility.cs | 2 +- GCodeClean/Split/SplitFile.cs | 3 +-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/GCodeClean/Merge/NodeFileIO.cs b/GCodeClean/Merge/NodeFileIO.cs index beccac3..cf2309e 100644 --- a/GCodeClean/Merge/NodeFileIO.cs +++ b/GCodeClean/Merge/NodeFileIO.cs @@ -51,20 +51,20 @@ public static List GetNodes(this string inputFolder) { public static void MergeNodes(this string inputFolder, List nodes) { var mergeFileName = $"{inputFolder}-ts.nc"; - var idFtm = $"D{nodes[^1].Id.ToString().Length}"; + var idFtm = nodes.Count.IdFormat(); var firstNodeFileName = nodes[0].NodeFileName(inputFolder, idFtm); var firstNodeInputLines = firstNodeFileName.ReadFileLines(); var preambleLines = firstNodeInputLines.GetPreamble(); File.WriteAllLines(mergeFileName, preambleLines); + var lastLine = new Line(""); + foreach (var node in nodes) { var nodeFileName = node.NodeFileName(inputFolder, idFtm); var inputLines = nodeFileName.ReadFileLines(); var travellingComments = inputLines.GetTravellingComments(); - string firstLine = ""; - var iL = inputLines.GetEnumerator(); while (iL.MoveNext()) { @@ -75,15 +75,16 @@ public static void MergeNodes(this string inputFolder, List nodes) { } foreach (var travelling in travellingComments) { - if (firstLine != "") { - File.AppendAllLines(mergeFileName, [firstLine]); - } - while (iL.MoveNext()) { var line = iL.Current; - File.AppendAllLines(mergeFileName, [line]); + if (new Line(line) != lastLine) { + File.AppendAllLines(mergeFileName, [line]); + } + lastLine = new Line(""); + if (line.EndsWith(travelling)) { - firstLine = (new Line(line)).ToSimpleString(); + lastLine = new Line(line); + lastLine = new Line(lastLine.ToSimpleString()); break; } } diff --git a/GCodeClean/Shared/Utility.cs b/GCodeClean/Shared/Utility.cs index 0c8b930..c16d567 100644 --- a/GCodeClean/Shared/Utility.cs +++ b/GCodeClean/Shared/Utility.cs @@ -90,7 +90,7 @@ public static List GetPostamble(this IEnumerable inputLines, str return postambleLines; } - public static string IdFormat(this short id) => $"D{id.ToString().Length}"; + public static string IdFormat(this int idCount) => $"D{idCount.ToString().Length}"; public static string NodeFileName(this Node node, string folderName, string idFtm) { return $"{folderName}{Path.DirectorySeparatorChar}{node.Tool}_{node.Id.ToString(idFtm)}_{node.Start.ToXYCoord()}_{node.End.ToXYCoord()}_gcc.nc"; diff --git a/GCodeClean/Split/SplitFile.cs b/GCodeClean/Split/SplitFile.cs index 7335173..6e3c0f8 100644 --- a/GCodeClean/Split/SplitFile.cs +++ b/GCodeClean/Split/SplitFile.cs @@ -20,8 +20,7 @@ public static void SplitFile(this IEnumerable inputLines, string outputF } Directory.CreateDirectory(outputFolder); - var (_, tLId, _, _) = travellingComments[^1].ParseTravelling(); - var idFtm = tLId.IdFormat(); + var idFtm = travellingComments.Count.IdFormat(); string firstLine = ""; From 39d5f912934b81ad9dc6c0b0c1eabea56c46646e Mon Sep 17 00:00:00 2001 From: md8n Date: Thu, 28 Dec 2023 09:30:10 +0900 Subject: [PATCH 14/19] Support recleaning --- GCodeClean/Processing/Processing.cs | 31 ++++++++++++++++++++++------- GCodeClean/Structure/Line.cs | 14 +++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/GCodeClean/Processing/Processing.cs b/GCodeClean/Processing/Processing.cs index ab413aa..04cca70 100644 --- a/GCodeClean/Processing/Processing.cs +++ b/GCodeClean/Processing/Processing.cs @@ -296,12 +296,7 @@ public static async IAsyncEnumerable ZClamp( travelingToken.Source = "G0"; } } else if (zToken.Number == 0 && travelingToken.ToString() == "G1") { - // If Z == 0 and the source is G1, then triple it for later File split markup - yield return line; - var zeroLine = new Line(line); - zeroLine.AllTokens.Intersect(ModalGroup.ModalSimpleMotion).First().Source = "G0"; - zeroLine.AllTokens.First(t => t.Code == 'Z').Number = zClampConstrained; - yield return zeroLine; + // If Z == 0 and the source is G1, then we want to leave it alone as a surface exit from a cut } else if (zToken.Number < 0 && travelingToken.ToString() == "G0") { // If Z < 0 and source is G0 then the motion should be G1 (probably) travelingToken.Source = "G1"; @@ -522,6 +517,15 @@ public static async IAsyncEnumerable DetectTravelling(this IAsyncEnumerabl entrySet = true; } exitLine = new Line(line); + var exitComments = exitLine.AllCommentTokens; + if (exitComments.Count > 0) { + for (var ix = exitComments.Count - 1; ix >= 0; ix--) { + if (exitComments[ix].Source.StartsWith("(||Travelling||")) { + exitLine.RemoveToken(exitComments[ix]); + break; + } + } + } } if (line.HasToken('Z')) { @@ -531,7 +535,20 @@ public static async IAsyncEnumerable DetectTravelling(this IAsyncEnumerabl travellingLine = new Line(line); travellingLine.ReplaceToken(new Token("G1"), new Token("G0")); - line.AppendToken(new Token($"(||Travelling||{context.GetToolNumber()}||{blockIx++}||>>{entryLine}>>{exitLine}>>||)")); + // Replace any existing travelling comment + var travellingComment = new Token($"(||Travelling||{context.GetToolNumber()}||{blockIx++}||>>{entryLine}>>{exitLine}>>||)"); + var comments = line.AllCommentTokens; + if (comments.Count > 0) { + for (var ix = 0; ix < comments.Count; ix++) { + if (comments[ix].Source.StartsWith("(||Travelling||")) { + line.ReplaceToken(comments[ix], travellingComment); + break; + } + } + } else { + line.AppendToken(travellingComment); + } + entryLine = new Line(); entrySet = false; } diff --git a/GCodeClean/Structure/Line.cs b/GCodeClean/Structure/Line.cs index 319f52c..a7737d8 100644 --- a/GCodeClean/Structure/Line.cs +++ b/GCodeClean/Structure/Line.cs @@ -38,6 +38,17 @@ public List Tokens { } } + /// + /// Gets all comment Tokens within the line. + /// + public List AllCommentTokens { + get { +#pragma warning disable S2365 // Properties should not make collection or array copies + return _tokens.Where(t => t.IsComment).ToList(); +#pragma warning restore S2365 // Properties should not make collection or array copies + } + } + /// /// Set the private member _tokens to the supplied value, ensuring that the order of tokens is correct /// Then set the status values @@ -60,6 +71,9 @@ private void SetTokens() { _tokens = [.. blockDeleteToken, .. lineNumberToken, .. allOtherTokens, .. allCommentTokens]; + // Reset the _source to match + _source = string.Join(' ', _tokens.Select(t => t.Source)); + SetStatuses(); } From 11c8a6eb10ecda69814223d18a89e369f3986674 Mon Sep 17 00:00:00 2001 From: md8n Date: Thu, 28 Dec 2023 09:35:07 +0900 Subject: [PATCH 15/19] temp removal of EliminateNeedlessTravelling --- CLI/Clean/CleanCommand.cs | 4 +++- CLI/Clean/CleanSettings.cs | 10 +++++----- README.md | 4 ---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/CLI/Clean/CleanCommand.cs b/CLI/Clean/CleanCommand.cs index 22b5ab8..7235261 100644 --- a/CLI/Clean/CleanCommand.cs +++ b/CLI/Clean/CleanCommand.cs @@ -91,11 +91,13 @@ public override async Task ExecuteAsync([NotNull] CommandContext context, [ var outputFile = DetermineOutputFilename(settings); AnsiConsole.MarkupLine($"Outputting to: [bold green]{outputFile}[/]"); + var eliminateNeedlessTravelling = false; // settings.EliminateNeedlessTravelling + // Determine our starting context var preambleContext = await inputFile.GetPreambleContext(); var inputLines = inputFile.ReadLinesAsync(); - var reassembledLines = inputLines.CleanLines(preambleContext, dedupSelection, minimisationStrategy, settings.LineNumbers, settings.EliminateNeedlessTravelling, zClamp, arcTolerance, tolerance, settings.Annotate, tokenDefinitions); + var reassembledLines = inputLines.CleanLines(preambleContext, dedupSelection, minimisationStrategy, settings.LineNumbers, eliminateNeedlessTravelling, zClamp, arcTolerance, tolerance, settings.Annotate, tokenDefinitions); var lineCount = outputFile.WriteLinesAsync(reassembledLines); await foreach (var line in lineCount) { diff --git a/CLI/Clean/CleanSettings.cs b/CLI/Clean/CleanSettings.cs index b9e4db7..5d54e52 100644 --- a/CLI/Clean/CleanSettings.cs +++ b/CLI/Clean/CleanSettings.cs @@ -45,10 +45,10 @@ public sealed class CleanSettings : CommonSettings { [Description("Restrict z-axis positive values to the supplied value")] public FlagValue ZClamp { get; set; } - [CommandOption("--eliminateNeedlessTravelling")] - [Description("Eliminate needless 'travelling', extra movements with positive z-axis values")] - [DefaultValue(true)] - public bool EliminateNeedlessTravelling { get; set; } + //[CommandOption("--eliminateNeedlessTravelling")] + //[Description("Eliminate needless 'travelling', extra movements with positive z-axis values")] + //[DefaultValue(false)] + //public bool EliminateNeedlessTravelling { get; set; } public override ValidationResult Validate() { if (string.IsNullOrWhiteSpace(TokenDefs)) { @@ -65,7 +65,7 @@ public override ValidationResult Validate() { } public static string GetCleanTokenDefsPath(string tokenDefsPath) { - if (tokenDefsPath.ToUpperInvariant() == "TOKENDEFINITIONS.JSON") { + if (tokenDefsPath.Equals("TOKENDEFINITIONS.JSON", StringComparison.InvariantCultureIgnoreCase)) { var entryDir = Path.GetDirectoryName(AppContext.BaseDirectory); tokenDefsPath = $"{entryDir}{Path.DirectorySeparatorChar}tokenDefinitions.json"; diff --git a/README.md b/README.md index b5df64a..a56fbcb 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,6 @@ OPTIONS: arcs (G2, G3) below which they will be converted to lines (G1) --zClamp [ZCLAMP] Restrict z-axis positive values to the supplied value - --eliminateNeedlessTravelling True Eliminate needless 'travelling', extra movements with - positive z-axis values COMMANDS: clean Clean your GCode file. This is the default command @@ -122,8 +120,6 @@ COMMANDS: - 0.02 to 0.5 for inches or - 0.5 to 10.0 for millimeters -`--eliminateNeedlessTravelling` is a simple switch. Normally needless travelling (intermediary `G0` with positive z-axis values) will be eliminated, but adding this flag and setting it to false will ensure they are preserved. If you have something protruding from the surface of the workpiece then you should turn this option to false (just in case). - For the tolerance and clamp values, the smallest value (inch or mm specific) is used as the default value. Now find yourself a gcode (`.nc`, `.gcode`, etc.) file to use for the option `--filename `. From d2dca22ff60fd03229a5ea51e26519276629043a Mon Sep 17 00:00:00 2001 From: md8n Date: Thu, 28 Dec 2023 10:06:53 +0900 Subject: [PATCH 16/19] SplitFile improvements --- GCodeClean/Split/SplitFile.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/GCodeClean/Split/SplitFile.cs b/GCodeClean/Split/SplitFile.cs index 6e3c0f8..5e7a854 100644 --- a/GCodeClean/Split/SplitFile.cs +++ b/GCodeClean/Split/SplitFile.cs @@ -22,8 +22,6 @@ public static void SplitFile(this IEnumerable inputLines, string outputF var idFtm = travellingComments.Count.IdFormat(); - string firstLine = ""; - var iL = inputLines.GetEnumerator(); while (iL.MoveNext()) { @@ -39,15 +37,10 @@ public static void SplitFile(this IEnumerable inputLines, string outputF File.WriteAllLines(filename, preambleLines); - if (firstLine != "") { - File.AppendAllLines(filename, [firstLine]); - } - while (iL.MoveNext()) { var line = iL.Current; File.AppendAllLines(filename, [line]); if (line.EndsWith(travelling)) { - firstLine = (new Line(line)).ToSimpleString(); break; } } From 0841f7a4fea2f582632c5cc4f92bf38d37bb535d Mon Sep 17 00:00:00 2001 From: md8n Date: Fri, 29 Dec 2023 11:32:45 +0900 Subject: [PATCH 17/19] Changin License --- LICENSE | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f373e6f..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 md8n - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. From 368e426ef8e17f6740c35b7fb570507bca61b02a Mon Sep 17 00:00:00 2001 From: md8n Date: Fri, 29 Dec 2023 11:39:04 +0900 Subject: [PATCH 18/19] Swapped out spectre console --- BuildItYourself.md | 4 +- CLI/CLI.csproj | 5 +- CLI/Clean/CleanAction.cs | 88 ++++++++++++ CLI/Clean/CleanCommand.cs | 110 --------------- CLI/Clean/CleanOptions.cs | 64 +++++++++ CLI/Clean/CleanSettings.cs | 94 ------------- CLI/Common/CommonSettings.cs | 15 --- CLI/Merge/MergeAction.cs | 23 ++++ CLI/Merge/MergeCommand.cs | 25 ---- CLI/Merge/MergeSettings.cs | 15 --- CLI/Program.cs | 122 ++++++++++++++--- CLI/Split/{SplitCommand.cs => SplitAction.cs} | 23 ++-- CLI/Split/SplitSettings.cs | 9 -- GCodeClean.Tests/GCodeClean.Tests.csproj | 6 +- GCodeClean/Merge/Algorithm.cs | 9 +- GCodeClean/Merge/Edges.cs | 2 - GCodeClean/Merge/MergeFile.cs | 43 ++---- GCodeClean/Merge/NodeFileIO.cs | 2 - GCodeClean/Merge/NodesAndEdges.cs | 2 - GCodeClean/Merge/Utility.cs | 7 +- GCodeClean/Processing/Processing.cs | 1 - GCodeClean/Split/SplitFile.cs | 6 +- GCodeClean/gcodeclean.csproj | 3 +- README.md | 126 ++++++++++-------- 24 files changed, 387 insertions(+), 417 deletions(-) create mode 100644 CLI/Clean/CleanAction.cs delete mode 100644 CLI/Clean/CleanCommand.cs create mode 100644 CLI/Clean/CleanOptions.cs delete mode 100644 CLI/Clean/CleanSettings.cs delete mode 100644 CLI/Common/CommonSettings.cs create mode 100644 CLI/Merge/MergeAction.cs delete mode 100644 CLI/Merge/MergeCommand.cs delete mode 100644 CLI/Merge/MergeSettings.cs rename CLI/Split/{SplitCommand.cs => SplitAction.cs} (58%) delete mode 100644 CLI/Split/SplitSettings.cs diff --git a/BuildItYourself.md b/BuildItYourself.md index 3835fb8..26be4f0 100644 --- a/BuildItYourself.md +++ b/BuildItYourself.md @@ -4,8 +4,6 @@ There are standalone release builds available, for Linux, Raspberry Pi (linux-arm), and Windows at [GCodeClean releases](https://github.com/md8n/GCodeClean/releases). It is very easy to a build for MacOS / OSX (osx-64 / osx-arm) (see #Deployment below). -The standalone releases include all the relevant .NET 8.0 libraries for this application. - But you can build and run this project yourself, and for that you would need the .NET 8.0 SDK. And if you do build it yourself then there are a very large number of possible targets including 32bit, and many specific Linux distros, etc. @@ -80,7 +78,7 @@ The `dotnet restore` command above gets the runtimes for `linux-x64`, `linux-arm ## Authors -* **Lee HUMPHRIES** - *Initial work* - [md8n](https://github.com/md8n) +* **Lee HUMPHRIES** - *Initial work*, and *everything else* - [md8n](https://github.com/md8n) ## License diff --git a/CLI/CLI.csproj b/CLI/CLI.csproj index 953d856..6857dbe 100644 --- a/CLI/CLI.csproj +++ b/CLI/CLI.csproj @@ -21,8 +21,7 @@ The primary objective is to be a `GCode Linter`. - - + @@ -30,6 +29,6 @@ The primary objective is to be a `GCode Linter`. - + diff --git a/CLI/Clean/CleanAction.cs b/CLI/Clean/CleanAction.cs new file mode 100644 index 0000000..82acd85 --- /dev/null +++ b/CLI/Clean/CleanAction.cs @@ -0,0 +1,88 @@ +// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +using GCodeClean.IO; +using GCodeClean.Processing; +using GCodeClean.Structure; + + +namespace GCodeCleanCLI.Clean +{ + public static class CleanAction { + private static (string, List) GetMinimisationStrategy(string minimise, List dedupSelection) { + var minimisationStrategy = string.IsNullOrWhiteSpace(minimise) + ? "SOFT" + : minimise.ToUpperInvariant(); + if (!string.IsNullOrWhiteSpace(minimise) && minimisationStrategy != "SOFT") { + List hardList = [ + 'A', + 'B', + 'C', + 'D', + Letter.feedRate, + Letter.gCommand, + 'H', + 'L', + Letter.mCommand, + Letter.lineNumber, + 'P', + 'R', + Letter.spindleSpeed, + Letter.selectTool, + 'X', + 'Y', + 'Z' + ]; + dedupSelection = minimisationStrategy == "HARD" || minimisationStrategy == "MEDIUM" + ? hardList + : new List(minimisationStrategy).Intersect(hardList).ToList(); + } + + return (minimisationStrategy, dedupSelection); + } + + private static string DetermineOutputFilename(this string inputFile) { + var outputFile = inputFile; + + var inputExtension = Path.GetExtension(inputFile); + if (string.IsNullOrEmpty(inputExtension)) { + outputFile += "-gcc.nc"; + } else { + outputFile = outputFile.Replace(inputExtension, "-gcc" + inputExtension, StringComparison.InvariantCultureIgnoreCase); + } + + return outputFile; + } + + public static async Task ExecuteAsync(FileInfo filename, bool annotate, bool lineNumbers, string minimise, decimal tolerance, decimal arcTolerance, decimal zClamp, JsonDocument tokenDefinitions) { + var inputFile = filename.ToString(); + + var (minimisationStrategy, dedupSelection) = GetMinimisationStrategy(minimise, [Letter.feedRate, 'Z']); + + var outputFile = inputFile.DetermineOutputFilename(); + Console.WriteLine($"Outputting to: {outputFile}"); + + var eliminateNeedlessTravelling = false; // settings.EliminateNeedlessTravelling + + // Determine our starting context + var preambleContext = await inputFile.GetPreambleContext(); + + var inputLines = inputFile.ReadLinesAsync(); + var reassembledLines = inputLines.CleanLines(preambleContext, dedupSelection, minimisationStrategy, lineNumbers, eliminateNeedlessTravelling, zClamp, arcTolerance, tolerance, annotate, tokenDefinitions); + var lineCount = outputFile.WriteLinesAsync(reassembledLines); + + await foreach (var line in lineCount) { + Console.WriteLine($"Output lines: {line}"); + } + + return 0; + } + } +} \ No newline at end of file diff --git a/CLI/Clean/CleanCommand.cs b/CLI/Clean/CleanCommand.cs deleted file mode 100644 index 7235261..0000000 --- a/CLI/Clean/CleanCommand.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -using GCodeClean.IO; -using GCodeClean.Processing; -using GCodeClean.Structure; - -using Spectre.Console; -using Spectre.Console.Cli; - -namespace GCodeCleanCLI.Clean -{ - public class CleanCommand : AsyncCommand { - public static string DetermineOutputFilename(CleanSettings options) { - var inputFile = options.Filename; - var outputFile = inputFile; - - var inputExtension = Path.GetExtension(inputFile); - if (string.IsNullOrEmpty(inputExtension)) { - outputFile += "-gcc.nc"; - } else { - outputFile = outputFile.Replace(inputExtension, "-gcc" + inputExtension, StringComparison.InvariantCultureIgnoreCase); - } - - return outputFile; - } - - public static decimal ConstrainOption(FlagValue option, decimal min, decimal max, string msg) { - var value = min; - if (option.IsSet) { - if (option.Value < min) { - value = min; - } else if (option.Value > max) { - value = max; - } - } - AnsiConsole.MarkupLine($"{msg} [bold yellow]{value}[/]"); - - return value; - } - - public static (string, List) GetMinimisationStrategy(string minimise, List dedupSelection) { - var minimisationStrategy = string.IsNullOrWhiteSpace(minimise) - ? "SOFT" - : minimise.ToUpperInvariant(); - if (!string.IsNullOrWhiteSpace(minimise) && minimisationStrategy != "SOFT") { - List hardList = [ - 'A', 'B', 'C', - 'D', - Letter.feedRate, - Letter.gCommand, - 'H', 'L', - Letter.mCommand, - Letter.lineNumber, - 'P', 'R', - Letter.spindleSpeed, - Letter.selectTool, - 'X', 'Y', 'Z' - ]; - dedupSelection = minimisationStrategy == "HARD" || minimisationStrategy == "MEDIUM" - ? hardList - : new List(minimisationStrategy).Intersect(hardList).ToList(); - } - - return (minimisationStrategy, dedupSelection); - } - - public override async Task ExecuteAsync([NotNull] CommandContext context, [NotNull] CleanSettings settings) { - var inputFile = settings.Filename; - - if (!File.Exists(inputFile)) { - return 1; - } - - var tolerance = ConstrainOption(settings.Tolerance, 0.00005M, 0.5M, "Clipping and general mathematical tolerance:"); - var arcTolerance = ConstrainOption(settings.ArcTolerance, 0.00005M, 0.5M, "Arc simplification tolerance:"); - var zClamp = ConstrainOption(settings.ZClamp, 0.02M, 10.0M, "Z-axis clamping value (max traveling height):"); - AnsiConsole.MarkupLine("[blue]All tolerance and clamping values may be further adjusted to allow for inches vs. millimeters[/]"); - - var (minimisationStrategy, dedupSelection) = GetMinimisationStrategy(settings.Minimise, [Letter.feedRate, 'Z']); - var tokenDefsPath = CleanSettings.GetCleanTokenDefsPath(settings.TokenDefs); - var (tokenDefinitions, _) = CleanSettings.LoadAndVerifyTokenDefs(tokenDefsPath); - - var outputFile = DetermineOutputFilename(settings); - AnsiConsole.MarkupLine($"Outputting to: [bold green]{outputFile}[/]"); - - var eliminateNeedlessTravelling = false; // settings.EliminateNeedlessTravelling - - // Determine our starting context - var preambleContext = await inputFile.GetPreambleContext(); - - var inputLines = inputFile.ReadLinesAsync(); - var reassembledLines = inputLines.CleanLines(preambleContext, dedupSelection, minimisationStrategy, settings.LineNumbers, eliminateNeedlessTravelling, zClamp, arcTolerance, tolerance, settings.Annotate, tokenDefinitions); - var lineCount = outputFile.WriteLinesAsync(reassembledLines); - - await foreach (var line in lineCount) { - AnsiConsole.MarkupLine($"Output lines: [bold yellow]{line}[/]"); - } - - return 0; - } - } -} \ No newline at end of file diff --git a/CLI/Clean/CleanOptions.cs b/CLI/Clean/CleanOptions.cs new file mode 100644 index 0000000..312ccd1 --- /dev/null +++ b/CLI/Clean/CleanOptions.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System; +using System.IO; +using System.Text.Json; + + +namespace GCodeCleanCLI.Clean +{ + public static class CleanOptions { + //[Option("eliminateNeedlessTravelling", Default = false, HelpText = "Eliminate needless 'travelling', extra movements with positive z-axis values")] + //public bool EliminateNeedlessTravelling { get; set; } + + public static FileInfo GetCleanTokenDefsPath(this FileInfo tokenDefsPath) { + if (tokenDefsPath.ToString().Equals("TOKENDEFINITIONS.JSON", StringComparison.InvariantCultureIgnoreCase)) { + var entryDir = Path.GetDirectoryName(AppContext.BaseDirectory); + + tokenDefsPath = new FileInfo($"{entryDir}{Path.DirectorySeparatorChar}tokenDefinitions.json"); + } + return tokenDefsPath; + } + + public static (JsonDocument, string) LoadAndVerifyTokenDefs(this FileInfo tokenDefsPath) { + JsonDocument tokenDefinitions; + + try { + var tokenDefsSource = File.ReadAllText(tokenDefsPath.ToString()); + tokenDefinitions = JsonDocument.Parse(tokenDefsSource); + } catch (FileNotFoundException fileNotFoundEx) { + return (null, $"No token definitions file was found at {tokenDefsPath}. {fileNotFoundEx.Message}"); + } catch (JsonException jsonEx) { + return (null, $"The supplied file {tokenDefsPath} does not appear to be valid JSON. {jsonEx.Message}"); + } catch (Exception e) { + return (null, $"{e}"); + } + + return (tokenDefinitions, ""); + } + + private static decimal ConstrainOption(decimal? option, decimal min, decimal max, string msg) { + var value = min; + if (option.HasValue) { + if (option.Value < min) { + value = min; + } else if (option.Value > max) { + value = max; + } + } + Console.WriteLine($"{msg} {value}"); + + return value; + } + + public static (decimal tolerance, decimal arcTolerance, decimal zClamp) Constrain(decimal tolerance, decimal arcTolerance, decimal zClamp) { + tolerance = ConstrainOption(tolerance, 0.00005M, 0.5M, "Clipping and general mathematical tolerance:"); + arcTolerance = ConstrainOption(arcTolerance, 0.00005M, 0.5M, "Arc simplification tolerance:"); + zClamp = ConstrainOption(zClamp, 0.02M, 10.0M, "Z-axis clamping value (max traveling height):"); + Console.WriteLine("All tolerance and clamping values may be further adjusted to allow for inches vs. millimeters"); + + return (tolerance, arcTolerance, zClamp); + } + } +} \ No newline at end of file diff --git a/CLI/Clean/CleanSettings.cs b/CLI/Clean/CleanSettings.cs deleted file mode 100644 index 5d54e52..0000000 --- a/CLI/Clean/CleanSettings.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. - -using System; -using System.ComponentModel; -using System.IO; -using System.Text.Json; - -using GCodeCleanCLI.Common; - -using Spectre.Console; -using Spectre.Console.Cli; - -namespace GCodeCleanCLI.Clean -{ - public sealed class CleanSettings : CommonSettings { - [CommandOption("--tokenDefs ")] - [Description("Full path to the [bold][italic]tokenDefinitions.json[/][/] file.")] - [DefaultValue("tokenDefinitions.json")] - public string TokenDefs { get; set; } - - [CommandOption("--annotate")] - [Description("Annotate the GCode with inline comments.")] - public bool Annotate { get; set; } - - [CommandOption("--lineNumbers")] - [Description("Keep line numbers")] - [DefaultValue(false)] - public bool LineNumbers { get; set; } - - [CommandOption("--minimise ")] - [Description("Select preferred minimisation strategy,\r\n[bold][italic]'soft'[/][/] - (default) [bold]FZ[/] only,\r\n[bold][italic]'medium'[/][/] - All codes excluding [bold]IJK[/] (but leave spaces in place),\r\n[bold][italic]'hard'[/][/] - All codes excluding [bold]IJK[/] and remove spaces,\r\nor list of codes e.g. [bold]FGXYZ[/]")] - [DefaultValue("soft")] - public string Minimise { get; set; } - - [CommandOption("--tolerance [TOLERANCE]")] - [Description("Enter a clipping tolerance for the various deduplication operations")] - public FlagValue Tolerance { get; set; } - - [CommandOption("--arcTolerance [ARCTOLERANCE]")] - [Description("Enter a tolerance for the 'point-to-point' length of arcs ([bold]G2[/], [bold]G3[/]) below which they will be converted to lines ([bold]G1[/])")] - public FlagValue ArcTolerance { get; set; } - - [CommandOption("--zClamp [ZCLAMP]")] - [Description("Restrict z-axis positive values to the supplied value")] - public FlagValue ZClamp { get; set; } - - //[CommandOption("--eliminateNeedlessTravelling")] - //[Description("Eliminate needless 'travelling', extra movements with positive z-axis values")] - //[DefaultValue(false)] - //public bool EliminateNeedlessTravelling { get; set; } - - public override ValidationResult Validate() { - if (string.IsNullOrWhiteSpace(TokenDefs)) { - return ValidationResult.Error("[bold yellow]The path to the token definitions JSON file is missing. Proper clipping and annotating of the GCode cannot be performed.[/]"); - } - - var tokenDefsPath = GetCleanTokenDefsPath(TokenDefs); - var (tokenDefinitions, errorResult) = LoadAndVerifyTokenDefs(tokenDefsPath); - if (tokenDefinitions == null) { - return ValidationResult.Error(errorResult); - } - - return ValidationResult.Success(); - } - - public static string GetCleanTokenDefsPath(string tokenDefsPath) { - if (tokenDefsPath.Equals("TOKENDEFINITIONS.JSON", StringComparison.InvariantCultureIgnoreCase)) { - var entryDir = Path.GetDirectoryName(AppContext.BaseDirectory); - - tokenDefsPath = $"{entryDir}{Path.DirectorySeparatorChar}tokenDefinitions.json"; - } - return tokenDefsPath; - } - - public static (JsonDocument, string) LoadAndVerifyTokenDefs(string tokenDefsPath) { - JsonDocument tokenDefinitions; - - try { - var tokenDefsSource = File.ReadAllText(tokenDefsPath); - tokenDefinitions = JsonDocument.Parse(tokenDefsSource); - } catch (FileNotFoundException fileNotFoundEx) { - return (null, $"[bold yellow]No token definitions file was found at {tokenDefsPath}. {fileNotFoundEx.Message}[/]"); - } catch (JsonException jsonEx) { - return (null, $"[bold yellow]The supplied file {tokenDefsPath} does not appear to be valid JSON. {jsonEx.Message}[/]"); - } catch (Exception e) { - AnsiConsole.MarkupLine($"[bold yellow]{e}[/]"); - throw; - } - - return (tokenDefinitions, ""); - } - } -} \ No newline at end of file diff --git a/CLI/Common/CommonSettings.cs b/CLI/Common/CommonSettings.cs deleted file mode 100644 index 0c504ab..0000000 --- a/CLI/Common/CommonSettings.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. - -using System.ComponentModel; - -using Spectre.Console.Cli; - -namespace GCodeCleanCLI.Common -{ - public class CommonSettings : CommandSettings { - [CommandOption("-f|--filename ")] - [Description("Full path to the input filename. This is the [italic]only[/] required option.")] - public string Filename { get; set; } - } -} \ No newline at end of file diff --git a/CLI/Merge/MergeAction.cs b/CLI/Merge/MergeAction.cs new file mode 100644 index 0000000..99a5317 --- /dev/null +++ b/CLI/Merge/MergeAction.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for details. + +using System; +using System.IO; +using GCodeClean.Merge; + + +namespace GCodeCleanCLI.Merge +{ + public static class MergeAction { + public static int Execute(DirectoryInfo folder) { + var inputFolder = folder.ToString(); + Console.WriteLine($"Inputting from folder: {inputFolder}"); + + inputFolder.MergeFile(); + + Console.WriteLine("Merge completed"); + + return 0; + } + } +} diff --git a/CLI/Merge/MergeCommand.cs b/CLI/Merge/MergeCommand.cs deleted file mode 100644 index b864931..0000000 --- a/CLI/Merge/MergeCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. - -using System.Diagnostics.CodeAnalysis; - -using GCodeClean.Merge; - -using Spectre.Console; -using Spectre.Console.Cli; - -namespace GCodeCleanCLI.Merge -{ - public class MergeCommand : Command { - public override int Execute([NotNull] CommandContext context, [NotNull] MergeSettings settings) { - var inputFolder = settings.Foldername; - AnsiConsole.MarkupLine($"Inputting from folder: [bold green]{inputFolder}[/]"); - - inputFolder.MergeFile(); - - AnsiConsole.MarkupLine($"Merge completed"); - - return 0; - } - } -} \ No newline at end of file diff --git a/CLI/Merge/MergeSettings.cs b/CLI/Merge/MergeSettings.cs deleted file mode 100644 index 194118a..0000000 --- a/CLI/Merge/MergeSettings.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. - -using System.ComponentModel; - -using Spectre.Console.Cli; - -namespace GCodeCleanCLI.Merge -{ - public class MergeSettings : CommandSettings { - [CommandOption("-f|--foldername ")] - [Description("Full path to the input folder. This is the [italic]only[/] required option.")] - public string Foldername { get; set; } - } -} \ No newline at end of file diff --git a/CLI/Program.cs b/CLI/Program.cs index c14eabe..83d0f9d 100644 --- a/CLI/Program.cs +++ b/CLI/Program.cs @@ -1,13 +1,15 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. +using System.CommandLine; +using System.IO; +using System.Linq; using System.Threading.Tasks; using GCodeCleanCLI.Clean; using GCodeCleanCLI.Merge; using GCodeCleanCLI.Split; -using Spectre.Console.Cli; namespace GCodeCleanCLI { @@ -15,24 +17,106 @@ public static class Program { public static async Task Main(string[] args) { - var app = new CommandApp(); - app.SetDefaultCommand(); - app.Configure(config => { - config.ValidateExamples(); - - config.AddCommand("clean") - .WithDescription("Clean your GCode file. This is the default command"); - config.AddCommand("split") - .WithDescription("Split your GCode file into individual cutting actions"); - config.AddCommand("merge") - .WithDescription("Merge a folder of files, produced by split, back into a single GCode file"); - }); - - if (args.Length == 0) { - args = ["-h"]; - } - - return await app.RunAsync(args); + var filenameOption = new Option( + name: "--filename", + description: "Full path to the input filename" + ) { IsRequired = true }; + + var folderOption = new Option( + name: "--folder", + description: "Full path to the input folder" + ) { IsRequired = true }; + + var tokenDefsOption = new Option( + name: "--tokenDefs", + description: "Full path to the tokenDefinitions.json file", + isDefault: true, + parseArgument: result => { + FileInfo tokDef; + if (!result.Tokens.Any()) { + tokDef = new FileInfo("tokenDefinitions.json"); + } else { + tokDef = new FileInfo(result.Tokens.Single().Value); + } + var tokenDefsPath = tokDef.GetCleanTokenDefsPath(); + var (tokenDefinitions, errorResult) = tokenDefsPath.LoadAndVerifyTokenDefs(); + if (tokenDefinitions == null) { + result.ErrorMessage = errorResult; + } + return tokenDefsPath; + }); + + var annotateOption = new Option( + name: "--annotate", + description: "Annotate the GCode with inline comments", + getDefaultValue: () => false); + + var lineNumbersOption = new Option( + name: "--lineNumbers", + description: "Keep line numbers", + getDefaultValue: () => false); + + var minimiseOption = new Option( + name: "--minimise", + description: "Select preferred minimisation strategy,\r\n'soft' - (default) FZ only,\r\n'medium' - All codes excluding IJK(but leave spaces in place),\r\n'hard' - All codes excluding IJK and remove spaces,\r\nor list of codes e.g.FGXYZ", + getDefaultValue: () => "soft"); + + var toleranceOption = new Option( + name: "--tolerance", + description: "Enter a clipping tolerance for the various deduplication operations. Default value ultimately depends on the units"); + + var arcToleranceOption = new Option( + name: "--arcTolerance", + description: "Enter a tolerance for the 'point-to-point' length of arcs (G2, G3) below which they will be converted to lines (G1)"); + + var zClampOption = new Option( + name: "--zClamp", + description: "Restrict z-axis positive values to the supplied value"); + + var rootCommand = new RootCommand("GCodeClean"); + + var cleanCommand = new Command("clean", "Clean your GCode file.") + { + filenameOption, + tokenDefsOption, + annotateOption, + lineNumbersOption, + minimiseOption, + toleranceOption, + arcToleranceOption, + zClampOption + }; + rootCommand.AddCommand(cleanCommand); + cleanCommand.SetHandler(async (filename, tokenDefs, annotate, lineNumbers, minimise, tolerance, arcTolerance, zClamp) => { + await RunCleanAsync(filename!, tokenDefs, annotate, lineNumbers, minimise, tolerance, arcTolerance, zClamp); + }, + filenameOption, tokenDefsOption, annotateOption, lineNumbersOption, minimiseOption, toleranceOption, arcToleranceOption, zClampOption); + + var splitCommand = new Command("split", "Split your GCode file into individual cutting actions.") { filenameOption }; + rootCommand.AddCommand(splitCommand); + splitCommand.SetHandler((filename) => { RunSplit(filename!); }, filenameOption); + + var mergeCommand = new Command("merge", "Merge a folder of files, produced by split, back into a single GCode file.") { folderOption }; + rootCommand.AddCommand(mergeCommand); + mergeCommand.SetHandler((folder) => { RunMerge(folder!); }, folderOption); + + return await rootCommand.InvokeAsync(args); + } + + //async method + internal static async Task RunCleanAsync(FileInfo filename, FileInfo tokenDefs, bool annotate, bool lineNumbers, string minimise, decimal tolerance, decimal arcTolerance, decimal zClamp) { + (tolerance, arcTolerance, zClamp) = CleanOptions.Constrain(tolerance, arcTolerance, zClamp); + var (tokenDefinitions, _) = tokenDefs.LoadAndVerifyTokenDefs(); + + return await CleanAction.ExecuteAsync(filename, annotate, lineNumbers, minimise, tolerance, arcTolerance, zClamp, tokenDefinitions); + } + + internal static int RunSplit(FileInfo filename) { + return SplitAction.Execute(filename); + } + + internal static int RunMerge(DirectoryInfo filename) { + return MergeAction.Execute(filename); } } } diff --git a/CLI/Split/SplitCommand.cs b/CLI/Split/SplitAction.cs similarity index 58% rename from CLI/Split/SplitCommand.cs rename to CLI/Split/SplitAction.cs index 801fefc..7e2965b 100644 --- a/CLI/Split/SplitCommand.cs +++ b/CLI/Split/SplitAction.cs @@ -1,12 +1,9 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. -using System.Diagnostics.CodeAnalysis; +using System; using System.IO; -using Spectre.Console; -using Spectre.Console.Cli; - using GCodeClean.IO; using GCodeClean.Shared; using GCodeClean.Split; @@ -14,22 +11,20 @@ namespace GCodeCleanCLI.Split { - public class SplitCommand : Command { - - public static string DetermineOutputFoldername(SplitSettings options) { - var inputFile = options.Filename; - + public static class SplitAction { + private static string DetermineOutputFoldername(this string inputFile) { var outputFolderPath = Path.GetDirectoryName(inputFile); var outputFolder = Path.GetFileNameWithoutExtension(inputFile); return Path.Join(outputFolderPath, outputFolder); } - public override int Execute([NotNull] CommandContext context, [NotNull] SplitSettings settings) { - var outputFolder = DetermineOutputFoldername(settings); - AnsiConsole.MarkupLine($"Outputting to folder: [bold green]{outputFolder}[/]"); + public static int Execute(FileInfo filename) { + var inputFile = filename.ToString(); + + var outputFolder = inputFile.DetermineOutputFoldername(); + Console.WriteLine($"Outputting to folder: {outputFolder}"); - var inputFile = settings.Filename; var inputLines = inputFile.ReadFileLines(); var travellingComments = inputLines.GetTravellingComments(); @@ -38,7 +33,7 @@ public override int Execute([NotNull] CommandContext context, [NotNull] SplitSet inputLines.SplitFile(outputFolder, travellingComments, preambleLines, postambleLines); - AnsiConsole.MarkupLine($"Split completed"); + Console.WriteLine($"Split completed"); return 0; } diff --git a/CLI/Split/SplitSettings.cs b/CLI/Split/SplitSettings.cs deleted file mode 100644 index 65293d6..0000000 --- a/CLI/Split/SplitSettings.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. - -using GCodeCleanCLI.Common; - -namespace GCodeCleanCLI.Split -{ - public sealed class SplitSettings : CommonSettings { } -} \ No newline at end of file diff --git a/GCodeClean.Tests/GCodeClean.Tests.csproj b/GCodeClean.Tests/GCodeClean.Tests.csproj index 9a8765a..016eb24 100644 --- a/GCodeClean.Tests/GCodeClean.Tests.csproj +++ b/GCodeClean.Tests/GCodeClean.Tests.csproj @@ -9,8 +9,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -25,7 +25,7 @@ - + diff --git a/GCodeClean/Merge/Algorithm.cs b/GCodeClean/Merge/Algorithm.cs index 1c602d6..ecf661a 100644 --- a/GCodeClean/Merge/Algorithm.cs +++ b/GCodeClean/Merge/Algorithm.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Spectre.Console; using GCodeClean.Processing; using GCodeClean.Shared; @@ -20,7 +19,7 @@ public static class Algorithm /// /// public static List GetPrimaryEdges(this List nodes) { - AnsiConsole.MarkupLine($"Pass [bold yellow]0[/]: Primary Edges"); + Console.WriteLine("Pass 0: Primary Edges"); List primaryEdges = []; foreach (var (tool, id, start, end) in nodes) { @@ -48,13 +47,13 @@ public static List GetPrimaryEdges(this List nodes) { /// /// public static List GetSecondaryEdges(this List pairedEdges, List nodes, short weighting) { - AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Secondary Edges"); + Console.WriteLine($"Pass {weighting}: Secondary Edges"); List seedPairings = [.. pairedEdges.GetResidualSeedPairings(nodes, weighting).Where(sp => sp.Distance == 0)]; return seedPairings.GetFilteredSeedPairings(pairedEdges); } public static List PairSeedingToInjPairings(this List pairedEdges, List nodes, short weighting) { - AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Peer Seeding"); + Console.WriteLine($"Pass {weighting}: Peer Seeding"); List seedPairings = [.. pairedEdges.GetResidualSeedPairings(nodes, weighting).OrderBy(tp => tp.Distance)]; /* Injecting nodes into other existing edge pairings, not found to be useful */ @@ -67,7 +66,7 @@ public static List PairSeedingToInjPairings(this List pairedEdges, L } public static List BuildResidualPairs(this List pairedEdges, List nodes, short weighting) { - AnsiConsole.MarkupLine($"Pass [bold yellow]{weighting}[/]: Residual pairs"); + Console.WriteLine($"Pass {weighting}: Residual pairs"); var unpairedPrevNodes = pairedEdges.UnpairedPrevNodes(nodes).Select(n => n.Id); var unpairedNextNodes = pairedEdges.UnpairedNextNodes(nodes).Select(n => n.Id); diff --git a/GCodeClean/Merge/Edges.cs b/GCodeClean/Merge/Edges.cs index eca3cfb..0ec0da0 100644 --- a/GCodeClean/Merge/Edges.cs +++ b/GCodeClean/Merge/Edges.cs @@ -4,8 +4,6 @@ using System.Collections.Generic; using System.Linq; -using Spectre.Console; - namespace GCodeClean.Merge { diff --git a/GCodeClean/Merge/MergeFile.cs b/GCodeClean/Merge/MergeFile.cs index 184bea1..013f8cd 100644 --- a/GCodeClean/Merge/MergeFile.cs +++ b/GCodeClean/Merge/MergeFile.cs @@ -1,10 +1,9 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. +using System; using System.Linq; -using Spectre.Console; - namespace GCodeClean.Merge { @@ -14,7 +13,7 @@ public static void MergeFile(this string inputFolder) { if (!inputFolder.FolderExists()) { - AnsiConsole.MarkupLine($"No such folder found. Nothing to see here, move along."); + Console.WriteLine("No such folder found. Nothing to see here, move along."); return; } @@ -22,7 +21,7 @@ public static void MergeFile(this string inputFolder) var tools = nodes.Select(n => n.Tool).Distinct().ToList(); if (tools.Count > 1) { - AnsiConsole.MarkupLine("[bold red]Currently only one tool per merge is supported[/]"); + Console.WriteLine("Currently only one tool per merge is supported"); return; } @@ -42,40 +41,18 @@ public static void MergeFile(this string inputFolder) var nodeIdList = pairedEdges.GetNodeIds(); var newDistance = nodes.TotalDistance(nodeIdList); - AnsiConsole.MarkupLine($"Total distinct tools: {tools.Count}"); - AnsiConsole.MarkupLine($"Total nodes: {nodes.Count}"); - AnsiConsole.MarkupLine($"Total edges: {pairedEdges.Count}"); + Console.WriteLine($"Total distinct tools: {tools.Count}"); + Console.WriteLine($"Total nodes: {nodes.Count}"); + Console.WriteLine($"Total edges: {pairedEdges.Count}"); var (startIds, endIds) = pairedEdges.GetStartsAndEnds(); - AnsiConsole.MarkupLine($"Starting node Id: {string.Join(',', startIds)}"); - AnsiConsole.MarkupLine($"Ending node Id: {string.Join(',', endIds)}"); + Console.WriteLine($"Starting node Id: {string.Join(',', startIds)}"); + Console.WriteLine($"Ending node Id: {string.Join(',', endIds)}"); - AnsiConsole.MarkupLine($"Current travelling distance: {currentDistance}"); - AnsiConsole.MarkupLine($"New travelling distance: {newDistance}"); + Console.WriteLine($"Current travelling distance: {currentDistance}"); + Console.WriteLine($"New travelling distance: {newDistance}"); inputFolder.MergeNodes(pairedEdges.GetNodes(nodes)); - - // List<(string tool, List nodeIds)> cutList = []; - // foreach(var toolStartId in toolStartIds) { - // var tool = nodes.First(n => n.Id == toolStartId).Tool; - // var pairedEdge = pairedEdges.Find(pe => pe.PrevId == toolStartId); - // List nodeIds = [pairedEdge.PrevId]; - //#pragma warning disable S2583 - //#pragma warning disable CS8073 - // do { - // var nextId = pairedEdge.NextId; - // nodeIds.Add(nextId); - // pairedEdge = pairedEdges.Find(pe => pe.PrevId == nextId); - // } while (pairedEdge != null); - //#pragma warning restore CS8073 - //#pragma warning restore S2583 - // cutList.Add( (tool, nodeIds) ); - // } - - //foreach (var pair in primaryPairs) { - // AnsiConsole.MarkupLine($"Node primary pairs: [bold yellow]{string.Join(',', pair)}[/]"); - //} - //AnsiConsole.MarkupLine($"Count primary pairs: [bold yellow]{primaryPairs.Count}[/]"); } } } diff --git a/GCodeClean/Merge/NodeFileIO.cs b/GCodeClean/Merge/NodeFileIO.cs index cf2309e..d7ea386 100644 --- a/GCodeClean/Merge/NodeFileIO.cs +++ b/GCodeClean/Merge/NodeFileIO.cs @@ -6,8 +6,6 @@ using System.IO; using System.Linq; -using Spectre.Console; - using GCodeClean.Shared; using GCodeClean.Structure; using GCodeClean.IO; diff --git a/GCodeClean/Merge/NodesAndEdges.cs b/GCodeClean/Merge/NodesAndEdges.cs index e0a59bb..15b83f5 100644 --- a/GCodeClean/Merge/NodesAndEdges.cs +++ b/GCodeClean/Merge/NodesAndEdges.cs @@ -4,8 +4,6 @@ using System.Collections.Generic; using System.Linq; -using Spectre.Console; - using GCodeClean.Processing; using GCodeClean.Shared; diff --git a/GCodeClean/Merge/Utility.cs b/GCodeClean/Merge/Utility.cs index 56810a0..7d65cab 100644 --- a/GCodeClean/Merge/Utility.cs +++ b/GCodeClean/Merge/Utility.cs @@ -1,11 +1,10 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. +using System; using System.Collections.Generic; using System.Linq; -using Spectre.Console; - using GCodeClean.Processing; using GCodeClean.Shared; @@ -67,9 +66,9 @@ public static List GetInjectablePairings(this List pairedEdges, List } } } - AnsiConsole.MarkupLine($"Injection Pairings:"); + Console.WriteLine("Injection Pairings:"); foreach (var pair in injPairings.Select(tps => (tps.PrevId, tps.NextId, tps.Distance, tps.Weighting))) { - AnsiConsole.MarkupLine($"[bold yellow]{pair}[/]"); + Console.WriteLine($"{pair}"); } return injPairings; } diff --git a/GCodeClean/Processing/Processing.cs b/GCodeClean/Processing/Processing.cs index 04cca70..ba19ceb 100644 --- a/GCodeClean/Processing/Processing.cs +++ b/GCodeClean/Processing/Processing.cs @@ -10,7 +10,6 @@ using GCodeClean.Structure; -using Spectre.Console; namespace GCodeClean.Processing { diff --git a/GCodeClean/Split/SplitFile.cs b/GCodeClean/Split/SplitFile.cs index 5e7a854..b6b4890 100644 --- a/GCodeClean/Split/SplitFile.cs +++ b/GCodeClean/Split/SplitFile.cs @@ -1,13 +1,11 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for details. +using System; using System.Collections.Generic; using System.IO; -using Spectre.Console; - using GCodeClean.Processing; -using GCodeClean.Structure; using GCodeClean.Shared; @@ -33,7 +31,7 @@ public static void SplitFile(this IEnumerable inputLines, string outputF foreach (var travelling in travellingComments) { var filename = travelling.ParseTravelling().NodeFileName(outputFolder, idFtm); - AnsiConsole.MarkupLine($"Filename: [bold yellow]{filename}[/]"); + Console.WriteLine($"Filename: {filename}"); File.WriteAllLines(filename, preambleLines); diff --git a/GCodeClean/gcodeclean.csproj b/GCodeClean/gcodeclean.csproj index 53f9190..61bfc68 100644 --- a/GCodeClean/gcodeclean.csproj +++ b/GCodeClean/gcodeclean.csproj @@ -21,7 +21,6 @@ - @@ -32,7 +31,7 @@ - + diff --git a/README.md b/README.md index a56fbcb..ccbd003 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ We also have: There are standalone release builds available, for Linux, Raspberry Pi (linux-arm), and Windows at [GCodeClean releases](https://github.com/md8n/GCodeClean/releases). It is very easy to a build for MacOS / OSX (osx-64) (see `BuildItYourself.md`). Download the release you need and unzip it in a folder that works for you. GCodeClean is a command line application, so you run it by using a 'terminal' and typing the command in to do what you want. -The standalone releases include all the relevant .NET 8.0 libraries for this application. +The standalone releases are single file executables that include all the relevant .NET 8.0 libraries. Alternatively you can build and run this project yourself. See `BuildItYourself.md` for instructions and tips. And how to deploy. And if you do build it yourself then there are a very large number of possible targets including 32bit, and many specific Linux distros, etc. @@ -31,11 +31,11 @@ After downloading the release you need for your OS and unpacking it into its own Change directory to the location where you unpacked (unzipped) the release - you're looking for the file called `GCC.exe`, this is the command line app you'll use. -GCodeClean has two 'commands' `clean` and `split`. `clean` is the one you'll be most interested in. +GCodeClean has tthree 'commands' `clean`, `split` and `merge`. `clean` is the one you'll be most interested in. for Windows you would type in something like and press `enter`: ``` -.\GCC.exe --filename +.\gcc --filename ``` Or for Linux (e.g. Ubuntu 18.04 / 20.04 / 22.04) it would be: @@ -50,55 +50,56 @@ For the above `` tells `GCC` not just the nam ### Command Line Parameters -Throw the `--help` command line option at the GCodeClean `GCC` (in other words type in `GCC --help`) and you'll get back the following: +Throw the `--help` command line option at the GCodeClean `GCC` (in other words type in `.\gcc --help`) and you'll get back the following: ``` -USAGE: - GCC.dll [OPTIONS] +Description: + GCodeClean -OPTIONS: - -h, --help Prints help information - -v, --version Prints version information +Usage: + GCC [command] [options] -COMMANDS: - clean - split +Options: + --version Show version information + -?, -h, --help Show help and usage information + +Commands: + clean Clean your GCode file. + split Split your GCode file into individual cutting actions. + merge Merge a folder of files, produced by split, back into a single GCode file. ``` #### The `clean` Command -Repeat the above `--help` command, but specify `clean` as the command (in other words type in `GCC clean --help`) and you'll get back the following: +Repeat the above `--help` command, but specify `clean` as the command (in other words type in `.\gcc clean --help`) and you'll get back the following: ``` -USAGE: - GCC.dll clean [OPTIONS] - -OPTIONS: - DEFAULT - -h, --help Prints help information - -f, --filename Full path to the input filename. This is the only - required option - --tokenDefs tokenDefinitions.json Full path to the tokenDefinitions.json file - --annotate Annotate the GCode with inline comments - --lineNumbers Keep line numbers - --minimise soft Select preferred minimisation strategy, - 'soft' - (default) FZ only, - 'medium' - All codes excluding IJK (but leave spaces - in place), - 'hard' - All codes excluding IJK and remove spaces, - or list of codes e.g. FGXYZ - --tolerance [TOLERANCE] Enter a clipping tolerance for the various - deduplication operations - --arcTolerance [ARCTOLERANCE] Enter a tolerance for the 'point-to-point' length of - arcs (G2, G3) below which they will be converted to - lines (G1) - --zClamp [ZCLAMP] Restrict z-axis positive values to the supplied value - -COMMANDS: - clean Clean your GCode file. This is the default command +Description: + Clean your GCode file. + +Usage: + GCC clean [options] + +Options: + --filename (REQUIRED) Full path to the input filename + --tokenDefs Full path to the tokenDefinitions.json file [default: + C:\GitHub\GCodeClean\tokenDefinitions.json] + --annotate Annotate the GCode with inline comments [default: False] + --lineNumbers Keep line numbers [default: False] + --minimise Select preferred minimisation strategy, + 'soft' - (default) FZ only, + 'medium' - All codes excluding IJK(but leave spaces in place), + 'hard' - All codes excluding IJK and remove spaces, + or list of codes e.g.FGXYZ [default: soft] + --tolerance Enter a clipping tolerance for the various deduplication operations. Default value + ultimately depends on the units + --arcTolerance Enter a tolerance for the 'point-to-point' length of arcs (G2, G3) below which they will + be converted to lines (G1) + --zClamp Restrict z-axis positive values to the supplied value + -?, -h, --help Show help and usage information ``` -`--annotate` is a simple switch, include it on its own to have your GCode annotated with inline comments (even if you specify hard minimisation). +`--annotate` is a simple switch, include it to have your GCode annotated with inline comments (even if you specify hard minimisation). `--lineNumbers` is also a simple switch. Normally line numbers will be stripped out (they are NOT recommended), but adding this flag will ensure they are preserved (if you must). @@ -130,7 +131,7 @@ The GCodeClean `GCC` will require Read access to that file, and Write access to And then run the `GCC` executable. e.g. for Windows that might look like: ``` -.\GCC.exe --filename FacadeFullAlternate.nc --minimise soft --annotate +.\gcc --filename FacadeFullAlternate.nc --minimise soft --annotate ``` or for Linux (Ubuntu 18.04 / 20.04) @@ -145,23 +146,23 @@ Note: If the input file does not exist (or can't be found, i.e. your typo) then `Exit code`: * `0` - Success -* `2` - File or Directory not found exception - check for typos etc. +* some other number - some exception, for example file / folder not found - check for typos etc. #### The `split` Command -Repeat the above `--help` command, but specify `split` as the command (in other words type in `GCC split --help`) and you'll get back the following: +Repeat the above `--help` command, but specify `split` as the command (in other words type in `.\gcc split --help`) and you'll get back the following: ``` -DESCRIPTION: -Split your GCode file into individual cutting actions +Description: + Split your GCode file into individual cutting actions. -USAGE: - GCC.dll split [OPTIONS] +Usage: + GCC split [options] -OPTIONS: - -h, --help Prints help information - -f, --filename Full path to the input filename. This is the only required option +Options: + --filename (REQUIRED) Full path to the input filename + -?, -h, --help Show help and usage information ``` `split` assumes the `filename` provided, is for a GCode file that has already been `clean`ed. It will create a folder with the same name as `filename` but minus the filename extension (`.nc` etc.). And within that folder it will create one individual file for each cutting path in the original file. @@ -170,6 +171,27 @@ Each individual file should be a valid GCode file that can be run independently. The name of these files will be made up of 4 parts, the tool number, the index of the cutting path from the original file (starting at 0), the starting XY coordinate of the cutting path, and the finishing XY coordinate of the cutting path. Each part of the filename is delimited with an underscore `_`. + +#### The `merge` Command + +Repeat the above `--help` command, but specify `merge` as the command (in other words type in `.\gcc merge --help`) and you'll get back the following: + +``` +Description: + Merge a folder of files, produced by split, back into a single GCode file. + +Usage: + GCC merge [options] + +Options: + --folder (REQUIRED) Full path to the input folder + -?, -h, --help Show help and usage information +``` + +`merge` assumes the `folder` provided, is for a GCode file that has already been `split`. It will examine all of the names of the files in that folder and extract from them the necessary details for it to attempt a 'better' order for each of the cutting paths (individual files). Once it has determined that it will merge all of the files back together in that 'better' order. + +The output file will have `-ts.nc` appended to name of the input folder that you provided on the command line. + ## What's Special about GCodeClean? GCodeClean uses async streaming throughout from input to output. Hopefully this should keep memory consumption and the number of threads to a minimum regardless of what OS / Architecture you use. @@ -235,7 +257,7 @@ G0 X39.29 Y-105.937 ## Authors -* **Lee HUMPHRIES** - *Initial work* - [md8n](https://github.com/md8n) +* **Lee HUMPHRIES** - *Initial work*, and *everything else* - [md8n](https://github.com/md8n) ## License @@ -244,5 +266,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Acknowledgments * To all those comments I picked up out of different people's posts in Stack Overflow -* The quality info on C# 8, and IAsyncEnumerable were invaluable +* The quality info on C# 8, and IAsyncEnumerable (which came out with C# 5) were invaluable * All the sample GCode files provided by the Maslow CNC community [Maslow CNC](https://forums.maslowcnc.com/) From 5220da16ed441c976415defd4569456aec92300b Mon Sep 17 00:00:00 2001 From: md8n Date: Fri, 29 Dec 2023 12:22:45 +0900 Subject: [PATCH 19/19] Complete License change --- BuildItYourself.md | 2 +- GCodeClean.Tests/Dedup.Tests.cs | 2 +- GCodeClean.Tests/Line.Tests.cs | 4 ++-- GCodeClean.Tests/Merge.Tests.cs | 2 +- GCodeClean.Tests/Processing.Tests.cs | 2 +- GCodeClean.Tests/Workflow.Tests.cs | 4 ++-- GCodeClean/Merge/Algorithm.cs | 2 +- GCodeClean/Merge/Edges.cs | 2 +- GCodeClean/Merge/MergeFile.cs | 2 +- GCodeClean/Merge/NodeFileIO.cs | 2 +- GCodeClean/Merge/Nodes.cs | 2 +- GCodeClean/Merge/NodesAndEdges.cs | 2 +- GCodeClean/Merge/Structure.cs | 2 +- GCodeClean/Merge/Utility.cs | 2 +- GCodeClean/Processing/Dedup.cs | 4 ++-- GCodeClean/Processing/Default.cs | 2 +- GCodeClean/Processing/Linter.cs | 2 +- GCodeClean/Processing/Processing.cs | 2 +- GCodeClean/Processing/Tokeniser.cs | 2 +- GCodeClean/Processing/Utility.cs | 2 +- GCodeClean/Processing/Workflow.cs | 2 +- GCodeClean/Shared/Structure.cs | 2 +- GCodeClean/Shared/Utility.cs | 2 +- GCodeClean/Split/SplitFile.cs | 2 +- GCodeClean/Structure/Context.cs | 2 +- GCodeClean/Structure/Coord.cs | 4 ++-- GCodeClean/Structure/Letter.cs | 2 +- GCodeClean/Structure/Line.cs | 2 +- GCodeClean/Structure/ModalGroup.cs | 4 ++-- GCodeClean/Structure/Token.cs | 2 +- README.md | 2 +- 31 files changed, 36 insertions(+), 36 deletions(-) diff --git a/BuildItYourself.md b/BuildItYourself.md index 26be4f0..248c2ef 100644 --- a/BuildItYourself.md +++ b/BuildItYourself.md @@ -82,4 +82,4 @@ The `dotnet restore` command above gets the runtimes for `linux-x64`, `linux-arm ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details +This project is licensed under the AGPL License - see the [LICENSE](LICENSE) file for details diff --git a/GCodeClean.Tests/Dedup.Tests.cs b/GCodeClean.Tests/Dedup.Tests.cs index 77e4319..4379c76 100644 --- a/GCodeClean.Tests/Dedup.Tests.cs +++ b/GCodeClean.Tests/Dedup.Tests.cs @@ -1,5 +1,5 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean.Tests/Line.Tests.cs b/GCodeClean.Tests/Line.Tests.cs index c762360..c24d6da 100644 --- a/GCodeClean.Tests/Line.Tests.cs +++ b/GCodeClean.Tests/Line.Tests.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2021 - Lee HUMPHRIES (lee@md8n.com) and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Copyright (c) 2021-2023 - Lee HUMPHRIES (lee@md8n.com) and contributors. All rights reserved. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using GCodeClean.Structure; diff --git a/GCodeClean.Tests/Merge.Tests.cs b/GCodeClean.Tests/Merge.Tests.cs index 8ba4f8b..35bbf9f 100644 --- a/GCodeClean.Tests/Merge.Tests.cs +++ b/GCodeClean.Tests/Merge.Tests.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com) and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System.Collections.Generic; using System.Linq; diff --git a/GCodeClean.Tests/Processing.Tests.cs b/GCodeClean.Tests/Processing.Tests.cs index f9dd4dc..4ee3f2f 100644 --- a/GCodeClean.Tests/Processing.Tests.cs +++ b/GCodeClean.Tests/Processing.Tests.cs @@ -1,5 +1,5 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean.Tests/Workflow.Tests.cs b/GCodeClean.Tests/Workflow.Tests.cs index 9ec67e4..885dd2f 100644 --- a/GCodeClean.Tests/Workflow.Tests.cs +++ b/GCodeClean.Tests/Workflow.Tests.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Merge/Algorithm.cs b/GCodeClean/Merge/Algorithm.cs index ecf661a..fcef8fd 100644 --- a/GCodeClean/Merge/Algorithm.cs +++ b/GCodeClean/Merge/Algorithm.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Merge/Edges.cs b/GCodeClean/Merge/Edges.cs index 0ec0da0..bbb52e4 100644 --- a/GCodeClean/Merge/Edges.cs +++ b/GCodeClean/Merge/Edges.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System.Collections.Generic; using System.Linq; diff --git a/GCodeClean/Merge/MergeFile.cs b/GCodeClean/Merge/MergeFile.cs index 013f8cd..5ba477c 100644 --- a/GCodeClean/Merge/MergeFile.cs +++ b/GCodeClean/Merge/MergeFile.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Linq; diff --git a/GCodeClean/Merge/NodeFileIO.cs b/GCodeClean/Merge/NodeFileIO.cs index d7ea386..9f83373 100644 --- a/GCodeClean/Merge/NodeFileIO.cs +++ b/GCodeClean/Merge/NodeFileIO.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Merge/Nodes.cs b/GCodeClean/Merge/Nodes.cs index aa5297c..1fb77f0 100644 --- a/GCodeClean/Merge/Nodes.cs +++ b/GCodeClean/Merge/Nodes.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System.Collections.Generic; using System.Linq; diff --git a/GCodeClean/Merge/NodesAndEdges.cs b/GCodeClean/Merge/NodesAndEdges.cs index 15b83f5..ff9a70c 100644 --- a/GCodeClean/Merge/NodesAndEdges.cs +++ b/GCodeClean/Merge/NodesAndEdges.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System.Collections.Generic; using System.Linq; diff --git a/GCodeClean/Merge/Structure.cs b/GCodeClean/Merge/Structure.cs index 84cc530..2d0fdd2 100644 --- a/GCodeClean/Merge/Structure.cs +++ b/GCodeClean/Merge/Structure.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. namespace GCodeClean.Merge { diff --git a/GCodeClean/Merge/Utility.cs b/GCodeClean/Merge/Utility.cs index 7d65cab..8f43d94 100644 --- a/GCodeClean/Merge/Utility.cs +++ b/GCodeClean/Merge/Utility.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Processing/Dedup.cs b/GCodeClean/Processing/Dedup.cs index 641f630..314ce1c 100644 --- a/GCodeClean/Processing/Dedup.cs +++ b/GCodeClean/Processing/Dedup.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2020-2022 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Processing/Default.cs b/GCodeClean/Processing/Default.cs index 848e455..5af4e58 100644 --- a/GCodeClean/Processing/Default.cs +++ b/GCodeClean/Processing/Default.cs @@ -1,5 +1,5 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System.Collections.Generic; diff --git a/GCodeClean/Processing/Linter.cs b/GCodeClean/Processing/Linter.cs index 6bd5cb3..b92f497 100644 --- a/GCodeClean/Processing/Linter.cs +++ b/GCodeClean/Processing/Linter.cs @@ -1,5 +1,5 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System.Collections.Generic; using System.Linq; diff --git a/GCodeClean/Processing/Processing.cs b/GCodeClean/Processing/Processing.cs index ba19ceb..b89dfd1 100644 --- a/GCodeClean/Processing/Processing.cs +++ b/GCodeClean/Processing/Processing.cs @@ -1,5 +1,5 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Processing/Tokeniser.cs b/GCodeClean/Processing/Tokeniser.cs index b3f2f65..ae4c8d0 100644 --- a/GCodeClean/Processing/Tokeniser.cs +++ b/GCodeClean/Processing/Tokeniser.cs @@ -1,5 +1,5 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Processing/Utility.cs b/GCodeClean/Processing/Utility.cs index bb1f4b4..024f136 100644 --- a/GCodeClean/Processing/Utility.cs +++ b/GCodeClean/Processing/Utility.cs @@ -1,5 +1,5 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Processing/Workflow.cs b/GCodeClean/Processing/Workflow.cs index a8083b9..b8f672a 100644 --- a/GCodeClean/Processing/Workflow.cs +++ b/GCodeClean/Processing/Workflow.cs @@ -1,5 +1,5 @@ // Copyright (c) 2022-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System.Collections.Generic; using System.Linq; diff --git a/GCodeClean/Shared/Structure.cs b/GCodeClean/Shared/Structure.cs index cbb0657..740f79e 100644 --- a/GCodeClean/Shared/Structure.cs +++ b/GCodeClean/Shared/Structure.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. // Structures shared by Split and Merge diff --git a/GCodeClean/Shared/Utility.cs b/GCodeClean/Shared/Utility.cs index c16d567..4397bf9 100644 --- a/GCodeClean/Shared/Utility.cs +++ b/GCodeClean/Shared/Utility.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Split/SplitFile.cs b/GCodeClean/Split/SplitFile.cs index b6b4890..07171c6 100644 --- a/GCodeClean/Split/SplitFile.cs +++ b/GCodeClean/Split/SplitFile.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Structure/Context.cs b/GCodeClean/Structure/Context.cs index 1aeaf57..f5bff50 100644 --- a/GCodeClean/Structure/Context.cs +++ b/GCodeClean/Structure/Context.cs @@ -1,5 +1,5 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System.Collections.Generic; using System.Linq; diff --git a/GCodeClean/Structure/Coord.cs b/GCodeClean/Structure/Coord.cs index 2c28e0b..f146eb7 100644 --- a/GCodeClean/Structure/Coord.cs +++ b/GCodeClean/Structure/Coord.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2020 - Lee HUMPHRIES (lee@md8n.com) and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com) and contributors. All rights reserved. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Structure/Letter.cs b/GCodeClean/Structure/Letter.cs index ac3a03d..136a4b7 100644 --- a/GCodeClean/Structure/Letter.cs +++ b/GCodeClean/Structure/Letter.cs @@ -1,5 +1,5 @@ // Copyright (c) 2023 - Lee HUMPHRIES (lee@md8n.com) and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. namespace GCodeClean.Structure { diff --git a/GCodeClean/Structure/Line.cs b/GCodeClean/Structure/Line.cs index a7737d8..4872818 100644 --- a/GCodeClean/Structure/Line.cs +++ b/GCodeClean/Structure/Line.cs @@ -1,5 +1,5 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com). All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Collections.Generic; diff --git a/GCodeClean/Structure/ModalGroup.cs b/GCodeClean/Structure/ModalGroup.cs index 911bea1..f5145c8 100644 --- a/GCodeClean/Structure/ModalGroup.cs +++ b/GCodeClean/Structure/ModalGroup.cs @@ -1,5 +1,5 @@ -// Copyright (c) 2020 - Lee HUMPHRIES (lee@md8n.com) and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com) and contributors. All rights reserved. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System.Collections.Immutable; diff --git a/GCodeClean/Structure/Token.cs b/GCodeClean/Structure/Token.cs index e9fbd47..96c5605 100644 --- a/GCodeClean/Structure/Token.cs +++ b/GCodeClean/Structure/Token.cs @@ -1,5 +1,5 @@ // Copyright (c) 2020-2023 - Lee HUMPHRIES (lee@md8n.com) and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE.txt file in the project root for details. +// Licensed under the AGPL license. See LICENSE.txt file in the project root for details. using System; using System.Linq; diff --git a/README.md b/README.md index ccbd003..ef0050f 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ G0 X39.29 Y-105.937 ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details +This project is licensed under the AGPL License - see the [LICENSE](LICENSE) file for details ## Acknowledgments