style: apply CSharpier formatting to files merged from arc-tangency branch
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user