mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-11 20:21:09 -05:00
fix(cutting): center straight leads on a corner a tab wraps
A tab entered at a fillet joint trims back around the corner, so the cut stops on one edge and starts on the other. Each straight lead took its own edge's normal and the pair sat 150 degrees apart, reading as one bent line through the corner. Both now take the bisector of the two ends' normals and form the same V they form on a straight edge, falling back to their own normals when either lead would not clear the part. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6ee81a3501
commit
52e421dcbe
3 files changed
+115
-2
No files matched your search
@@ -528,6 +528,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
|
||||
var turn = LeadTurn(contourType, winding);
|
||||
double leadInNormal;
|
||||
var configuredLeadIn = leadIn;
|
||||
// A planner-placed scrap pierce replaces the configured lead-in with a straight one.
|
||||
if (scrapPierce is { } pierce)
|
||||
leadIn = StraightLeadIn(point, pierce, out leadInNormal);
|
||||
@@ -542,7 +543,6 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
leadIn = PierceZone(memoContour, shape, point, leadIn, leadInNormal, turn);
|
||||
}
|
||||
}
|
||||
program.Codes.AddRange(leadIn.Generate(point, leadInNormal, turn));
|
||||
|
||||
var reindexedShape = shape.ReindexAt(point, entity);
|
||||
|
||||
@@ -555,6 +555,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
var leadOutNormal = normal;
|
||||
if (tabbed)
|
||||
{
|
||||
var first = reindexedShape.Entities.Count > 0 ? reindexedShape.Entities[0] : null;
|
||||
reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size);
|
||||
|
||||
// Leave from where the trimmed cut actually ends, on that entity's normal:
|
||||
@@ -564,6 +565,18 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
var last = reindexedShape.Entities[^1];
|
||||
leadOutPoint = EntityEndPoint(last);
|
||||
leadOutNormal = ComputeNormal(leadOutPoint, last, contourType, winding);
|
||||
|
||||
// Straight leads either side of a tab that wraps a corner share its bisector,
|
||||
// as they share the edge normal on a straight edge.
|
||||
if (ReferenceEquals(leadIn, configuredLeadIn) && leadIn is LineLeadIn lineIn
|
||||
&& leadOut is LineLeadOut lineOut && first is Line or Arc
|
||||
&& TabCornerNormal(shape, point, TravelTangent(first, point), leadInNormal, lineIn,
|
||||
leadOutPoint, TravelTangent(last, leadOutPoint), leadOutNormal, lineOut, winding,
|
||||
turn, Parameters.GetEffectivePierceClearance()) is { } tabNormal)
|
||||
{
|
||||
leadInNormal = tabNormal;
|
||||
leadOutNormal = tabNormal;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -572,6 +585,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
Parameters.GetEffectivePierceClearance(), out leadOutNormal);
|
||||
}
|
||||
|
||||
program.Codes.AddRange(leadIn.Generate(point, leadInNormal, turn));
|
||||
program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point));
|
||||
program.Codes.AddRange(leadOut.Generate(leadOutPoint, leadOutNormal, turn));
|
||||
}
|
||||
@@ -923,6 +937,46 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
return Angle.NormalizeRad(System.Math.Atan2(y, x));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The normal straight leads either side of a perimeter tab share when the tab wraps a
|
||||
/// convex corner: the bisector of the normals where the cut starts and where it stops, so
|
||||
/// the leads form the V they form on a straight edge, centered on the corner. Null when the
|
||||
/// tab lies on one edge or wraps a reflex turn, or when either lead would not keep
|
||||
/// <paramref name="clearance"/> from the part or would cross it.
|
||||
/// </summary>
|
||||
private static double? TabCornerNormal(
|
||||
Shape shape,
|
||||
Vector start,
|
||||
Vector tangentStart,
|
||||
double startNormal,
|
||||
LineLeadIn leadIn,
|
||||
Vector end,
|
||||
Vector tangentEnd,
|
||||
double endNormal,
|
||||
LineLeadOut leadOut,
|
||||
RotationType winding,
|
||||
RotationType turn,
|
||||
double clearance
|
||||
)
|
||||
{
|
||||
if (!IsFinite(tangentStart) || !IsFinite(tangentEnd)
|
||||
|| ClassifyTurn(tangentEnd, tangentStart, winding) != CornerKind.Convex)
|
||||
return null;
|
||||
|
||||
var x = System.Math.Cos(startNormal) + System.Math.Cos(endNormal);
|
||||
var y = System.Math.Sin(startNormal) + System.Math.Sin(endNormal);
|
||||
if (x * x + y * y <= Tolerance.Epsilon * Tolerance.Epsilon)
|
||||
return null;
|
||||
|
||||
var normal = Angle.NormalizeRad(System.Math.Atan2(y, x));
|
||||
var pierce = leadIn.GetPiercePoint(start, normal, turn);
|
||||
var leadOutEnd = ((LinearMove)leadOut.Generate(end, normal, turn)[0]).EndPoint;
|
||||
return IsClearStraightLead(shape, start, pierce, clearance)
|
||||
&& IsClearStraightLead(shape, end, leadOutEnd, clearance)
|
||||
? normal
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A straight lead from <paramref name="end"/> to the corner stays in the scrap:
|
||||
/// its free end keeps <paramref name="clearance"/> from the contour and the lead
|
||||
|
||||
@@ -84,6 +84,58 @@ public class TabbedLeadOutTests
|
||||
Assert.Equal(5, arc.Center.Y, 9);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void LineLeads_TabWrappingAFilletedCorner_ShareTheCornerBisector(bool reversed)
|
||||
{
|
||||
// Entered at the fillet joint the tab trims back around: on its own edge's normal the
|
||||
// lead-in and lead-out would sit 150 degrees apart, not the 60 degree V of a straight edge.
|
||||
const double fillet = 0.015;
|
||||
const double tab = 0.03;
|
||||
var clean = new Program(Mode.Absolute);
|
||||
if (reversed)
|
||||
{
|
||||
clean.MoveTo(fillet, 10); clean.ArcTo(0, 10 - fillet, fillet, 10 - fillet, RotationType.CCW);
|
||||
clean.LineTo(0, 0); clean.LineTo(10, 0); clean.LineTo(10, 10); clean.LineTo(fillet, 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
clean.MoveTo(0, 0); clean.LineTo(0, 10 - fillet);
|
||||
clean.ArcTo(fillet, 10, fillet, 10 - fillet, RotationType.CW);
|
||||
clean.LineTo(10, 10); clean.LineTo(10, 0); clean.LineTo(0, 0);
|
||||
}
|
||||
var part = new Part(new Drawing("tabbed-fillet", clean));
|
||||
var perimeter = LeadInTestHelpers.Profile(part).Perimeter;
|
||||
var entry = reversed ? new Vector(0, 10 - fillet) : new Vector(fillet, 10);
|
||||
var entity = perimeter.Entities.OfType<Line>().First(l => l.StartPoint.DistanceTo(entry) < 1e-9);
|
||||
var parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 60 },
|
||||
ExternalLeadOut = new LineLeadOut { Length = 0.1, ApproachAngle = 60 },
|
||||
TabsEnabled = true,
|
||||
TabConfig = new NormalTab { Size = tab },
|
||||
};
|
||||
|
||||
part.ApplySingleLeadIn(parameters, entry, entity, ContourType.External);
|
||||
|
||||
var geometry = part.Program.ToGeometry();
|
||||
var leadIn = Assert.IsType<Line>(Assert.Single(geometry.Where(e => e.Layer == SpecialLayers.Leadin)));
|
||||
var leadOut = Assert.IsType<Line>(Assert.Single(geometry.Where(e => e.Layer == SpecialLayers.Leadout)));
|
||||
LeadInTestHelpers.AssertPoint(entry, leadIn.EndPoint);
|
||||
Assert.Equal(tab, leadOut.StartPoint.DistanceTo(entry), 9);
|
||||
// The cut stops on the other edge, past the corner.
|
||||
Assert.Equal(0, reversed ? 10 - leadOut.StartPoint.Y : leadOut.StartPoint.X, 9);
|
||||
|
||||
const double bisector = 3 * System.Math.PI / 4;
|
||||
var spread = OpenNest.Math.Angle.ToRadians(30);
|
||||
LeadInTestHelpers.AssertPoint(entry + Polar(0.15, bisector - spread), leadIn.StartPoint);
|
||||
LeadInTestHelpers.AssertPoint(leadOut.StartPoint + Polar(0.1, bisector + spread), leadOut.EndPoint);
|
||||
}
|
||||
|
||||
private static Vector Polar(double length, double angle) =>
|
||||
new(length * System.Math.Cos(angle), length * System.Math.Sin(angle));
|
||||
|
||||
private static OwnedExecution Emit(Program clean, Vector approach, LeadOut leadOut, bool tabs)
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
|
||||
@@ -46,6 +46,12 @@ the corner along the last-cut edge, with the same clearance fallback to the last
|
||||
edge's normal and a bisector at reflex corners. A tabbed perimeter gets no run-out:
|
||||
every lead-out style leaves from where the trimmed cut actually ends, on that end's
|
||||
normal, so an arc lead-out starts on its own radius and the tab gap stays uncut.
|
||||
When the tab wraps a convex corner (the cut starts on one edge and stops on the other, as at a
|
||||
fillet joint), a straight lead-in and straight lead-out both take the bisector of those two
|
||||
ends' normals instead, so they form the same V centered on the corner that they form on a
|
||||
straight edge. Each lead must keep the pierce clearance and cross the part only where it meets
|
||||
the cut; otherwise both keep their own edge's normal. The straight in-line corner lead-in and
|
||||
curved leads are unchanged.
|
||||
`ContourCuttingStrategy.ResolveLeadIn`/`ResolveLeadOut` own these rules;
|
||||
program generation and the manual preview share them. Other lead-in styles are
|
||||
unchanged.
|
||||
@@ -63,7 +69,8 @@ edges at both ends, turn convexly through less than a half circle, and have a re
|
||||
a convex corner entry (`PreparedContours.FilletReach`). Ranking counts the joint on both sides of
|
||||
the corner it rounds, as it would a sharp corner. Longer curves, half-circle ends and inside
|
||||
(reflex) fillets keep their tangent joints, which rank level with edge midpoints. The lead-in at
|
||||
a fillet's joint is the normal one for its edge, since emission still sees a smooth joint there.
|
||||
a fillet's joint is the normal one for its edge, since emission still sees a smooth joint there,
|
||||
unless a tab wraps the fillet (see above).
|
||||
|
||||
## Curved leads
|
||||
|
||||
|
||||
Reference in new issue
Block a user