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
{
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;
+22 -6
View File
@@ -14,7 +14,9 @@ namespace OpenNest.Geometry
/// the arc passes through both endpoints and departs P1 in the given direction.
/// </summary>
internal static (Vector center, double radius, double deviation) FitWithStartTangent(
List<Vector> points, Vector tangent)
List<Vector> 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.
/// </summary>
internal static (Vector center, double radius, double deviation) FitThroughEndpointsWithTangents(
List<Vector> points, Vector startTangent, Vector endTangent)
internal static (
Vector center,
double radius,
double deviation
) FitThroughEndpointsWithTangents(
List<Vector> 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);
}
/// <summary>
/// Computes the maximum radial deviation of interior points from a circle.
/// </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;
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;
}
+209 -85
View File
@@ -15,8 +15,10 @@ public class ArcCandidate
public double MaxDeviation { get; set; }
public Box BoundingBox { get; set; }
public bool IsSelected { get; set; } = true;
/// <summary>First point of the original line segments this candidate covers.</summary>
public Vector FirstPoint { get; set; }
/// <summary>Last point of the original line segments this candidate covers.</summary>
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<ArcCandidate> 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<Entity>();
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
/// </summary>
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>();
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<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 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<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;
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<Vector> points, TangentEstimate start, TangentEstimate end)
List<Vector> 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.
/// </summary>
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)
{
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.
/// </summary>
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);
reversed.Reverse();
@@ -483,7 +539,11 @@ public class GeometrySimplifier
private const double NeighborEdgeFactor = 3.0;
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())
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<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)
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
/// <summary>
/// Returns the entry direction (tangent at start point) of an entity.
/// </summary>
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,
};
/// <summary>
/// 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<Vector> points, Entity sourceEntity)
private static Arc CreateArc(
Vector center,
double radius,
List<Vector> 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
/// <summary>
/// Returns the exit direction (tangent at endpoint) of an entity.
/// </summary>
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,
};
/// <summary>
/// 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.
/// </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 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;
}
@@ -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" : "");
}
/// <summary>
@@ -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",
};
/// <summary>
/// Returns the cut config a polyline of the given layer should use, or null
@@ -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<GravographISPostConfig>(json, JsonOptions)
Config =
JsonSerializer.Deserialize<GravographISPostConfig>(json, JsonOptions)
?? new GravographISPostConfig();
}
else
@@ -103,14 +104,16 @@ namespace OpenNest.Posts.GravographIS
/// </summary>
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 cut = new List<IReadOnlyList<Vector>>();
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
+299 -82
View File
@@ -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
/// </summary>
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
// 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
);
}
/// <summary>
@@ -128,8 +238,10 @@ namespace OpenNest.Posts.GravographIS
/// </summary>
public void Write(IReadOnlyList<GravographPass> 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<Vector> poly,
ref bool firstPolyline, ref int headX, ref int headY,
int envelopeXSteps, int envelopeYSteps, int polyIndex)
private void WritePolyline(
Stream output,
IReadOnlyList<Vector> 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."
);
}
}
}
@@ -59,7 +59,8 @@ namespace OpenNest.Posts.GravographIS
/// </summary>
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>();
@@ -91,7 +92,8 @@ namespace OpenNest.Posts.GravographIS
private void ExtractPart(Part part, List<LayeredPolyline> 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<LayeredPolyline> sink, ref List<Vector> current,
ref LayerType currentLayer, LayerType moveLayer, Vector seed)
private static void StartOrSplit(
List<LayeredPolyline> sink,
ref List<Vector> current,
ref LayerType currentLayer,
LayerType moveLayer,
Vector seed
)
{
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)
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<Vector> sink)
private static void TessellateArc(
Vector start,
ArcMove arc,
Vector offset,
double chordTol,
List<Vector> 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++)
@@ -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);
@@ -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)
@@ -7,19 +7,21 @@ namespace OpenNest.Tests.GravographIS;
public class GravographISPostProcessorTests
{
private static LayeredPolyline Poly(LayerType layer, params Vector[] pts)
=> new LayeredPolyline(new List<Vector>(pts), layer);
private static LayeredPolyline Poly(LayerType layer, params Vector[] pts) =>
new LayeredPolyline(new List<Vector>(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);
@@ -191,20 +191,37 @@ public class GravographISWriterTests
[Fact]
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 passes = new List<GravographPass>
{
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<GravographPass>
{
new GravographPass { 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" },
new GravographPass
{
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();
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<GravographPass>
{
new GravographPass { 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 },
new GravographPass
{
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();
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;
@@ -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]