style: apply CSharpier formatting to files merged from arc-tangency branch

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