diff --git a/OpenNest.Core/Converters/ConvertGeometry.cs b/OpenNest.Core/Converters/ConvertGeometry.cs index bae7b16..fcbcf7e 100644 --- a/OpenNest.Core/Converters/ConvertGeometry.cs +++ b/OpenNest.Core/Converters/ConvertGeometry.cs @@ -95,7 +95,16 @@ namespace OpenNest.Converters } else { - pgm.Codes.Add(new ArcMove(endpt, arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW) { Layer = layer }); + pgm.Codes.Add( + new ArcMove( + endpt, + arc.Center, + arc.IsReversed ? RotationType.CW : RotationType.CCW + ) + { + Layer = layer, + } + ); } return lastpt; @@ -108,7 +117,12 @@ namespace OpenNest.Converters if (startpt.DistanceTo(lastpt) > Tolerance.ChainTolerance) pgm.MoveTo(startpt); - pgm.Codes.Add(new ArcMove(startpt, circle.Center, circle.Rotation) { Layer = ClassifyLayer(circle) }); + pgm.Codes.Add( + new ArcMove(startpt, circle.Center, circle.Rotation) + { + Layer = ClassifyLayer(circle), + } + ); lastpt = startpt; return lastpt; @@ -130,8 +144,10 @@ namespace OpenNest.Converters private static LayerType ClassifyLayer(Entity geo) { var name = geo.Layer?.Name; - if (string.Equals(name, "ENGRAVE", System.StringComparison.OrdinalIgnoreCase) || - string.Equals(name, "ETCH", System.StringComparison.OrdinalIgnoreCase)) + if ( + string.Equals(name, "ENGRAVE", System.StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "ETCH", System.StringComparison.OrdinalIgnoreCase) + ) return LayerType.Scribe; return LayerType.Cut; diff --git a/OpenNest.Core/Geometry/ArcFit.cs b/OpenNest.Core/Geometry/ArcFit.cs index 6240d5c..72b1dab 100644 --- a/OpenNest.Core/Geometry/ArcFit.cs +++ b/OpenNest.Core/Geometry/ArcFit.cs @@ -14,7 +14,9 @@ namespace OpenNest.Geometry /// the arc passes through both endpoints and departs P1 in the given direction. /// internal static (Vector center, double radius, double deviation) FitWithStartTangent( - List points, Vector tangent) + List points, + Vector tangent + ) { if (points.Count < 3) return (Vector.Invalid, 0, double.MaxValue); @@ -64,8 +66,15 @@ namespace OpenNest.Geometry /// two requested ones — when the requested tangents are consistent with a single /// circular arc, both are matched exactly. /// - internal static (Vector center, double radius, double deviation) FitThroughEndpointsWithTangents( - List points, Vector startTangent, Vector endTangent) + internal static ( + Vector center, + double radius, + double deviation + ) FitThroughEndpointsWithTangents( + List points, + Vector startTangent, + Vector endTangent + ) { if (points.Count < 3) return (Vector.Invalid, 0, double.MaxValue); @@ -104,14 +113,20 @@ namespace OpenNest.Geometry private static double SignedAngle(double ux, double uy, Vector to) { var len = System.Math.Sqrt(to.X * to.X + to.Y * to.Y); - if (len < 1e-10) return 0; + if (len < 1e-10) + return 0; return System.Math.Atan2(ux * to.Y - uy * to.X, ux * to.X + uy * to.Y); } /// /// Computes the maximum radial deviation of interior points from a circle. /// - internal static double MaxRadialDeviation(List points, double cx, double cy, double radius) + internal static double MaxRadialDeviation( + List points, + double cx, + double cy, + double radius + ) { var maxDev = 0.0; for (var i = 1; i < points.Count - 1; i++) @@ -120,7 +135,8 @@ namespace OpenNest.Geometry var py = points[i].Y - cy; var dist = System.Math.Sqrt(px * px + py * py); var dev = System.Math.Abs(dist - radius); - if (dev > maxDev) maxDev = dev; + if (dev > maxDev) + maxDev = dev; } return maxDev; } diff --git a/OpenNest.Core/Geometry/GeometrySimplifier.cs b/OpenNest.Core/Geometry/GeometrySimplifier.cs index 11f11b3..72cf371 100644 --- a/OpenNest.Core/Geometry/GeometrySimplifier.cs +++ b/OpenNest.Core/Geometry/GeometrySimplifier.cs @@ -15,8 +15,10 @@ public class ArcCandidate public double MaxDeviation { get; set; } public Box BoundingBox { get; set; } public bool IsSelected { get; set; } = true; + /// First point of the original line segments this candidate covers. public Vector FirstPoint { get; set; } + /// Last point of the original line segments this candidate covers. public Vector LastPoint { get; set; } } @@ -46,9 +48,7 @@ public class MirrorAxisResult var dx = p.X - Point.X; var dy = p.Y - Point.Y; var dot = dx * Direction.X + dy * Direction.Y; - return new Vector( - p.X - 2 * (dx - dot * Direction.X), - p.Y - 2 * (dy - dot * Direction.Y)); + return new Vector(p.X - 2 * (dx - dot * Direction.X), p.Y - 2 * (dy - dot * Direction.Y)); } } @@ -74,9 +74,14 @@ public class GeometrySimplifier var runStart = i; var layerName = entities[i].Layer?.Name; var lineCount = 0; - while (i < entities.Count && (entities[i] is Line || entities[i] is Arc) && entities[i].Layer?.Name == layerName) + while ( + i < entities.Count + && (entities[i] is Line || entities[i] is Arc) + && entities[i].Layer?.Name == layerName + ) { - if (entities[i] is Line) lineCount++; + if (entities[i] is Line) + lineCount++; i++; } var runEnd = i - 1; @@ -90,10 +95,7 @@ public class GeometrySimplifier public Shape Apply(Shape shape, List candidates) { - var selected = candidates - .Where(c => c.IsSelected) - .OrderBy(c => c.StartIndex) - .ToList(); + var selected = candidates.Where(c => c.IsSelected).OrderBy(c => c.StartIndex).ToList(); var newEntities = new List(); var i = 0; @@ -132,11 +134,10 @@ public class GeometrySimplifier foreach (var e in shape.Entities) midpoints.Add(e.BoundingBox.Center); - if (midpoints.Count < 4) return MirrorAxisResult.None; + if (midpoints.Count < 4) + return MirrorAxisResult.None; - var centroid = new Vector( - midpoints.Average(p => p.X), - midpoints.Average(p => p.Y)); + var centroid = new Vector(midpoints.Average(p => p.X), midpoints.Average(p => p.Y)); var cx = centroid.X; var cy = centroid.Y; @@ -190,8 +191,7 @@ public class GeometrySimplifier return bestResult.Score >= 0.8 ? bestResult : MirrorAxisResult.None; } - private static double NormalizeAngle(double angle) => - angle < 0 ? angle + Angle.TwoPI : angle; + private static double NormalizeAngle(double angle) => angle < 0 ? angle + Angle.TwoPI : angle; private static Vector Normalize(Vector v) { @@ -231,7 +231,8 @@ public class GeometrySimplifier for (var j = 0; j < points.Count; j++) { - if (i == j) continue; + if (i == j) + continue; var d = reflected.DistanceTo(points[j]); if (d < matchTol) { @@ -251,17 +252,20 @@ public class GeometrySimplifier /// public void Symmetrize(List candidates, MirrorAxisResult axis) { - if (!axis.IsValid || candidates.Count < 2) return; + if (!axis.IsValid || candidates.Count < 2) + return; var paired = new HashSet(); for (var i = 0; i < candidates.Count; i++) { - if (paired.Contains(i)) continue; + if (paired.Contains(i)) + continue; var ci = candidates[i]; var ciCenter = ci.BoundingBox.Center; - if (PerpendicularDistance(ciCenter, axis.Point, axis.Direction) < 0.1) continue; // on the axis + if (PerpendicularDistance(ciCenter, axis.Point, axis.Direction) < 0.1) + continue; // on the axis var mirrorCenter = axis.Reflect(ciCenter); @@ -269,7 +273,8 @@ public class GeometrySimplifier var bestDist = double.MaxValue; for (var j = i + 1; j < candidates.Count; j++) { - if (paired.Contains(j)) continue; + if (paired.Contains(j)) + continue; var d = mirrorCenter.DistanceTo(candidates[j].BoundingBox.Center); if (d < bestDist) { @@ -279,7 +284,8 @@ public class GeometrySimplifier } var matchTol = System.Math.Max(ci.BoundingBox.Width, ci.BoundingBox.Length) * 0.5; - if (bestJ < 0 || bestDist > matchTol) continue; + if (bestJ < 0 || bestDist > matchTol) + continue; paired.Add(i); paired.Add(bestJ); @@ -287,7 +293,10 @@ public class GeometrySimplifier var cj = candidates[bestJ]; var sourceIdx = i; var targetIdx = bestJ; - if (cj.LineCount > ci.LineCount || (cj.LineCount == ci.LineCount && cj.MaxDeviation < ci.MaxDeviation)) + if ( + cj.LineCount > ci.LineCount + || (cj.LineCount == ci.LineCount && cj.MaxDeviation < ci.MaxDeviation) + ) { sourceIdx = bestJ; targetIdx = i; @@ -323,8 +332,12 @@ public class GeometrySimplifier var mirrorEp = axis.Reflect(ep); // Mirroring reverses winding — swap start/end to preserve arc direction - var mirrorStart = NormalizeAngle(System.Math.Atan2(mirrorEp.Y - mirrorCenter.Y, mirrorEp.X - mirrorCenter.X)); - var mirrorEnd = NormalizeAngle(System.Math.Atan2(mirrorSp.Y - mirrorCenter.Y, mirrorSp.X - mirrorCenter.X)); + var mirrorStart = NormalizeAngle( + System.Math.Atan2(mirrorEp.Y - mirrorCenter.Y, mirrorEp.X - mirrorCenter.X) + ); + var mirrorEnd = NormalizeAngle( + System.Math.Atan2(mirrorSp.Y - mirrorCenter.Y, mirrorSp.X - mirrorCenter.X) + ); var result = new Arc(mirrorCenter, arc.Radius, mirrorStart, mirrorEnd, arc.IsReversed); result.Layer = arc.Layer; @@ -332,7 +345,12 @@ public class GeometrySimplifier return result; } - private void FindCandidatesInRun(List entities, int runStart, int runEnd, List candidates) + private void FindCandidatesInRun( + List entities, + int runStart, + int runEnd, + List candidates + ) { var j = runStart; var chainedTangent = Vector.Invalid; @@ -349,45 +367,63 @@ public class GeometrySimplifier chainedTangent = ComputeEndTangent(result.Center, result.Points); var arc = CreateArc(result.Center, result.Radius, result.Points, entities[j]); - candidates.Add(new ArcCandidate - { - StartIndex = j, - EndIndex = result.EndIndex, - FittedArc = arc, - MaxDeviation = result.Deviation, - BoundingBox = result.Points.GetBoundingBox(), - FirstPoint = arc.StartPoint(), - LastPoint = arc.EndPoint(), - }); + candidates.Add( + new ArcCandidate + { + StartIndex = j, + EndIndex = result.EndIndex, + FittedArc = arc, + MaxDeviation = result.Deviation, + BoundingBox = result.Points.GetBoundingBox(), + FirstPoint = arc.StartPoint(), + LastPoint = arc.EndPoint(), + } + ); j = result.EndIndex + 1; } } - private record ArcFitResult(Vector Center, double Radius, double Deviation, List Points, int EndIndex); + private record ArcFitResult( + Vector Center, + double Radius, + double Deviation, + List Points, + int EndIndex + ); - private ArcFitResult TryFitArcAt(List entities, int start, int runEnd, Vector chainedTangent) + private ArcFitResult TryFitArcAt( + List entities, + int start, + int runEnd, + Vector chainedTangent + ) { var k = start + MinLines - 1; - if (k > runEnd) return null; + if (k > runEnd) + return null; var points = CollectPoints(entities, start, k); - if (points.Count < 3) return null; + if (points.Count < 3) + return null; var startTangent = EstimateStartTangent(entities, start, points, chainedTangent); var endTangent = EstimateEndTangent(entities, k, points); var (center, radius, dev) = TryFit(points, startTangent, endTangent); - if (!center.IsValid()) return null; + if (!center.IsValid()) + return null; // Extend the arc as far as possible while (k + 1 <= runEnd) { var extPoints = CollectPoints(entities, start, k + 1); - if (extPoints.Count < 3) break; + if (extPoints.Count < 3) + break; var extEndTangent = EstimateEndTangent(entities, k + 1, extPoints); var (nc, nr, nd) = TryFit(extPoints, startTangent, extEndTangent); - if (!nc.IsValid()) break; + if (!nc.IsValid()) + break; k++; center = nc; @@ -407,7 +443,10 @@ public class GeometrySimplifier } private (Vector center, double radius, double deviation) TryFit( - List points, TangentEstimate start, TangentEstimate end) + List points, + TangentEstimate start, + TangentEstimate end + ) { foreach (var (center, radius, dev) in FitAttempts(points, start, end)) { @@ -434,23 +473,38 @@ public class GeometrySimplifier /// passes exactly through both endpoints, so no gaps are introduced. /// private IEnumerable<(Vector center, double radius, double deviation)> FitAttempts( - List points, TangentEstimate start, TangentEstimate end) + List points, + TangentEstimate start, + TangentEstimate end + ) { if (start.Trusted && !end.Trusted) { yield return ArcFit.FitWithStartTangent(points, start.Direction); - yield return ArcFit.FitThroughEndpointsWithTangents(points, start.Direction, end.Direction); + yield return ArcFit.FitThroughEndpointsWithTangents( + points, + start.Direction, + end.Direction + ); yield return FitWithEndTangent(points, end.Direction); } else if (end.Trusted && !start.Trusted) { yield return FitWithEndTangent(points, end.Direction); - yield return ArcFit.FitThroughEndpointsWithTangents(points, start.Direction, end.Direction); + yield return ArcFit.FitThroughEndpointsWithTangents( + points, + start.Direction, + end.Direction + ); yield return ArcFit.FitWithStartTangent(points, start.Direction); } else { - yield return ArcFit.FitThroughEndpointsWithTangents(points, start.Direction, end.Direction); + yield return ArcFit.FitThroughEndpointsWithTangents( + points, + start.Direction, + end.Direction + ); yield return ArcFit.FitWithStartTangent(points, start.Direction); yield return FitWithEndTangent(points, end.Direction); } @@ -463,7 +517,9 @@ public class GeometrySimplifier /// by running the start-tangent fit on the reversed point sequence. /// private static (Vector center, double radius, double deviation) FitWithEndTangent( - List points, Vector endTangent) + List points, + Vector endTangent + ) { var reversed = new List(points); reversed.Reverse(); @@ -483,7 +539,11 @@ public class GeometrySimplifier private const double NeighborEdgeFactor = 3.0; private static TangentEstimate EstimateStartTangent( - List entities, int start, List points, Vector chainedTangent) + List entities, + int start, + List points, + Vector chainedTangent + ) { if (chainedTangent.IsValid()) return new TangentEstimate(chainedTangent, true); @@ -495,23 +555,39 @@ public class GeometrySimplifier if (start > 0) { var prev = entities[start - 1]; - var prevEnd = prev switch { Line l => l.EndPoint, Arc a => a.EndPoint(), _ => Vector.Invalid }; + var prevEnd = prev switch + { + Line l => l.EndPoint, + Arc a => a.EndPoint(), + _ => Vector.Invalid, + }; if (prevEnd.IsValid() && prevEnd.DistanceTo(points[0]) < 1e-6) { if (prev is Arc) return new TangentEstimate(GetExitDirection(prev), true); - if (prev is Line prevLine && prevLine.StartPoint.DistanceTo(prevLine.EndPoint) >= NeighborEdgeFactor * firstChordLen) + if ( + prev is Line prevLine + && prevLine.StartPoint.DistanceTo(prevLine.EndPoint) + >= NeighborEdgeFactor * firstChordLen + ) return new TangentEstimate(GetExitDirection(prevLine), true); } } var chord = new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y); if (points.Count >= 3) - return new TangentEstimate(EstimateVertexTangent(points[0], points[1], points[2], chord), false); + return new TangentEstimate( + EstimateVertexTangent(points[0], points[1], points[2], chord), + false + ); return new TangentEstimate(chord, false); } - private static TangentEstimate EstimateEndTangent(List entities, int k, List points) + private static TangentEstimate EstimateEndTangent( + List entities, + int k, + List points + ) { if (entities[k] is Arc endArc) return new TangentEstimate(GetExitDirection(endArc), true); @@ -520,19 +596,31 @@ public class GeometrySimplifier if (k + 1 < entities.Count) { var next = entities[k + 1]; - var nextStart = next switch { Line l => l.StartPoint, Arc a => a.StartPoint(), _ => Vector.Invalid }; + var nextStart = next switch + { + Line l => l.StartPoint, + Arc a => a.StartPoint(), + _ => Vector.Invalid, + }; if (nextStart.IsValid() && nextStart.DistanceTo(points[^1]) < 1e-6) { if (next is Arc nextArc) return new TangentEstimate(GetEntryDirection(nextArc), true); - if (next is Line nextLine && nextLine.StartPoint.DistanceTo(nextLine.EndPoint) >= NeighborEdgeFactor * lastChordLen) + if ( + next is Line nextLine + && nextLine.StartPoint.DistanceTo(nextLine.EndPoint) + >= NeighborEdgeFactor * lastChordLen + ) return new TangentEstimate(GetExitDirection(nextLine), true); } } var chord = new Vector(points[^1].X - points[^2].X, points[^1].Y - points[^2].Y); if (points.Count >= 3) - return new TangentEstimate(EstimateVertexTangent(points[^1], points[^2], points[^3], chord), false); + return new TangentEstimate( + EstimateVertexTangent(points[^1], points[^2], points[^3], chord), + false + ); return new TangentEstimate(chord, false); } @@ -563,14 +651,18 @@ public class GeometrySimplifier /// /// Returns the entry direction (tangent at start point) of an entity. /// - private static Vector GetEntryDirection(Entity entity) => entity switch - { - Line line => new Vector(line.EndPoint.X - line.StartPoint.X, line.EndPoint.Y - line.StartPoint.Y), - Arc arc => arc.IsReversed - ? new Vector(System.Math.Sin(arc.StartAngle), -System.Math.Cos(arc.StartAngle)) - : new Vector(-System.Math.Sin(arc.StartAngle), System.Math.Cos(arc.StartAngle)), - _ => Vector.Invalid, - }; + private static Vector GetEntryDirection(Entity entity) => + entity switch + { + Line line => new Vector( + line.EndPoint.X - line.StartPoint.X, + line.EndPoint.Y - line.StartPoint.Y + ), + Arc arc => arc.IsReversed + ? new Vector(System.Math.Sin(arc.StartAngle), -System.Math.Cos(arc.StartAngle)) + : new Vector(-System.Math.Sin(arc.StartAngle), System.Math.Cos(arc.StartAngle)), + _ => Vector.Invalid, + }; /// /// Computes the tangent direction at the last point of a fitted arc, @@ -622,9 +714,17 @@ public class GeometrySimplifier var dInit = (maxSagitta * maxSagitta - halfChord * halfChord) / (2 * maxSagitta); var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord); - var dOpt = GoldenSectionMin(dInit - range, dInit + range, - d => ArcFit.MaxRadialDeviation(points, mx + d * nx, my + d * ny, - System.Math.Sqrt(halfChord * halfChord + d * d))); + var dOpt = GoldenSectionMin( + dInit - range, + dInit + range, + d => + ArcFit.MaxRadialDeviation( + points, + mx + d * nx, + my + d * ny, + System.Math.Sqrt(halfChord * halfChord + d * d) + ) + ); var center = new Vector(mx + dOpt * nx, my + dOpt * ny); var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt); @@ -676,13 +776,22 @@ public class GeometrySimplifier return points; } - private static Arc CreateArc(Vector center, double radius, List points, Entity sourceEntity) + private static Arc CreateArc( + Vector center, + double radius, + List points, + Entity sourceEntity + ) { var firstPoint = points[0]; var lastPoint = points[^1]; - var startAngle = NormalizeAngle(System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X)); - var endAngle = NormalizeAngle(System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X)); + var startAngle = NormalizeAngle( + System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X) + ); + var endAngle = NormalizeAngle( + System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X) + ); var isReversed = SumSignedAngles(center, points) < 0; var arc = new Arc(center, radius, startAngle, endAngle, isReversed); @@ -694,14 +803,18 @@ public class GeometrySimplifier /// /// Returns the exit direction (tangent at endpoint) of an entity. /// - private static Vector GetExitDirection(Entity entity) => entity switch - { - Line line => new Vector(line.EndPoint.X - line.StartPoint.X, line.EndPoint.Y - line.StartPoint.Y), - Arc arc => arc.IsReversed - ? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle)) - : new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)), - _ => Vector.Invalid, - }; + private static Vector GetExitDirection(Entity entity) => + entity switch + { + Line line => new Vector( + line.EndPoint.X - line.StartPoint.X, + line.EndPoint.Y - line.StartPoint.Y + ), + Arc arc => arc.IsReversed + ? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle)) + : new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)), + _ => Vector.Invalid, + }; /// /// Sums signed angular change traversing consecutive points around a center. @@ -715,8 +828,10 @@ public class GeometrySimplifier var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X); var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X); var da = a2 - a1; - while (da > System.Math.PI) da -= Angle.TwoPI; - while (da < -System.Math.PI) da += Angle.TwoPI; + while (da > System.Math.PI) + da -= Angle.TwoPI; + while (da < -System.Math.PI) + da += Angle.TwoPI; total += da; } return total; @@ -727,7 +842,12 @@ public class GeometrySimplifier /// back to the original line segments. This catches cases where points lie /// on a large circle but the arc bulges far from the original straight geometry. /// - private static double MaxArcToSegmentDeviation(List points, Vector center, double radius, bool isReversed) + private static double MaxArcToSegmentDeviation( + List points, + Vector center, + double radius, + bool isReversed + ) { var startAngle = System.Math.Atan2(points[0].Y - center.Y, points[0].X - center.X); var endAngle = System.Math.Atan2(points[^1].Y - center.Y, points[^1].X - center.X); @@ -735,11 +855,13 @@ public class GeometrySimplifier var sweep = endAngle - startAngle; if (isReversed) { - if (sweep > 0) sweep -= Angle.TwoPI; + if (sweep > 0) + sweep -= Angle.TwoPI; } else { - if (sweep < 0) sweep += Angle.TwoPI; + if (sweep < 0) + sweep += Angle.TwoPI; } var sampleCount = System.Math.Max(10, (int)(System.Math.Abs(sweep) * radius * 10)); @@ -758,9 +880,11 @@ public class GeometrySimplifier for (var j = 0; j < points.Count - 1; j++) { var dist = DistanceToSegment(arcPt, points[j], points[j + 1]); - if (dist < minDist) minDist = dist; + if (dist < minDist) + minDist = dist; } - if (minDist > maxDev) maxDev = minDist; + if (minDist > maxDev) + maxDev = minDist; } return maxDev; } diff --git a/OpenNest.Posts.GravographIS/GravographISPostConfig.cs b/OpenNest.Posts.GravographIS/GravographISPostConfig.cs index 15f7bce..9322240 100644 --- a/OpenNest.Posts.GravographIS/GravographISPostConfig.cs +++ b/OpenNest.Posts.GravographIS/GravographISPostConfig.cs @@ -15,18 +15,23 @@ namespace OpenNest.Posts.GravographIS public int FeedMmPerSec { get; set; } = 10; [DisplayName("Depth (inches)")] - [Description("Programmed Z plunge (DZ). Note: the spring-floated spindle means this does not set actual cut depth — tool protrusion does.")] + [Description( + "Programmed Z plunge (DZ). Note: the spring-floated spindle means this does not set actual cut depth — tool protrusion does." + )] public double Depth { get; set; } = 0.25; [DisplayName("Pause Before")] - [Description("Stop the spindle and prompt the operator before this pass begins, so the tool can be swapped/adjusted.")] + [Description( + "Stop the spindle and prompt the operator before this pass begins, so the tool can be swapped/adjusted." + )] public bool PauseBefore { get; set; } [DisplayName("Pause Message")] [Description("Message shown on the controller during the pause.")] public string PauseMessage { get; set; } = ""; - public override string ToString() => $"{FeedMmPerSec} mm/s, {Depth:0.###}\"" + (PauseBefore ? ", pause" : ""); + public override string ToString() => + $"{FeedMmPerSec} mm/s, {Depth:0.###}\"" + (PauseBefore ? ", pause" : ""); } /// @@ -40,24 +45,28 @@ namespace OpenNest.Posts.GravographIS [Category("Engrave (Scribe)")] [DisplayName("Engrave")] [Description("Parameters for engrave/scribe geometry (text).")] - public LayerCutConfig Engrave { get; set; } = new LayerCutConfig - { - FeedMmPerSec = 10, - Depth = 0.25, - PauseBefore = false, - PauseMessage = "", - }; + public LayerCutConfig Engrave { get; set; } = + new LayerCutConfig + { + FeedMmPerSec = 10, + Depth = 0.25, + PauseBefore = false, + PauseMessage = "", + }; [Category("Cut")] [DisplayName("Cut")] - [Description("Parameters for cut geometry (outlines). Pauses for a tool change by default.")] - public LayerCutConfig Cut { get; set; } = new LayerCutConfig - { - FeedMmPerSec = 3, - Depth = 0.25, - PauseBefore = true, - PauseMessage = "Change tool", - }; + [Description( + "Parameters for cut geometry (outlines). Pauses for a tool change by default." + )] + public LayerCutConfig Cut { get; set; } = + new LayerCutConfig + { + FeedMmPerSec = 3, + Depth = 0.25, + PauseBefore = true, + PauseMessage = "Change tool", + }; /// /// Returns the cut config a polyline of the given layer should use, or null diff --git a/OpenNest.Posts.GravographIS/GravographISPostProcessor.cs b/OpenNest.Posts.GravographIS/GravographISPostProcessor.cs index 7d1fd45..7139f32 100644 --- a/OpenNest.Posts.GravographIS/GravographISPostProcessor.cs +++ b/OpenNest.Posts.GravographIS/GravographISPostProcessor.cs @@ -22,7 +22,7 @@ namespace OpenNest.Posts.GravographIS private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, - Converters = { new JsonStringEnumConverter() } + Converters = { new JsonStringEnumConverter() }, }; public string Name => "Gravograph IS8000"; @@ -48,7 +48,8 @@ namespace OpenNest.Posts.GravographIS if (File.Exists(configPath)) { var json = File.ReadAllText(configPath); - Config = JsonSerializer.Deserialize(json, JsonOptions) + Config = + JsonSerializer.Deserialize(json, JsonOptions) ?? new GravographISPostConfig(); } else @@ -103,14 +104,16 @@ namespace OpenNest.Posts.GravographIS /// public IReadOnlyList BuildPasses(IEnumerable polylines) { - if (polylines == null) throw new ArgumentNullException(nameof(polylines)); + if (polylines == null) + throw new ArgumentNullException(nameof(polylines)); var engrave = new List>(); var cut = new List>(); foreach (var poly in polylines) { - if (poly == null) continue; + if (poly == null) + continue; var block = Config.ConfigFor(poly.Layer); if (block == null) continue; // non-cutting (Display) geometry diff --git a/OpenNest.Posts.GravographIS/GravographISWriter.cs b/OpenNest.Posts.GravographIS/GravographISWriter.cs index 24fffdf..5a27b57 100644 --- a/OpenNest.Posts.GravographIS/GravographISWriter.cs +++ b/OpenNest.Posts.GravographIS/GravographISWriter.cs @@ -48,16 +48,99 @@ namespace OpenNest.Posts.GravographIS // fixed return-to-home block. private static readonly byte[] PreambleTemplate = new byte[] { - 0x21, 0x41, 0x53, 0x20, 0x33, 0x38, 0x3b, 0x01, 0x90, 0x01, - 0xf4, 0x01, 0x90, 0x01, 0xf4, 0x01, 0x90, 0x01, 0xf4, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x09, 0x00, 0x00, 0x03, 0xe8, 0x05, 0x06, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfd, 0x32, 0x44, 0x00, - 0x00, 0xff, 0xfd, 0x4d, 0x43, 0x00, 0x01, 0xff, 0xfd, 0x4f, - 0x55, 0xff, 0xfb, 0xff, 0xfd, 0x4f, 0x55, 0xff, 0xfa, 0xff, - 0xfd, 0x50, 0x5a, 0x00, 0x00, 0xff, 0xfd, 0x56, 0x53, 0x00, - 0x23, 0xff, 0xfd, 0x56, 0x5a, 0x00, 0x23, 0xff, 0xfd, 0x44, - 0x5a, 0x01, 0xfc, + 0x21, + 0x41, + 0x53, + 0x20, + 0x33, + 0x38, + 0x3b, + 0x01, + 0x90, + 0x01, + 0xf4, + 0x01, + 0x90, + 0x01, + 0xf4, + 0x01, + 0x90, + 0x01, + 0xf4, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x09, + 0x00, + 0x00, + 0x03, + 0xe8, + 0x05, + 0x06, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xff, + 0xfd, + 0x32, + 0x44, + 0x00, + 0x00, + 0xff, + 0xfd, + 0x4d, + 0x43, + 0x00, + 0x01, + 0xff, + 0xfd, + 0x4f, + 0x55, + 0xff, + 0xfb, + 0xff, + 0xfd, + 0x4f, + 0x55, + 0xff, + 0xfa, + 0xff, + 0xfd, + 0x50, + 0x5a, + 0x00, + 0x00, + 0xff, + 0xfd, + 0x56, + 0x53, + 0x00, + 0x23, + 0xff, + 0xfd, + 0x56, + 0x5a, + 0x00, + 0x23, + 0xff, + 0xfd, + 0x44, + 0x5a, + 0x01, + 0xfc, }; // Stripped 36-byte postamble: lift, aux off, motor off, operator beep, @@ -73,11 +156,36 @@ namespace OpenNest.Posts.GravographIS // lift + PU travel to the operator-set origin before these final commands. private static readonly byte[] EndJobBytes = new byte[] { - 0xff, 0xfd, 0x4f, 0x55, 0xff, 0xfa, // OU 0xFFFA aux off - 0xff, 0xfd, 0x4f, 0x55, 0xff, 0xfb, // OU 0xFFFB aux off - 0xff, 0xfd, 0x4d, 0x43, 0x00, 0x00, // MC 0x0000 motor off - 0xff, 0xfd, 0x4f, 0x50, 0x00, 0x00, // OP 0x0000 operator beep - 0xff, 0xfd, 0x4a, 0x46, 0x00, 0x00, // JF 0x0000 job finish + 0xff, + 0xfd, + 0x4f, + 0x55, + 0xff, + 0xfa, // OU 0xFFFA aux off + 0xff, + 0xfd, + 0x4f, + 0x55, + 0xff, + 0xfb, // OU 0xFFFB aux off + 0xff, + 0xfd, + 0x4d, + 0x43, + 0x00, + 0x00, // MC 0x0000 motor off + 0xff, + 0xfd, + 0x4f, + 0x50, + 0x00, + 0x00, // OP 0x0000 operator beep + 0xff, + 0xfd, + 0x4a, + 0x46, + 0x00, + 0x00, // JF 0x0000 job finish }; // 80 steps/mm × 25.4 mm/in @@ -86,9 +194,7 @@ namespace OpenNest.Posts.GravographIS public GravographISWriterOptions Options { get; } public GravographISWriter() - : this(new GravographISWriterOptions()) - { - } + : this(new GravographISWriterOptions()) { } public GravographISWriter(GravographISWriterOptions options) { @@ -103,21 +209,25 @@ namespace OpenNest.Posts.GravographIS /// public void Write(IEnumerable> polylines, Stream output) { - if (polylines == null) throw new ArgumentNullException(nameof(polylines)); + if (polylines == null) + throw new ArgumentNullException(nameof(polylines)); // A single pass at the configured feed/depth — byte-identical to the // original single-group output (no transitions, no pause). - Write(new[] - { - new GravographPass + Write( + new[] { - Polylines = polylines, - FeedMmPerSec = Options.FeedMmPerSec, - DepthInches = Options.DepthInches, - PauseBefore = false, - PauseMessage = "", + new GravographPass + { + Polylines = polylines, + FeedMmPerSec = Options.FeedMmPerSec, + DepthInches = Options.DepthInches, + PauseBefore = false, + PauseMessage = "", + }, }, - }, output); + output + ); } /// @@ -128,8 +238,10 @@ namespace OpenNest.Posts.GravographIS /// public void Write(IReadOnlyList passes, Stream output) { - if (passes == null) throw new ArgumentNullException(nameof(passes)); - if (output == null) throw new ArgumentNullException(nameof(output)); + if (passes == null) + throw new ArgumentNullException(nameof(passes)); + if (output == null) + throw new ArgumentNullException(nameof(output)); var firstFeed = passes.Count > 0 ? passes[0].FeedMmPerSec : Options.FeedMmPerSec; var firstDepth = passes.Count > 0 ? passes[0].DepthInches : Options.DepthInches; @@ -146,10 +258,16 @@ namespace OpenNest.Posts.GravographIS // catch bad records before they ship to the engraver. var headX = 0; var headY = 0; - var envelopeXSteps = (int)System.Math.Round(Options.WorkEnvelopeXMm * StepsPerMm, - MidpointRounding.AwayFromZero); - var envelopeYSteps = (int)System.Math.Round(Options.WorkEnvelopeYMm * StepsPerMm, - MidpointRounding.AwayFromZero); + var envelopeXSteps = (int) + System.Math.Round( + Options.WorkEnvelopeXMm * StepsPerMm, + MidpointRounding.AwayFromZero + ); + var envelopeYSteps = (int) + System.Math.Round( + Options.WorkEnvelopeYMm * StepsPerMm, + MidpointRounding.AwayFromZero + ); var firstPolyline = true; var polyIndex = 0; @@ -167,9 +285,18 @@ namespace OpenNest.Posts.GravographIS // Park: lift Z, then rapid (pen-up) back to the operator origin so // the head is clear of the work while the tool is swapped. WriteLiftOnly(output); - WriteTravel(output, (byte)'P', (byte)'U', - checked(-headX), checked(-headY), - ref headX, ref headY, envelopeXSteps, envelopeYSteps, polyIndex); + WriteTravel( + output, + (byte)'P', + (byte)'U', + checked(-headX), + checked(-headY), + ref headX, + ref headY, + envelopeXSteps, + envelopeYSteps, + polyIndex + ); WritePauseCore(output, pass.PauseMessage); } @@ -182,12 +309,18 @@ namespace OpenNest.Posts.GravographIS if (pass.DepthInches != currentDepth) { - WriteCommand(output, (byte)'D', (byte)'Z', DepthInStepsAsInt16(pass.DepthInches)); + WriteCommand( + output, + (byte)'D', + (byte)'Z', + DepthInStepsAsInt16(pass.DepthInches) + ); currentDepth = pass.DepthInches; } } - if (pass.Polylines == null) continue; + if (pass.Polylines == null) + continue; foreach (var poly in pass.Polylines) { @@ -195,31 +328,62 @@ namespace OpenNest.Posts.GravographIS if (poly == null || poly.Count < 2) continue; - WritePolyline(output, poly, ref firstPolyline, ref headX, ref headY, - envelopeXSteps, envelopeYSteps, polyIndex); + WritePolyline( + output, + poly, + ref firstPolyline, + ref headX, + ref headY, + envelopeXSteps, + envelopeYSteps, + polyIndex + ); } } WriteLiftOnly(output); if (Options.ReturnToOriginAtEnd && !firstPolyline) { - WriteTravel(output, (byte)'P', (byte)'U', - checked(-headX), checked(-headY), - ref headX, ref headY, envelopeXSteps, envelopeYSteps, polyIndex); + WriteTravel( + output, + (byte)'P', + (byte)'U', + checked(-headX), + checked(-headY), + ref headX, + ref headY, + envelopeXSteps, + envelopeYSteps, + polyIndex + ); } output.Write(EndJobBytes, 0, EndJobBytes.Length); } - private void WritePolyline(Stream output, IReadOnlyList poly, - ref bool firstPolyline, ref int headX, ref int headY, - int envelopeXSteps, int envelopeYSteps, int polyIndex) + private void WritePolyline( + Stream output, + IReadOnlyList poly, + ref bool firstPolyline, + ref int headX, + ref int headY, + int envelopeXSteps, + int envelopeYSteps, + int polyIndex + ) { var (startX, startY) = ToWire(poly[0]); - WriteTravel(output, + WriteTravel( + output, firstPolyline ? (byte)'D' : (byte)'P', firstPolyline ? (byte)'R' : (byte)'U', - checked(startX - headX), checked(startY - headY), - ref headX, ref headY, envelopeXSteps, envelopeYSteps, polyIndex); + checked(startX - headX), + checked(startY - headY), + ref headX, + ref headY, + envelopeXSteps, + envelopeYSteps, + polyIndex + ); // PD command + single records-follow flag, then one record per segment. output.WriteByte(0xFF); @@ -236,8 +400,15 @@ namespace OpenNest.Posts.GravographIS var (cx, cy) = ToWire(poly[i]); var dx = checked(cx - prevX); var dy = checked(cy - prevY); - EnsureEnvelope(headX + dx, headY + dy, envelopeXSteps, envelopeYSteps, - polyIndex, segment: i, isTravel: false); + EnsureEnvelope( + headX + dx, + headY + dy, + envelopeXSteps, + envelopeYSteps, + polyIndex, + segment: i, + isTravel: false + ); WriteRecord(output, dx, dy); prevX = cx; prevY = cy; @@ -267,7 +438,8 @@ namespace OpenNest.Posts.GravographIS // space-padded to a whole number of packets so widths stay 2 bytes. private static void WriteMessagePackets(Stream s, string message) { - if (string.IsNullOrEmpty(message)) return; + if (string.IsNullOrEmpty(message)) + return; var chars = Encoding.ASCII.GetBytes(message); for (var i = 0; i < chars.Length; i += 2) @@ -295,11 +467,18 @@ namespace OpenNest.Posts.GravographIS private const double StepsPerMm = 80.0; - private void EnsureEnvelope(int wireX, int wireY, - int envXSteps, int envYSteps, - int polyIndex, int segment, bool isTravel) + private void EnsureEnvelope( + int wireX, + int wireY, + int envXSteps, + int envYSteps, + int polyIndex, + int segment, + bool isTravel + ) { - if (!Options.EnvelopeGuardEnabled) return; + if (!Options.EnvelopeGuardEnabled) + return; // Wire frame: X is identity to input; Y is negated. With the operator // origin set at the upper-left of the work envelope and an OpenNest @@ -313,16 +492,21 @@ namespace OpenNest.Posts.GravographIS var inputY = -wireY / (double)StepsPerInch; var kind = isTravel ? "pen-up travel" : "cut segment"; throw new InvalidOperationException( - $"Polyline {polyIndex} {kind} (segment {segment}) would place the head at " + - $"({inputX:F3}\", {inputY:F3}\"), outside the {Options.WorkEnvelopeXMm}×{Options.WorkEnvelopeYMm} mm " + - $"work envelope from upper-left origin. Refusing to emit the record."); + $"Polyline {polyIndex} {kind} (segment {segment}) would place the head at " + + $"({inputX:F3}\", {inputY:F3}\"), outside the {Options.WorkEnvelopeXMm}×{Options.WorkEnvelopeYMm} mm " + + $"work envelope from upper-left origin. Refusing to emit the record." + ); } private static short DepthInStepsAsInt16(double depthInches) { - var steps = (long)System.Math.Round(depthInches * StepsPerInch, MidpointRounding.AwayFromZero); + var steps = (long) + System.Math.Round(depthInches * StepsPerInch, MidpointRounding.AwayFromZero); if (steps < short.MinValue || steps > short.MaxValue) - throw new ArgumentOutOfRangeException(nameof(depthInches), $"Depth {depthInches} in. → {steps} steps overflows int16."); + throw new ArgumentOutOfRangeException( + nameof(depthInches), + $"Depth {depthInches} in. → {steps} steps overflows int16." + ); return (short)steps; } @@ -335,10 +519,18 @@ namespace OpenNest.Posts.GravographIS return (x, y); } - private void WriteTravel(Stream s, byte c0, byte c1, int dx, int dy, - ref int headX, ref int headY, - int envelopeXSteps, int envelopeYSteps, - int polyIndex) + private void WriteTravel( + Stream s, + byte c0, + byte c1, + int dx, + int dy, + ref int headX, + ref int headY, + int envelopeXSteps, + int envelopeYSteps, + int polyIndex + ) { if (dx == 0 && dy == 0) return; @@ -352,20 +544,31 @@ namespace OpenNest.Posts.GravographIS var chunks = System.Math.Max( (int)System.Math.Ceiling(System.Math.Abs(dx) / (double)short.MaxValue), - (int)System.Math.Ceiling(System.Math.Abs(dy) / (double)short.MaxValue)); - if (chunks < 1) chunks = 1; + (int)System.Math.Ceiling(System.Math.Abs(dy) / (double)short.MaxValue) + ); + if (chunks < 1) + chunks = 1; var emittedX = 0; var emittedY = 0; for (var i = 1; i <= chunks; i++) { - var targetX = (int)System.Math.Round(dx * (i / (double)chunks), MidpointRounding.AwayFromZero); - var targetY = (int)System.Math.Round(dy * (i / (double)chunks), MidpointRounding.AwayFromZero); + var targetX = (int) + System.Math.Round(dx * (i / (double)chunks), MidpointRounding.AwayFromZero); + var targetY = (int) + System.Math.Round(dy * (i / (double)chunks), MidpointRounding.AwayFromZero); var chunkX = checked(targetX - emittedX); var chunkY = checked(targetY - emittedY); - EnsureEnvelope(headX + chunkX, headY + chunkY, envelopeXSteps, envelopeYSteps, - polyIndex, segment: 0, isTravel: true); + EnsureEnvelope( + headX + chunkX, + headY + chunkY, + envelopeXSteps, + envelopeYSteps, + polyIndex, + segment: 0, + isTravel: true + ); WriteRecord(s, chunkX, chunkY); emittedX = targetX; @@ -399,11 +602,16 @@ namespace OpenNest.Posts.GravographIS private static void WriteRecord(Stream s, int dx, int dy) { - if (dx < short.MinValue || dx > short.MaxValue || - dy < short.MinValue || dy > short.MaxValue) + if ( + dx < short.MinValue + || dx > short.MaxValue + || dy < short.MinValue + || dy > short.MaxValue + ) { throw new InvalidOperationException( - $"Move delta ({dx}, {dy}) steps overflows signed int16 — split moves upstream."); + $"Move delta ({dx}, {dy}) steps overflows signed int16 — split moves upstream." + ); } int word1; @@ -423,11 +631,15 @@ namespace OpenNest.Posts.GravographIS else { var maxAbs = System.Math.Max(absDx, absDy); - word1 = (int)System.Math.Round(16384.0 * maxAbs / len, MidpointRounding.AwayFromZero); + word1 = (int) + System.Math.Round(16384.0 * maxAbs / len, MidpointRounding.AwayFromZero); param = (int)System.Math.Round(len / 22.4, MidpointRounding.AwayFromZero); - if (param < 1) param = 1; - if (param > 180) param = 180; - if (word1 > 16384) word1 = 16384; + if (param < 1) + param = 1; + if (param > 180) + param = 180; + if (word1 > 16384) + word1 = 16384; } WriteBigEndianInt16(s, (short)word1); @@ -448,8 +660,12 @@ namespace OpenNest.Posts.GravographIS { for (int i = 0; i <= buffer.Length - 6; i++) { - if (buffer[i] == 0xFF && buffer[i + 1] == 0xFD && - buffer[i + 2] == c0 && buffer[i + 3] == c1) + if ( + buffer[i] == 0xFF + && buffer[i + 1] == 0xFD + && buffer[i + 2] == c0 + && buffer[i + 3] == c1 + ) { buffer[i + 4] = (byte)((value >> 8) & 0xFF); buffer[i + 5] = (byte)(value & 0xFF); @@ -458,7 +674,8 @@ namespace OpenNest.Posts.GravographIS } throw new InvalidOperationException( - $"Command '{(char)c0}{(char)c1}' not found in preamble template."); + $"Command '{(char)c0}{(char)c1}' not found in preamble template." + ); } } } diff --git a/OpenNest.Posts.GravographIS/NestPolylineExtractor.cs b/OpenNest.Posts.GravographIS/NestPolylineExtractor.cs index 3434f17..577eec3 100644 --- a/OpenNest.Posts.GravographIS/NestPolylineExtractor.cs +++ b/OpenNest.Posts.GravographIS/NestPolylineExtractor.cs @@ -59,7 +59,8 @@ namespace OpenNest.Posts.GravographIS /// public List ExtractLayered(Nest nest) { - if (nest == null) throw new ArgumentNullException(nameof(nest)); + if (nest == null) + throw new ArgumentNullException(nameof(nest)); var result = new List(); @@ -91,7 +92,8 @@ namespace OpenNest.Posts.GravographIS private void ExtractPart(Part part, List sink) { var program = part.Program; - if (program == null) return; + if (program == null) + return; // The walk below treats Motion.EndPoint as absolute. Convert a working // copy to absolute mode so G91 programs (the form OpenNest's UI writes) @@ -123,7 +125,13 @@ namespace OpenNest.Posts.GravographIS case LinearMove linear: { - StartOrSplit(sink, ref current, ref currentLayer, linear.Layer, pos + offset); + StartOrSplit( + sink, + ref current, + ref currentLayer, + linear.Layer, + pos + offset + ); var end = linear.EndPoint; current.Add(end + offset); pos = end; @@ -147,8 +155,13 @@ namespace OpenNest.Posts.GravographIS // seeded at `seed` (the current pen position). When the layer changes // mid-chain the previous polyline is flushed and a new one begins at the // shared seam vertex so engrave and cut passes stay geometrically continuous. - private static void StartOrSplit(List sink, ref List current, - ref LayerType currentLayer, LayerType moveLayer, Vector seed) + private static void StartOrSplit( + List sink, + ref List current, + ref LayerType currentLayer, + LayerType moveLayer, + Vector seed + ) { if (current == null) { @@ -163,7 +176,11 @@ namespace OpenNest.Posts.GravographIS } } - private static void FlushCurrent(List sink, ref List current, LayerType layer) + private static void FlushCurrent( + List sink, + ref List current, + LayerType layer + ) { if (current != null && current.Count >= 2) sink.Add(new LayeredPolyline(current, layer)); @@ -175,8 +192,13 @@ namespace OpenNest.Posts.GravographIS // (G-code I/J in this codebase are stored as the absolute center), arc.EndPoint // is absolute end. The starting point is assumed to already be in the polyline; // intermediate samples and the endpoint are appended. - private static void TessellateArc(Vector start, ArcMove arc, Vector offset, - double chordTol, List sink) + private static void TessellateArc( + Vector start, + ArcMove arc, + Vector offset, + double chordTol, + List sink + ) { var c = arc.CenterPoint; var r = c.DistanceTo(start); @@ -193,18 +215,22 @@ namespace OpenNest.Posts.GravographIS if (arc.Rotation == RotationType.CW) { sweep = a0 - a1; - if (sweep <= 0) sweep += 2 * System.Math.PI; + if (sweep <= 0) + sweep += 2 * System.Math.PI; } else { sweep = a1 - a0; - if (sweep <= 0) sweep += 2 * System.Math.PI; + if (sweep <= 0) + sweep += 2 * System.Math.PI; } // Treat a near-zero sweep with coincident start/end as a full circle. - if (sweep < 1e-9 && - System.Math.Abs(start.X - arc.EndPoint.X) < 1e-9 && - System.Math.Abs(start.Y - arc.EndPoint.Y) < 1e-9) + if ( + sweep < 1e-9 + && System.Math.Abs(start.X - arc.EndPoint.X) < 1e-9 + && System.Math.Abs(start.Y - arc.EndPoint.Y) < 1e-9 + ) { sweep = 2 * System.Math.PI; } @@ -215,7 +241,8 @@ namespace OpenNest.Posts.GravographIS maxAngleStep = System.Math.PI / 32; var steps = (int)System.Math.Ceiling(sweep / maxAngleStep); - if (steps < 1) steps = 1; + if (steps < 1) + steps = 1; var direction = arc.Rotation == RotationType.CW ? -1.0 : 1.0; for (int i = 1; i < steps; i++) diff --git a/OpenNest.Tests/Converters/ConvertGeometryLayerTests.cs b/OpenNest.Tests/Converters/ConvertGeometryLayerTests.cs index 2243855..b4b2ecf 100644 --- a/OpenNest.Tests/Converters/ConvertGeometryLayerTests.cs +++ b/OpenNest.Tests/Converters/ConvertGeometryLayerTests.cs @@ -37,7 +37,10 @@ public class ConvertGeometryLayerTests [Fact] public void AddArc_EngraveLayer_TagsScribe() { - var arc = new Arc(new Vector(0, 0), 1, 0, System.Math.PI / 2) { Layer = new Layer("ENGRAVE") }; + var arc = new Arc(new Vector(0, 0), 1, 0, System.Math.PI / 2) + { + Layer = new Layer("ENGRAVE"), + }; var pgm = ProgramFor(arc); diff --git a/OpenNest.Tests/Geometry/GeometrySimplifierTests.cs b/OpenNest.Tests/Geometry/GeometrySimplifierTests.cs index 44ba0b1..b659734 100644 --- a/OpenNest.Tests/Geometry/GeometrySimplifierTests.cs +++ b/OpenNest.Tests/Geometry/GeometrySimplifierTests.cs @@ -166,8 +166,14 @@ public class GeometrySimplifierTests // Arc must be tangent to the adjacent straight edges at its endpoints var startDelta = AngleBetweenDeg(ArcTangentAt(arc, arc.StartPoint()), new Vector(1, 0)); var endDelta = AngleBetweenDeg(ArcTangentAt(arc, arc.EndPoint()), new Vector(0, 1)); - Assert.True(startDelta < 0.3, $"Arc start not tangent to incoming line: off by {startDelta:F3} deg"); - Assert.True(endDelta < 0.3, $"Arc end not tangent to outgoing line: off by {endDelta:F3} deg"); + Assert.True( + startDelta < 0.3, + $"Arc start not tangent to incoming line: off by {startDelta:F3} deg" + ); + Assert.True( + endDelta < 0.3, + $"Arc end not tangent to outgoing line: off by {endDelta:F3} deg" + ); } [Fact] @@ -184,7 +190,12 @@ public class GeometrySimplifierTests { var ang = OpenNest.Math.Angle.ToRadians(10 * i); var radius = r1 + deltas1[i]; - pts.Add(new Vector(c1.X + radius * System.Math.Cos(ang), c1.Y + radius * System.Math.Sin(ang))); + pts.Add( + new Vector( + c1.X + radius * System.Math.Cos(ang), + c1.Y + radius * System.Math.Sin(ang) + ) + ); } // Second arc center along the junction radius so tangents match at the junction @@ -197,7 +208,12 @@ public class GeometrySimplifierTests { var ang = OpenNest.Math.Angle.ToRadians(60 + 8 * i); var radius = r2 + deltas2[i]; - pts.Add(new Vector(c2.X + radius * System.Math.Cos(ang), c2.Y + radius * System.Math.Sin(ang))); + pts.Add( + new Vector( + c2.X + radius * System.Math.Cos(ang), + c2.Y + radius * System.Math.Sin(ang) + ) + ); } var shape = new Shape(); @@ -215,8 +231,14 @@ public class GeometrySimplifierTests Assert.True(arcA.EndPoint().DistanceTo(arcB.StartPoint()) < 1e-6); // Tangent continuity across the junction - var junctionDelta = AngleBetweenDeg(ArcTangentAt(arcA, arcA.EndPoint()), ArcTangentAt(arcB, arcB.StartPoint())); - Assert.True(junctionDelta < 0.3, $"Tangent break of {junctionDelta:F3} deg at arc-arc junction"); + var junctionDelta = AngleBetweenDeg( + ArcTangentAt(arcA, arcA.EndPoint()), + ArcTangentAt(arcB, arcB.StartPoint()) + ); + Assert.True( + junctionDelta < 0.3, + $"Tangent break of {junctionDelta:F3} deg at arc-arc junction" + ); } private static Vector ArcTangentAt(Arc arc, Vector pt) diff --git a/OpenNest.Tests/GravographIS/GravographISPostProcessorTests.cs b/OpenNest.Tests/GravographIS/GravographISPostProcessorTests.cs index 89399eb..53db1ba 100644 --- a/OpenNest.Tests/GravographIS/GravographISPostProcessorTests.cs +++ b/OpenNest.Tests/GravographIS/GravographISPostProcessorTests.cs @@ -7,19 +7,21 @@ namespace OpenNest.Tests.GravographIS; public class GravographISPostProcessorTests { - private static LayeredPolyline Poly(LayerType layer, params Vector[] pts) - => new LayeredPolyline(new List(pts), layer); + private static LayeredPolyline Poly(LayerType layer, params Vector[] pts) => + new LayeredPolyline(new List(pts), layer); [Fact] public void BuildPasses_EngraveAndCut_OrdersEngraveFirstThenCutWithPause() { var post = new GravographISPostProcessor(); - var passes = post.BuildPasses(new[] - { - Poly(LayerType.Cut, new Vector(0, 0), new Vector(1, 0)), - Poly(LayerType.Scribe, new Vector(0, 0), new Vector(0, 1)), - }); + var passes = post.BuildPasses( + new[] + { + Poly(LayerType.Cut, new Vector(0, 0), new Vector(1, 0)), + Poly(LayerType.Scribe, new Vector(0, 0), new Vector(0, 1)), + } + ); Assert.Equal(2, passes.Count); @@ -36,10 +38,9 @@ public class GravographISPostProcessorTests { var post = new GravographISPostProcessor(); - var passes = post.BuildPasses(new[] - { - Poly(LayerType.Cut, new Vector(0, 0), new Vector(1, 0)), - }); + var passes = post.BuildPasses( + new[] { Poly(LayerType.Cut, new Vector(0, 0), new Vector(1, 0)) } + ); Assert.Single(passes); Assert.Equal(post.Config.Cut.FeedMmPerSec, passes[0].FeedMmPerSec); @@ -50,11 +51,13 @@ public class GravographISPostProcessorTests { var post = new GravographISPostProcessor(); - var passes = post.BuildPasses(new[] - { - Poly(LayerType.Display, new Vector(0, 0), new Vector(1, 0)), - Poly(LayerType.Cut, new Vector(0, 0), new Vector(0, 1)), - }); + var passes = post.BuildPasses( + new[] + { + Poly(LayerType.Display, new Vector(0, 0), new Vector(1, 0)), + Poly(LayerType.Cut, new Vector(0, 0), new Vector(0, 1)), + } + ); Assert.Single(passes); Assert.Equal(post.Config.Cut.FeedMmPerSec, passes[0].FeedMmPerSec); diff --git a/OpenNest.Tests/GravographIS/GravographISWriterTests.cs b/OpenNest.Tests/GravographIS/GravographISWriterTests.cs index 74f052d..9fcaaa4 100644 --- a/OpenNest.Tests/GravographIS/GravographISWriterTests.cs +++ b/OpenNest.Tests/GravographIS/GravographISWriterTests.cs @@ -191,20 +191,37 @@ public class GravographISWriterTests [Fact] public void Passes_PauseBeforeCut_EmitsPauseSequenceBetweenGroups() { - var engrave = new List> { new[] { new Vector(0, 0), new Vector(1, 0) } }; + var engrave = new List> + { + new[] { new Vector(0, 0), new Vector(1, 0) }, + }; var cut = new List> { new[] { new Vector(0, 0), new Vector(0, -1) } }; var passes = new List { - new GravographPass { Polylines = engrave, FeedMmPerSec = 10, DepthInches = 0.25 }, - new GravographPass { Polylines = cut, FeedMmPerSec = 3, DepthInches = 0.25, PauseBefore = true, PauseMessage = "Hi" }, + new GravographPass + { + Polylines = engrave, + FeedMmPerSec = 10, + DepthInches = 0.25, + }, + new GravographPass + { + Polylines = cut, + FeedMmPerSec = 3, + DepthInches = 0.25, + PauseBefore = true, + PauseMessage = "Hi", + }, }; using var ms = new MemoryStream(); - new GravographISWriter(new GravographISWriterOptions - { - EnvelopeGuardEnabled = false, - ReturnToOriginAtEnd = false, - }).Write(passes, ms); + new GravographISWriter( + new GravographISWriterOptions + { + EnvelopeGuardEnabled = false, + ReturnToOriginAtEnd = false, + } + ).Write(passes, ms); var bytes = ms.ToArray(); var mcOff = IndexOf(bytes, 0, (byte)'M', (byte)'C', 0x00, 0x00); @@ -217,9 +234,16 @@ public class GravographISWriterTests var mcOn = IndexOf(bytes, lbEnd, (byte)'M', (byte)'C', 0x00, 0x01); Assert.True(mcOff >= 0, "motor-off (MC 0000) not found"); - Assert.True(mcOff < ouFb && ouFb < ouFa && ouFa < lbBegin && lbBegin < lbMsg - && lbMsg < nr && nr < lbEnd && lbEnd < mcOn, - "pause commands out of order"); + Assert.True( + mcOff < ouFb + && ouFb < ouFa + && ouFa < lbBegin + && lbBegin < lbMsg + && lbMsg < nr + && nr < lbEnd + && lbEnd < mcOn, + "pause commands out of order" + ); // Resume sets the cut feed inline (VS 0x0003) after the motor restarts. var vsCut = IndexOf(bytes, mcOn, (byte)'V', (byte)'S', 0x00, 0x03); @@ -231,12 +255,34 @@ public class GravographISWriterTests { var passes = new List { - new GravographPass { Polylines = new List> { new[] { new Vector(0, 0), new Vector(1, 0) } }, FeedMmPerSec = 10 }, - new GravographPass { Polylines = new List> { new[] { new Vector(0, 0), new Vector(0, -1) } }, FeedMmPerSec = 3, PauseBefore = true, PauseMessage = "abc" }, + new GravographPass + { + Polylines = new List> + { + new[] { new Vector(0, 0), new Vector(1, 0) }, + }, + FeedMmPerSec = 10, + }, + new GravographPass + { + Polylines = new List> + { + new[] { new Vector(0, 0), new Vector(0, -1) }, + }, + FeedMmPerSec = 3, + PauseBefore = true, + PauseMessage = "abc", + }, }; using var ms = new MemoryStream(); - new GravographISWriter(new GravographISWriterOptions { EnvelopeGuardEnabled = false, ReturnToOriginAtEnd = false }).Write(passes, ms); + new GravographISWriter( + new GravographISWriterOptions + { + EnvelopeGuardEnabled = false, + ReturnToOriginAtEnd = false, + } + ).Write(passes, ms); var bytes = ms.ToArray(); var lbAb = IndexOf(bytes, 0, (byte)'L', (byte)'B', (byte)'a', (byte)'b'); @@ -250,24 +296,57 @@ public class GravographISWriterTests { var passes = new List { - new GravographPass { Polylines = new List> { new[] { new Vector(0, 0), new Vector(1, 0) } }, FeedMmPerSec = 10 }, - new GravographPass { Polylines = new List> { new[] { new Vector(0, 0), new Vector(0, -1) } }, FeedMmPerSec = 3, PauseBefore = false }, + new GravographPass + { + Polylines = new List> + { + new[] { new Vector(0, 0), new Vector(1, 0) }, + }, + FeedMmPerSec = 10, + }, + new GravographPass + { + Polylines = new List> + { + new[] { new Vector(0, 0), new Vector(0, -1) }, + }, + FeedMmPerSec = 3, + PauseBefore = false, + }, }; using var ms = new MemoryStream(); - new GravographISWriter(new GravographISWriterOptions { EnvelopeGuardEnabled = false, ReturnToOriginAtEnd = false }).Write(passes, ms); + new GravographISWriter( + new GravographISWriterOptions + { + EnvelopeGuardEnabled = false, + ReturnToOriginAtEnd = false, + } + ).Write(passes, ms); var bytes = ms.ToArray(); - Assert.True(IndexOf(bytes, 0, (byte)'V', (byte)'S', 0x00, 0x03) >= 0, "inline cut feed change missing"); - Assert.True(IndexOfCmd(bytes, (byte)'L', (byte)'B') < 0, "no LB message expected without a pause"); + Assert.True( + IndexOf(bytes, 0, (byte)'V', (byte)'S', 0x00, 0x03) >= 0, + "inline cut feed change missing" + ); + Assert.True( + IndexOfCmd(bytes, (byte)'L', (byte)'B') < 0, + "no LB message expected without a pause" + ); } private static int IndexOf(byte[] bytes, int from, byte c0, byte c1, byte hi, byte lo) { for (var i = System.Math.Max(0, from); i <= bytes.Length - 6; i++) { - if (bytes[i] == 0xFF && bytes[i + 1] == 0xFD && bytes[i + 2] == c0 && - bytes[i + 3] == c1 && bytes[i + 4] == hi && bytes[i + 5] == lo) + if ( + bytes[i] == 0xFF + && bytes[i + 1] == 0xFD + && bytes[i + 2] == c0 + && bytes[i + 3] == c1 + && bytes[i + 4] == hi + && bytes[i + 5] == lo + ) return i; } return -1; @@ -277,7 +356,12 @@ public class GravographISWriterTests { for (var i = 0; i <= bytes.Length - 4; i++) { - if (bytes[i] == 0xFF && bytes[i + 1] == 0xFD && bytes[i + 2] == c0 && bytes[i + 3] == c1) + if ( + bytes[i] == 0xFF + && bytes[i + 1] == 0xFD + && bytes[i + 2] == c0 + && bytes[i + 3] == c1 + ) return i; } return -1; diff --git a/OpenNest.Tests/GravographIS/NestPolylineExtractorTests.cs b/OpenNest.Tests/GravographIS/NestPolylineExtractorTests.cs index 7c99643..cd86129 100644 --- a/OpenNest.Tests/GravographIS/NestPolylineExtractorTests.cs +++ b/OpenNest.Tests/GravographIS/NestPolylineExtractorTests.cs @@ -55,10 +55,16 @@ public class NestPolylineExtractorTests Assert.Equal(2, polylines.Count); Assert.Equal(LayerType.Scribe, polylines[0].Layer); - Assert.Equal(new[] { new Vector(0, 0), new Vector(1, 0), new Vector(2, 0) }, polylines[0].Points); + Assert.Equal( + new[] { new Vector(0, 0), new Vector(1, 0), new Vector(2, 0) }, + polylines[0].Points + ); Assert.Equal(LayerType.Cut, polylines[1].Layer); - Assert.Equal(new[] { new Vector(2, 0), new Vector(2, 1), new Vector(3, 1) }, polylines[1].Points); + Assert.Equal( + new[] { new Vector(2, 0), new Vector(2, 1), new Vector(3, 1) }, + polylines[1].Points + ); } [Fact]