Compare commits
3 Commits
b15375cca5
...
3d4204db7b
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d4204db7b | |||
| 722f758e94 | |||
| 9b2322abe9 |
@@ -56,6 +56,60 @@ namespace OpenNest.Geometry
|
||||
return (new Vector(cx, cy), radius, MaxRadialDeviation(points, cx, cy, radius));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fits a circular arc constrained to be tangent to the given directions at both
|
||||
/// the first and last points. The center lies at the intersection of the normals
|
||||
/// at P1 and Pn, guaranteeing the arc departs P1 in the start direction and arrives
|
||||
/// at Pn in the end direction. Uses the radius from P1 (exact start tangent);
|
||||
/// deviation includes any endpoint gap at Pn.
|
||||
/// </summary>
|
||||
internal static (Vector center, double radius, double deviation) FitWithDualTangent(
|
||||
List<Vector> points, Vector startTangent, Vector endTangent)
|
||||
{
|
||||
if (points.Count < 3)
|
||||
return (Vector.Invalid, 0, double.MaxValue);
|
||||
|
||||
var p1 = points[0];
|
||||
var pn = points[^1];
|
||||
|
||||
var stLen = System.Math.Sqrt(startTangent.X * startTangent.X + startTangent.Y * startTangent.Y);
|
||||
var etLen = System.Math.Sqrt(endTangent.X * endTangent.X + endTangent.Y * endTangent.Y);
|
||||
if (stLen < 1e-10 || etLen < 1e-10)
|
||||
return (Vector.Invalid, 0, double.MaxValue);
|
||||
|
||||
// Normal to start tangent at P1 (perpendicular)
|
||||
var n1x = -startTangent.Y / stLen;
|
||||
var n1y = startTangent.X / stLen;
|
||||
|
||||
// Normal to end tangent at Pn
|
||||
var n2x = -endTangent.Y / etLen;
|
||||
var n2y = endTangent.X / etLen;
|
||||
|
||||
// Solve: P1 + t1*N1 = Pn + t2*N2
|
||||
var det = n1x * (-n2y) - (-n2x) * n1y;
|
||||
if (System.Math.Abs(det) < 1e-10)
|
||||
return (Vector.Invalid, 0, double.MaxValue);
|
||||
|
||||
var dx = pn.X - p1.X;
|
||||
var dy = pn.Y - p1.Y;
|
||||
var t1 = (dx * (-n2y) - (-n2x) * dy) / det;
|
||||
|
||||
var cx = p1.X + t1 * n1x;
|
||||
var cy = p1.Y + t1 * n1y;
|
||||
|
||||
// Use radius from P1 (guarantees exact start tangent and passes through P1)
|
||||
var r1 = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y));
|
||||
if (r1 < 1e-10)
|
||||
return (Vector.Invalid, 0, double.MaxValue);
|
||||
|
||||
// Measure endpoint gap at Pn
|
||||
var r2 = System.Math.Sqrt((cx - pn.X) * (cx - pn.X) + (cy - pn.Y) * (cy - pn.Y));
|
||||
var endpointDev = System.Math.Abs(r2 - r1);
|
||||
|
||||
var interiorDev = MaxRadialDeviation(points, cx, cy, r1);
|
||||
return (new Vector(cx, cy), r1, System.Math.Max(endpointDev, interiorDev));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the maximum radial deviation of interior points from a circle.
|
||||
/// </summary>
|
||||
|
||||
@@ -134,13 +134,11 @@ public class GeometrySimplifier
|
||||
|
||||
if (midpoints.Count < 4) return MirrorAxisResult.None;
|
||||
|
||||
// Centroid
|
||||
var cx = 0.0;
|
||||
var cy = 0.0;
|
||||
foreach (var p in midpoints) { cx += p.X; cy += p.Y; }
|
||||
cx /= midpoints.Count;
|
||||
cy /= midpoints.Count;
|
||||
var centroid = new Vector(cx, cy);
|
||||
var centroid = new Vector(
|
||||
midpoints.Average(p => p.X),
|
||||
midpoints.Average(p => p.Y));
|
||||
var cx = centroid.X;
|
||||
var cy = centroid.Y;
|
||||
|
||||
// Covariance matrix for PCA
|
||||
var cxx = 0.0;
|
||||
@@ -192,12 +190,25 @@ 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 Vector Normalize(Vector v)
|
||||
{
|
||||
var len = System.Math.Sqrt(v.X * v.X + v.Y * v.Y);
|
||||
return len < 1e-10 ? new Vector(1, 0) : new Vector(v.X / len, v.Y / len);
|
||||
}
|
||||
|
||||
private static double PerpendicularDistance(Vector point, Vector axisPoint, Vector axisDir)
|
||||
{
|
||||
var dx = point.X - axisPoint.X;
|
||||
var dy = point.Y - axisPoint.Y;
|
||||
var dot = dx * axisDir.X + dy * axisDir.Y;
|
||||
var px = dx - dot * axisDir.X;
|
||||
var py = dy - dot * axisDir.Y;
|
||||
return System.Math.Sqrt(px * px + py * py);
|
||||
}
|
||||
|
||||
private static double MirrorMatchScore(List<Vector> points, Vector axisPoint, Vector axisDir)
|
||||
{
|
||||
var matchTol = 0.1;
|
||||
@@ -206,14 +217,7 @@ public class GeometrySimplifier
|
||||
for (var i = 0; i < points.Count; i++)
|
||||
{
|
||||
var p = points[i];
|
||||
|
||||
// Distance from point to axis
|
||||
var dx = p.X - axisPoint.X;
|
||||
var dy = p.Y - axisPoint.Y;
|
||||
var dot = dx * axisDir.X + dy * axisDir.Y;
|
||||
var perpX = dx - dot * axisDir.X;
|
||||
var perpY = dy - dot * axisDir.Y;
|
||||
var dist = System.Math.Sqrt(perpX * perpX + perpY * perpY);
|
||||
var dist = PerpendicularDistance(p, axisPoint, axisDir);
|
||||
|
||||
// Points on the axis count as matched
|
||||
if (dist < matchTol)
|
||||
@@ -223,14 +227,12 @@ public class GeometrySimplifier
|
||||
}
|
||||
|
||||
// Reflect across axis and look for partner
|
||||
var mx = p.X - 2 * perpX;
|
||||
var my = p.Y - 2 * perpY;
|
||||
var reflected = new MirrorAxisResult(axisPoint, axisDir, 0).Reflect(p);
|
||||
|
||||
for (var j = 0; j < points.Count; j++)
|
||||
{
|
||||
if (i == j) continue;
|
||||
var d = System.Math.Sqrt((points[j].X - mx) * (points[j].X - mx) +
|
||||
(points[j].Y - my) * (points[j].Y - my));
|
||||
var d = reflected.DistanceTo(points[j]);
|
||||
if (d < matchTol)
|
||||
{
|
||||
matched++;
|
||||
@@ -259,14 +261,7 @@ public class GeometrySimplifier
|
||||
|
||||
var ci = candidates[i];
|
||||
var ciCenter = ci.BoundingBox.Center;
|
||||
|
||||
// Distance from candidate center to axis
|
||||
var dx = ciCenter.X - axis.Point.X;
|
||||
var dy = ciCenter.Y - axis.Point.Y;
|
||||
var dot = dx * axis.Direction.X + dy * axis.Direction.Y;
|
||||
var perpDist = System.Math.Sqrt((dx - dot * axis.Direction.X) * (dx - dot * axis.Direction.X) +
|
||||
(dy - dot * axis.Direction.Y) * (dy - dot * axis.Direction.Y));
|
||||
if (perpDist < 0.1) continue; // on the axis
|
||||
if (PerpendicularDistance(ciCenter, axis.Point, axis.Direction) < 0.1) continue; // on the axis
|
||||
|
||||
var mirrorCenter = axis.Reflect(ciCenter);
|
||||
|
||||
@@ -328,12 +323,8 @@ public class GeometrySimplifier
|
||||
var mirrorEp = axis.Reflect(ep);
|
||||
|
||||
// Mirroring reverses winding — swap start/end to preserve arc direction
|
||||
var mirrorStart = System.Math.Atan2(mirrorEp.Y - mirrorCenter.Y, mirrorEp.X - mirrorCenter.X);
|
||||
var mirrorEnd = System.Math.Atan2(mirrorSp.Y - mirrorCenter.Y, mirrorSp.X - mirrorCenter.X);
|
||||
|
||||
// Normalize to [0, 2pi)
|
||||
if (mirrorStart < 0) mirrorStart += Angle.TwoPI;
|
||||
if (mirrorEnd < 0) mirrorEnd += Angle.TwoPI;
|
||||
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;
|
||||
@@ -357,15 +348,16 @@ 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 = CreateArc(result.Center, result.Radius, result.Points, entities[j]),
|
||||
FittedArc = arc,
|
||||
MaxDeviation = result.Deviation,
|
||||
BoundingBox = result.Points.GetBoundingBox(),
|
||||
FirstPoint = result.Points[0],
|
||||
LastPoint = result.Points[^1],
|
||||
FirstPoint = arc.StartPoint(),
|
||||
LastPoint = arc.EndPoint(),
|
||||
});
|
||||
|
||||
j = result.EndIndex + 1;
|
||||
@@ -386,14 +378,16 @@ public class GeometrySimplifier
|
||||
? chainedTangent
|
||||
: new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
|
||||
|
||||
var (center, radius, dev) = TryFit(points, startTangent);
|
||||
var endTangent = GetExitDirection(entities[k]);
|
||||
var (center, radius, dev) = TryFit(points, startTangent, endTangent);
|
||||
if (!center.IsValid()) return null;
|
||||
|
||||
// Extend the arc as far as possible
|
||||
while (k + 1 <= runEnd)
|
||||
{
|
||||
var extPoints = CollectPoints(entities, start, k + 1);
|
||||
var (nc, nr, nd) = extPoints.Count >= 3 ? TryFit(extPoints, startTangent) : (Vector.Invalid, 0, 0d);
|
||||
var extEndTangent = GetExitDirection(entities[k + 1]);
|
||||
var (nc, nr, nd) = extPoints.Count >= 3 ? TryFit(extPoints, startTangent, extEndTangent) : (Vector.Invalid, 0, 0d);
|
||||
if (!nc.IsValid()) break;
|
||||
|
||||
k++;
|
||||
@@ -413,9 +407,23 @@ public class GeometrySimplifier
|
||||
return new ArcFitResult(center, radius, dev, points, k);
|
||||
}
|
||||
|
||||
private (Vector center, double radius, double deviation) TryFit(List<Vector> points, Vector startTangent)
|
||||
private (Vector center, double radius, double deviation) TryFit(List<Vector> points, Vector startTangent, Vector endTangent)
|
||||
{
|
||||
var (center, radius, dev) = FitWithStartTangent(points, startTangent);
|
||||
// Try dual-tangent fit first (matches direction at both endpoints)
|
||||
if (endTangent.IsValid())
|
||||
{
|
||||
var (dc, dr, dd) = ArcFit.FitWithDualTangent(points, startTangent, endTangent);
|
||||
if (dc.IsValid() && dd <= Tolerance)
|
||||
{
|
||||
var isRev = SumSignedAngles(dc, points) < 0;
|
||||
var aDev = MaxArcToSegmentDeviation(points, dc, dr, isRev);
|
||||
if (aDev <= Tolerance)
|
||||
return (dc, dr, System.Math.Max(dd, aDev));
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to start-tangent-only, then mirror axis
|
||||
var (center, radius, dev) = ArcFit.FitWithStartTangent(points, startTangent);
|
||||
if (!center.IsValid() || dev > Tolerance)
|
||||
(center, radius, dev) = FitMirrorAxis(points);
|
||||
if (!center.IsValid() || dev > Tolerance)
|
||||
@@ -430,16 +438,6 @@ public class GeometrySimplifier
|
||||
return (center, radius, System.Math.Max(dev, arcDev));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fits a circular arc constrained to be tangent to the given direction at the
|
||||
/// first point. The center lies at the intersection of the normal at P1 (perpendicular
|
||||
/// to the tangent) and the perpendicular bisector of the chord P1->Pn, guaranteeing
|
||||
/// the arc passes through both endpoints and departs P1 in the given direction.
|
||||
/// </summary>
|
||||
private static (Vector center, double radius, double deviation) FitWithStartTangent(
|
||||
List<Vector> points, Vector tangent) =>
|
||||
ArcFit.FitWithStartTangent(points, tangent);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the tangent direction at the last point of a fitted arc,
|
||||
/// used to chain tangent continuity to the next arc.
|
||||
@@ -447,15 +445,10 @@ public class GeometrySimplifier
|
||||
private static Vector ComputeEndTangent(Vector center, List<Vector> points)
|
||||
{
|
||||
var lastPt = points[^1];
|
||||
var totalAngle = SumSignedAngles(center, points);
|
||||
|
||||
var rx = lastPt.X - center.X;
|
||||
var ry = lastPt.Y - center.Y;
|
||||
|
||||
if (totalAngle >= 0)
|
||||
return new Vector(-ry, rx);
|
||||
else
|
||||
return new Vector(ry, -rx);
|
||||
var sign = SumSignedAngles(center, points) >= 0 ? 1 : -1;
|
||||
return new Vector(-sign * ry, sign * rx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -496,12 +489,12 @@ public class GeometrySimplifier
|
||||
var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord);
|
||||
|
||||
var dOpt = GoldenSectionMin(dInit - range, dInit + range,
|
||||
d => MaxRadialDeviation(points, mx + d * nx, my + d * ny,
|
||||
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);
|
||||
return (center, radius, MaxRadialDeviation(points, center.X, center.Y, radius));
|
||||
return (center, radius, ArcFit.MaxRadialDeviation(points, center.X, center.Y, radius));
|
||||
}
|
||||
|
||||
private static double GoldenSectionMin(double low, double high, Func<double, double> eval)
|
||||
@@ -554,20 +547,28 @@ public class GeometrySimplifier
|
||||
var firstPoint = points[0];
|
||||
var lastPoint = points[^1];
|
||||
|
||||
var startAngle = System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X);
|
||||
var endAngle = 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;
|
||||
|
||||
// Normalize to [0, 2pi)
|
||||
if (startAngle < 0) startAngle += Angle.TwoPI;
|
||||
if (endAngle < 0) endAngle += Angle.TwoPI;
|
||||
|
||||
var arc = new Arc(center, radius, startAngle, endAngle, isReversed);
|
||||
arc.Layer = sourceEntity.Layer;
|
||||
arc.Color = sourceEntity.Color;
|
||||
return arc;
|
||||
}
|
||||
|
||||
/// <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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Sums signed angular change traversing consecutive points around a center.
|
||||
/// Positive = CCW, negative = CW.
|
||||
@@ -587,12 +588,6 @@ public class GeometrySimplifier
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Max deviation of intermediate points (excluding endpoints) from a circle.
|
||||
/// </summary>
|
||||
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius) =>
|
||||
ArcFit.MaxRadialDeviation(points, cx, cy, radius);
|
||||
|
||||
/// <summary>
|
||||
/// Measures the maximum distance from sampled points along the fitted arc
|
||||
/// back to the original line segments. This catches cases where points lie
|
||||
|
||||
@@ -70,7 +70,7 @@ public sealed class CincinnatiFeatureWriter
|
||||
{
|
||||
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
|
||||
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
|
||||
writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
|
||||
writer.WriteLine($"G89 P{lib} ({speedClass} {cutDist})");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -108,7 +108,7 @@ public sealed class CincinnatiFeatureWriter
|
||||
sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
|
||||
|
||||
// Feedrate — etch always uses process feedrate
|
||||
var feedVar = ctx.IsEtch ? "#148" : GetFeedVariable(linear.Layer);
|
||||
var feedVar = ctx.IsEtch ? "#148" : GetLinearFeedVariable(linear.Layer);
|
||||
if (feedVar != lastFeedVar)
|
||||
{
|
||||
sb.Append($" F{feedVar}");
|
||||
@@ -138,11 +138,11 @@ public sealed class CincinnatiFeatureWriter
|
||||
var j = arc.CenterPoint.Y - currentPos.Y;
|
||||
sb.Append($" I{_fmt.FormatCoord(i)} J{_fmt.FormatCoord(j)}");
|
||||
|
||||
// Feedrate — etch always uses process feedrate, cut uses layer-based
|
||||
// Feedrate — etch always uses process feedrate, cut uses layer/radius-based
|
||||
var radius = currentPos.DistanceTo(arc.CenterPoint);
|
||||
var isFullCircle = IsFullCircle(currentPos, arc.EndPoint);
|
||||
var feedVar = ctx.IsEtch ? "#148"
|
||||
: isFullCircle ? "[#148*#128]"
|
||||
: GetFeedVariable(arc.Layer);
|
||||
: GetArcFeedrate(arc.Layer, radius, isFullCircle);
|
||||
if (feedVar != lastFeedVar)
|
||||
{
|
||||
sb.Append($" F{feedVar}");
|
||||
@@ -223,16 +223,45 @@ public sealed class CincinnatiFeatureWriter
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFeedVariable(LayerType layer)
|
||||
private static string GetLinearFeedVariable(LayerType layer)
|
||||
{
|
||||
return layer switch
|
||||
{
|
||||
LayerType.Leadin => "#126",
|
||||
LayerType.Cut => "#148",
|
||||
LayerType.Leadout => "#129",
|
||||
_ => "#148"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetArcFeedrate(LayerType layer, double radius, bool isFullCircle)
|
||||
{
|
||||
if (layer == LayerType.Leadin) return "#127";
|
||||
if (layer == LayerType.Leadout) return "#129";
|
||||
if (isFullCircle) return "[#148*#128]";
|
||||
return GetArcCutFeedrate(radius);
|
||||
}
|
||||
|
||||
private string GetArcCutFeedrate(double radius)
|
||||
{
|
||||
if (_config.ArcFeedrate == ArcFeedrateMode.None)
|
||||
return "#148";
|
||||
|
||||
// Find the smallest range that contains this radius
|
||||
ArcFeedrateRange best = null;
|
||||
foreach (var range in _config.ArcFeedrateRanges)
|
||||
{
|
||||
if (radius <= range.MaxRadius && (best == null || range.MaxRadius < best.MaxRadius))
|
||||
best = range;
|
||||
}
|
||||
|
||||
if (best == null)
|
||||
return "#148";
|
||||
|
||||
return _config.ArcFeedrate == ArcFeedrateMode.Variables
|
||||
? $"#{best.VariableNumber}"
|
||||
: $"[#148*{best.FeedratePercent.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)}]";
|
||||
}
|
||||
|
||||
private static bool IsFullCircle(Vector start, Vector end)
|
||||
{
|
||||
return Tolerance.IsEqualTo(start.X, end.X) && Tolerance.IsEqualTo(start.Y, end.Y);
|
||||
|
||||
@@ -269,12 +269,35 @@ namespace OpenNest.Posts.Cincinnati
|
||||
/// </summary>
|
||||
public double LeadInArcLine2FeedratePercent { get; set; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the feedrate percentage for lead-out moves.
|
||||
/// Default: 0.5 (50%)
|
||||
/// </summary>
|
||||
public double LeadOutFeedratePercent { get; set; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the feedrate multiplier for circular cuts.
|
||||
/// Default: 0.8 (80%)
|
||||
/// </summary>
|
||||
public double CircleFeedrateMultiplier { get; set; } = 0.8;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the arc feedrate calculation mode.
|
||||
/// Default: ArcFeedrateMode.None
|
||||
/// </summary>
|
||||
public ArcFeedrateMode ArcFeedrate { get; set; } = ArcFeedrateMode.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the radius-based arc feedrate ranges.
|
||||
/// Ranges are matched from smallest MaxRadius to largest.
|
||||
/// </summary>
|
||||
public List<ArcFeedrateRange> ArcFeedrateRanges { get; set; } = new()
|
||||
{
|
||||
new() { MaxRadius = 0.125, FeedratePercent = 0.25, VariableNumber = 123 },
|
||||
new() { MaxRadius = 0.750, FeedratePercent = 0.50, VariableNumber = 124 },
|
||||
new() { MaxRadius = 4.500, FeedratePercent = 0.80, VariableNumber = 125 }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the variable number for sheet width.
|
||||
/// Default: 110
|
||||
@@ -301,4 +324,34 @@ namespace OpenNest.Posts.Cincinnati
|
||||
public string Gas { get; set; } = "";
|
||||
public string Library { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how arc feedrates are calculated based on radius.
|
||||
/// </summary>
|
||||
public enum ArcFeedrateMode
|
||||
{
|
||||
/// <summary>No radius-based arc feedrate adjustment (only full circles use multiplier).</summary>
|
||||
None,
|
||||
|
||||
/// <summary>Inline percentage expressions: F [#148*pct] based on radius range.</summary>
|
||||
Percentages,
|
||||
|
||||
/// <summary>Radius-range-based variables: F #varNum based on radius range.</summary>
|
||||
Variables
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a radius range and its associated feedrate for arc moves.
|
||||
/// </summary>
|
||||
public class ArcFeedrateRange
|
||||
{
|
||||
/// <summary>Maximum radius for this range (inclusive).</summary>
|
||||
public double MaxRadius { get; set; }
|
||||
|
||||
/// <summary>Feedrate as a fraction of process feedrate (e.g. 0.25 = 25%).</summary>
|
||||
public double FeedratePercent { get; set; }
|
||||
|
||||
/// <summary>Variable number for Variables mode (e.g. 123).</summary>
|
||||
public int VariableNumber { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +112,9 @@ namespace OpenNest.Posts.Cincinnati
|
||||
var sheetIndex = i + 1;
|
||||
var subNumber = Config.SheetSubprogramStart + i;
|
||||
var cutLibrary = resolver.ResolveCutLibrary(plate.Material?.Name ?? "", plate.Thickness, gas);
|
||||
var isLastSheet = i == plates.Count - 1;
|
||||
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", sheetIndex, subNumber,
|
||||
cutLibrary, etchLibrary, partSubprograms);
|
||||
cutLibrary, etchLibrary, partSubprograms, isLastSheet);
|
||||
}
|
||||
|
||||
// Part sub-programs (if enabled)
|
||||
@@ -148,6 +149,18 @@ namespace OpenNest.Posts.Cincinnati
|
||||
vars.GetOrCreate("LeadInFeedrate", 126, $"[#148*{Config.LeadInFeedratePercent}]");
|
||||
vars.GetOrCreate("LeadInArcLine2Feedrate", 127, $"[#148*{Config.LeadInArcLine2FeedratePercent}]");
|
||||
vars.GetOrCreate("CircleFeedrate", 128, Config.CircleFeedrateMultiplier.ToString("0.#"));
|
||||
vars.GetOrCreate("LeadOutFeedrate", 129, $"[#148*{Config.LeadOutFeedratePercent}]");
|
||||
|
||||
if (Config.ArcFeedrate == ArcFeedrateMode.Variables)
|
||||
{
|
||||
foreach (var range in Config.ArcFeedrateRanges)
|
||||
{
|
||||
var name = $"ArcFeedR{range.MaxRadius.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture)}";
|
||||
vars.GetOrCreate(name, range.VariableNumber,
|
||||
$"[#148*{range.FeedratePercent.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)}]");
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,15 +37,21 @@ public sealed class CincinnatiPreambleWriter
|
||||
|
||||
w.WriteLine(CoordinateFormatter.Comment("MAIN PROGRAM"));
|
||||
|
||||
w.WriteLine(_config.PostedUnits == Units.Millimeters ? "G21" : "G20");
|
||||
w.WriteLine(_config.PostedUnits == Units.Millimeters ? "G21 G90" : "G20 G90");
|
||||
|
||||
if (_config.UseSmartRapids)
|
||||
w.WriteLine("G121 (SMART RAPIDS)");
|
||||
|
||||
w.WriteLine("M42");
|
||||
|
||||
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(initialLibrary))
|
||||
w.WriteLine($"G89 P {initialLibrary}");
|
||||
w.WriteLine($"G89 P{initialLibrary}");
|
||||
|
||||
w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)");
|
||||
|
||||
if (_config.PalletExchange == PalletMode.StartAndEnd)
|
||||
w.WriteLine("M50");
|
||||
|
||||
w.WriteLine("GOTO1 (GOTO SHEET NUMBER)");
|
||||
|
||||
for (var i = 1; i <= sheetCount; i++)
|
||||
|
||||
@@ -37,7 +37,8 @@ public sealed class CincinnatiSheetWriter
|
||||
/// </param>
|
||||
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
|
||||
string cutLibrary, string etchLibrary,
|
||||
Dictionary<(int, long), int> partSubprograms = null)
|
||||
Dictionary<(int, long), int> partSubprograms = null,
|
||||
bool isLastSheet = false)
|
||||
{
|
||||
if (plate.Parts.Count == 0)
|
||||
return;
|
||||
@@ -64,12 +65,12 @@ public sealed class CincinnatiSheetWriter
|
||||
w.WriteLine("N10000");
|
||||
w.WriteLine("G92 X#5021 Y#5022");
|
||||
if (!string.IsNullOrEmpty(cutLibrary))
|
||||
w.WriteLine($"G89 P {cutLibrary}");
|
||||
w.WriteLine($"G89 P{cutLibrary}");
|
||||
w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)");
|
||||
w.WriteLine("G90");
|
||||
w.WriteLine("M47");
|
||||
if (!string.IsNullOrEmpty(cutLibrary))
|
||||
w.WriteLine($"G89 P {cutLibrary}");
|
||||
w.WriteLine($"G89 P{cutLibrary}");
|
||||
w.WriteLine("GOTO1( Goto Feature )");
|
||||
|
||||
// 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last
|
||||
@@ -94,7 +95,9 @@ public sealed class CincinnatiSheetWriter
|
||||
// 5. Footer
|
||||
w.WriteLine("M42");
|
||||
w.WriteLine("G0 X0 Y0");
|
||||
if (_config.PalletExchange != PalletMode.None)
|
||||
var emitM50 = _config.PalletExchange == PalletMode.EndOfSheet
|
||||
|| (_config.PalletExchange == PalletMode.StartAndEnd && isLastSheet);
|
||||
if (emitM50)
|
||||
w.WriteLine($"N{sheetIndex + 1} M50");
|
||||
w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati;
|
||||
|
||||
@@ -105,11 +106,48 @@ public static class FeatureUtils
|
||||
}
|
||||
else if (code is ArcMove arc)
|
||||
{
|
||||
distance += currentPos.DistanceTo(arc.EndPoint);
|
||||
distance += ComputeArcLength(currentPos, arc);
|
||||
currentPos = arc.EndPoint;
|
||||
}
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the arc length from the current position through an arc move.
|
||||
/// Uses radius * sweep angle instead of chord length.
|
||||
/// </summary>
|
||||
public static double ComputeArcLength(Vector startPos, ArcMove arc)
|
||||
{
|
||||
var radius = startPos.DistanceTo(arc.CenterPoint);
|
||||
if (radius < Tolerance.Epsilon)
|
||||
return 0.0;
|
||||
|
||||
// Full circle: start ≈ end
|
||||
if (Tolerance.IsEqualTo(startPos.X, arc.EndPoint.X)
|
||||
&& Tolerance.IsEqualTo(startPos.Y, arc.EndPoint.Y))
|
||||
return 2.0 * System.Math.PI * radius;
|
||||
|
||||
var startAngle = System.Math.Atan2(
|
||||
startPos.Y - arc.CenterPoint.Y,
|
||||
startPos.X - arc.CenterPoint.X);
|
||||
var endAngle = System.Math.Atan2(
|
||||
arc.EndPoint.Y - arc.CenterPoint.Y,
|
||||
arc.EndPoint.X - arc.CenterPoint.X);
|
||||
|
||||
double sweep;
|
||||
if (arc.Rotation == RotationType.CW)
|
||||
{
|
||||
sweep = startAngle - endAngle;
|
||||
if (sweep <= 0) sweep += 2.0 * System.Math.PI;
|
||||
}
|
||||
else
|
||||
{
|
||||
sweep = endAngle - startAngle;
|
||||
if (sweep <= 0) sweep += 2.0 * System.Math.PI;
|
||||
}
|
||||
|
||||
return radius * sweep;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ public class CincinnatiFeatureWriterTests
|
||||
ctx.SheetDiagonal = 30.0;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("G89 P MILD10", output);
|
||||
Assert.Contains("G89 PMILD10", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -470,6 +470,123 @@ public class CincinnatiFeatureWriterTests
|
||||
Assert.True(m131Idx < m47Idx, "M131 should come before M47");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeadoutFeedrate_UsesVariable129()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(1.0, 1.0),
|
||||
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadout }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#129", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcLeadin_UsesVariable127()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(12.0, 20.0), new Vector(11.0, 20.0), RotationType.CCW) { Layer = LayerType.Leadin }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#127", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcFeedrate_VariablesMode_UsesRadiusRangeVariable()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
config.ArcFeedrate = ArcFeedrateMode.Variables;
|
||||
// Arc with radius 0.5 (center at 10.5, 20; start at 10, 20) → R=0.5 → #124 (R ≤ 0.750)
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(11.0, 20.0), new Vector(10.5, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#124", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcFeedrate_PercentagesMode_UsesInlineExpression()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
config.ArcFeedrate = ArcFeedrateMode.Percentages;
|
||||
// Arc with radius 0.1 (≤ 0.125) → 25%
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F[#148*0.25]", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcFeedrate_LargeRadius_UsesProcessFeedrate()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
config.ArcFeedrate = ArcFeedrateMode.Variables;
|
||||
// Arc with radius 10 (> 4.500) → falls through to #148
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(0.0, 0.0),
|
||||
new ArcMove(new Vector(20.0, 0.0), new Vector(10.0, 0.0), RotationType.CCW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#148", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcFeedrate_NoneMode_UsesProcessFeedrateForNonCircle()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
config.ArcFeedrate = ArcFeedrateMode.None;
|
||||
// Small radius arc but mode is None → process feedrate
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#148", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void G89_PAdjacentToFilename()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
var ctx = SimpleContext();
|
||||
ctx.LibraryFile = "MS135N2.lib";
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
// P must be directly adjacent to filename, no space
|
||||
Assert.Contains("G89 PMS135N2.lib", output);
|
||||
Assert.DoesNotContain("G89 P MS135N2.lib", output);
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string text, string pattern)
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
@@ -43,6 +43,74 @@ public class CincinnatiPostProcessorTests
|
||||
Assert.Contains("M99", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_EmitsLeadOutVariable()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
Assert.Contains("#129=", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_WithArcFeedrateVariables_EmitsRangeVariables()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PostedAccuracy = 4,
|
||||
ArcFeedrate = ArcFeedrateMode.Variables
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
Assert.Contains("#123=", output);
|
||||
Assert.Contains("#124=", output);
|
||||
Assert.Contains("#125=", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_WithArcFeedrateNone_OmitsRangeVariables()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PostedAccuracy = 4,
|
||||
ArcFeedrate = ArcFeedrateMode.None
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
Assert.DoesNotContain("#123=", output);
|
||||
Assert.DoesNotContain("#124=", output);
|
||||
Assert.DoesNotContain("#125=", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_EmitsG90InPreamble()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
Assert.Contains("G20 G90", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_ImplementsIPostProcessor()
|
||||
{
|
||||
|
||||
@@ -24,9 +24,9 @@ public class CincinnatiPreambleWriterTests
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("( NEST TestNest )", output);
|
||||
Assert.Contains("( CONFIGURATION - CL940 )", output);
|
||||
Assert.Contains("G20", output);
|
||||
Assert.Contains("G20 G90", output);
|
||||
Assert.Contains("M42", output);
|
||||
Assert.Contains("G89 P MS135N2PANEL.lib", output);
|
||||
Assert.Contains("G89 PMS135N2PANEL.lib", output);
|
||||
Assert.Contains("M98 P100 (Variable Declaration)", output);
|
||||
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
|
||||
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
|
||||
@@ -44,7 +44,72 @@ public class CincinnatiPreambleWriterTests
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.Contains("G21", sb.ToString());
|
||||
Assert.Contains("G21 G90", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsG90WithUnits()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PostedUnits = Units.Inches };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.Contains("G20 G90", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsG121_WhenSmartRapidsEnabled()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { UseSmartRapids = true };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.Contains("G121 (SMART RAPIDS)", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_OmitsG121_WhenSmartRapidsDisabled()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { UseSmartRapids = false };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.DoesNotContain("G121", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsM50_WhenStartAndEnd()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PalletExchange = PalletMode.StartAndEnd };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.Contains("M50", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_OmitsM50_WhenEndOfSheet()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PalletExchange = PalletMode.EndOfSheet };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.DoesNotContain("M50", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -32,7 +32,7 @@ public class CincinnatiSheetWriterTests
|
||||
Assert.Contains("#110=", output);
|
||||
Assert.Contains("#111=", output);
|
||||
Assert.Contains("G92 X#5021 Y#5022", output);
|
||||
Assert.Contains("G89 P MS135N2PANEL.lib", output);
|
||||
Assert.Contains("G89 PMS135N2PANEL.lib", output);
|
||||
Assert.Contains("M99", output);
|
||||
}
|
||||
|
||||
@@ -137,9 +137,110 @@ public class CincinnatiSheetWriterTests
|
||||
Assert.True(g85Idx < g84Idx, "G85 (etch) should come before G84 (cut)");
|
||||
|
||||
// Etch uses etch library
|
||||
Assert.Contains("G89 P EtchN2.lib", output);
|
||||
Assert.Contains("G89 PEtchN2.lib", output);
|
||||
// Cut uses cut library
|
||||
Assert.Contains("G89 P MS250O2.lib", output);
|
||||
Assert.Contains("G89 PMS250O2.lib", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_StartAndEnd_NoM50OnNonLastSheet()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PalletExchange = PalletMode.StartAndEnd,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: false);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.DoesNotContain("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_StartAndEnd_M50OnLastSheet()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PalletExchange = PalletMode.StartAndEnd,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: true);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_EndOfSheet_AlwaysEmitsM50()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PalletExchange = PalletMode.EndOfSheet,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: false);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeArcLength_FullCircle_Returns2PiR()
|
||||
{
|
||||
var start = new Vector(10.0, 20.0);
|
||||
var arc = new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW);
|
||||
var length = FeatureUtils.ComputeArcLength(start, arc);
|
||||
|
||||
// Radius = 5, full circle = 2 * PI * 5 ≈ 31.416
|
||||
Assert.Equal(2.0 * System.Math.PI * 5.0, length, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeArcLength_Semicircle_ReturnsPiR()
|
||||
{
|
||||
// Semicircle from (0,0) to (10,0) with center at (5,0), CCW → goes through (5,5)
|
||||
var start = new Vector(0.0, 0.0);
|
||||
var arc = new ArcMove(new Vector(10.0, 0.0), new Vector(5.0, 0.0), RotationType.CCW);
|
||||
var length = FeatureUtils.ComputeArcLength(start, arc);
|
||||
|
||||
// Radius = 5, semicircle = PI * 5 ≈ 15.708
|
||||
Assert.Equal(System.Math.PI * 5.0, length, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeCutDistance_WithArcs_UsesArcLengthNotChord()
|
||||
{
|
||||
// Full circle: chord = 0 but arc length = 2πr
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var distance = FeatureUtils.ComputeCutDistance(codes);
|
||||
|
||||
// Full circle with R=5 → 2πr ≈ 31.416
|
||||
Assert.True(distance > 30.0, $"Expected arc length > 30 but got {distance}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
@@ -127,4 +130,52 @@ public class GeometrySimplifierTests
|
||||
// Index 6 is the fitted arc replacing the second run
|
||||
Assert.IsType<Arc>(result.Entities[6]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_DynaPanDxf_NoGapsAfterSimplification()
|
||||
{
|
||||
var path = @"C:\Users\aisaacs\Desktop\Sullys Q29 DXFs\SULLYS-031 Dyna Pan.dxf";
|
||||
if (!File.Exists(path))
|
||||
return; // skip if file not available
|
||||
|
||||
var importer = new DxfImporter();
|
||||
var result = importer.Import(path);
|
||||
var shapes = ShapeBuilder.GetShapes(result.Entities);
|
||||
|
||||
var simplifier = new GeometrySimplifier { Tolerance = 0.004 };
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var candidates = simplifier.Analyze(shape);
|
||||
if (candidates.Count == 0) continue;
|
||||
|
||||
var simplified = simplifier.Apply(shape, candidates);
|
||||
|
||||
// Check for gaps between consecutive entities
|
||||
for (var i = 0; i < simplified.Entities.Count - 1; i++)
|
||||
{
|
||||
var current = simplified.Entities[i];
|
||||
var next = simplified.Entities[i + 1];
|
||||
|
||||
var currentEnd = current switch
|
||||
{
|
||||
Line l => l.EndPoint,
|
||||
Arc a => a.EndPoint(),
|
||||
_ => Vector.Invalid
|
||||
};
|
||||
var nextStart = next switch
|
||||
{
|
||||
Line l => l.StartPoint,
|
||||
Arc a => a.StartPoint(),
|
||||
_ => Vector.Invalid
|
||||
};
|
||||
|
||||
if (!currentEnd.IsValid() || !nextStart.IsValid()) continue;
|
||||
|
||||
var gap = currentEnd.DistanceTo(nextStart);
|
||||
Assert.True(gap < 0.005,
|
||||
$"Gap of {gap:F4} between entities {i} ({current.GetType().Name}) and {i + 1} ({next.GetType().Name})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,13 +494,30 @@ namespace OpenNest.Forms
|
||||
|
||||
using var dlg = new SaveFileDialog
|
||||
{
|
||||
Filter = "DXF Files|*.dxf",
|
||||
Filter = "DXF 2018 (*.dxf)|*.dxf|" +
|
||||
"DXF 2013 (*.dxf)|*.dxf|" +
|
||||
"DXF 2010 (*.dxf)|*.dxf|" +
|
||||
"DXF 2007 (*.dxf)|*.dxf|" +
|
||||
"DXF 2004 (*.dxf)|*.dxf|" +
|
||||
"DXF 2000 (*.dxf)|*.dxf|" +
|
||||
"DXF R14 (*.dxf)|*.dxf",
|
||||
FileName = Path.ChangeExtension(item.Name, ".dxf"),
|
||||
};
|
||||
|
||||
if (dlg.ShowDialog() != DialogResult.OK) return;
|
||||
|
||||
var doc = new ACadSharp.CadDocument();
|
||||
var version = dlg.FilterIndex switch
|
||||
{
|
||||
2 => ACadSharp.ACadVersion.AC1027,
|
||||
3 => ACadSharp.ACadVersion.AC1024,
|
||||
4 => ACadSharp.ACadVersion.AC1021,
|
||||
5 => ACadSharp.ACadVersion.AC1018,
|
||||
6 => ACadSharp.ACadVersion.AC1015,
|
||||
7 => ACadSharp.ACadVersion.AC1014,
|
||||
_ => ACadSharp.ACadVersion.AC1032,
|
||||
};
|
||||
|
||||
var doc = new ACadSharp.CadDocument(version);
|
||||
foreach (var entity in item.Entities)
|
||||
{
|
||||
switch (entity)
|
||||
|
||||
Reference in New Issue
Block a user