mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-06 18:05:44 -05:00
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>
156 lines
6.9 KiB
C#
156 lines
6.9 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Tests.CuttingPlanning;
|
|
|
|
namespace OpenNest.Tests.CuttingStrategy;
|
|
|
|
/// <summary>
|
|
/// A tab trims the perimeter short of its entry, so the lead-out leaves from where the
|
|
/// trimmed cut actually ends. Generating it from the nominal entry emitted an arc whose
|
|
/// start was off its own radius.
|
|
/// </summary>
|
|
public class TabbedLeadOutTests
|
|
{
|
|
private const double TabSize = 0.2;
|
|
private const double LeadRadius = 0.2;
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void ArcLeadOut_LeavesTangentlyFromTheTrimmedCutEnd(bool reversed)
|
|
{
|
|
var execution = Emit(ExplicitContourTests.Square(reversed), new Vector(-2, 5),
|
|
new ArcLeadOut { Radius = LeadRadius }, tabs: true);
|
|
var (cuts, leadout) = Split(execution);
|
|
|
|
var end = cuts[^1].End;
|
|
Assert.Equal(0, end.X, 9);
|
|
Assert.Equal(TabSize, System.Math.Abs(end.Y - 5), 9);
|
|
Assert.Equal(end, leadout.Start!.Value);
|
|
var arc = Assert.IsType<Arc>(leadout.Curve.ToEntity());
|
|
Assert.Equal(LeadRadius, arc.Radius, 9);
|
|
// Centerd on the actual end's outward normal, so it touches the edge only where it starts.
|
|
Assert.Equal(-LeadRadius, arc.Center.X, 9);
|
|
Assert.Equal(end.Y, arc.Center.Y, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void ArcLeadOut_OnCurvedPerimeter_IsTangentAtTheTrimmedEnd()
|
|
{
|
|
// Half disc: the entry is on the arc, so the trimmed end's radial differs from the entry's.
|
|
var clean = new Program();
|
|
clean.MoveTo(0, 5); clean.LineTo(10, 5); clean.ArcTo(0, 5, 5, 5, RotationType.CW);
|
|
var execution = Emit(clean, new Vector(5, -2), new ArcLeadOut { Radius = LeadRadius }, tabs: true);
|
|
var (cuts, leadout) = Split(execution);
|
|
|
|
var center = new Vector(5, 5);
|
|
var end = cuts[^1].End;
|
|
Assert.Equal(5, end.DistanceTo(center), 9);
|
|
Assert.Equal(TabSize, end.DistanceTo(new Vector(5, 0)), 9);
|
|
Assert.Equal(end, leadout.Start!.Value);
|
|
var arc = Assert.IsType<Arc>(leadout.Curve.ToEntity());
|
|
Assert.Equal(LeadRadius, arc.Radius, 9);
|
|
// Externally tangent to the perimeter at the actual end: center on its radial line.
|
|
Assert.Equal(5 + LeadRadius, arc.Center.DistanceTo(center), 9);
|
|
var radial = end - center;
|
|
var offset = arc.Center - end;
|
|
Assert.Equal(0, radial.X * offset.Y - radial.Y * offset.X, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void LineLeadOut_RunsAlongTheEndNormalFromTheTrimmedCutEnd()
|
|
{
|
|
var execution = Emit(ExplicitContourTests.Square(false), new Vector(-2, 5),
|
|
new LineLeadOut { Length = 0.3 }, tabs: true);
|
|
var (cuts, leadout) = Split(execution);
|
|
|
|
Assert.Equal(cuts[^1].End, leadout.Start!.Value);
|
|
Assert.Equal(-0.3, leadout.End.X, 9);
|
|
Assert.Equal(cuts[^1].End.Y, leadout.End.Y, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void UntabbedArcLeadOut_StillLeavesFromTheEntry()
|
|
{
|
|
var execution = Emit(ExplicitContourTests.Square(false), new Vector(-2, 5),
|
|
new ArcLeadOut { Radius = LeadRadius }, tabs: false);
|
|
var (cuts, leadout) = Split(execution);
|
|
|
|
Assert.Equal(new Vector(0, 5), cuts[^1].End);
|
|
var arc = Assert.IsType<Arc>(leadout.Curve.ToEntity());
|
|
Assert.Equal(-LeadRadius, arc.Center.X, 9);
|
|
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();
|
|
parameters.TabsEnabled = tabs;
|
|
parameters.TabConfig = new NormalTab { Size = TabSize };
|
|
parameters.ExternalLeadOut = leadOut;
|
|
var emitted = new ContourCuttingStrategy { Parameters = parameters }.Apply(clean, approach);
|
|
return ExecutionMotionReader.Read(emitted.Program, Vector.Zero, null, default);
|
|
}
|
|
|
|
private static (ExecutionMotion[] Cuts, ExecutionMotion Leadout) Split(OwnedExecution execution)
|
|
{
|
|
var cuts = execution.Motions.Where(m => m.Layer == LayerType.Display && !m.Rapid).ToArray();
|
|
Assert.NotEmpty(cuts);
|
|
return (cuts, Assert.Single(execution.Motions.Where(m => m.Layer == LayerType.Leadout)));
|
|
}
|
|
}
|