feat: add ApplySingle for exact-click single-contour lead-in placement
Adds ApplySingle to ContourCuttingStrategy that applies lead-in/out to only the contour containing the clicked entity, emitting other contours as raw geometry. Also adds ApplySingleLeadIn wrapper to Part. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,126 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
};
|
||||
}
|
||||
|
||||
public CuttingResult ApplySingle(Program partProgram, Vector point, Entity entity, ContourType contourType)
|
||||
{
|
||||
var entities = partProgram.ToGeometry();
|
||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||
|
||||
var scribeEntities = entities.FindAll(e => e.Layer == SpecialLayers.Scribe);
|
||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
|
||||
|
||||
var profile = new ShapeProfile(entities);
|
||||
|
||||
var result = new Program(Mode.Absolute);
|
||||
|
||||
EmitScribeContours(result, scribeEntities);
|
||||
|
||||
// Find the target shape that contains the clicked entity
|
||||
var (targetShape, matchedEntity) = FindTargetShape(profile, point, entity);
|
||||
|
||||
// Emit cutouts — only the target gets lead-in/out
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
{
|
||||
if (cutout == targetShape)
|
||||
{
|
||||
var ct = DetectContourType(cutout);
|
||||
EmitContour(result, cutout, point, matchedEntity, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitRawContour(result, cutout);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit perimeter
|
||||
if (profile.Perimeter == targetShape)
|
||||
{
|
||||
EmitContour(result, profile.Perimeter, point, matchedEntity, ContourType.External);
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitRawContour(result, profile.Perimeter);
|
||||
}
|
||||
|
||||
result.Mode = Mode.Incremental;
|
||||
|
||||
return new CuttingResult
|
||||
{
|
||||
Program = result,
|
||||
LastCutPoint = point
|
||||
};
|
||||
}
|
||||
|
||||
private static (Shape Shape, Entity Entity) FindTargetShape(ShapeProfile profile, Vector point, Entity clickedEntity)
|
||||
{
|
||||
var matched = FindMatchingEntity(profile.Perimeter, clickedEntity);
|
||||
if (matched != null)
|
||||
return (profile.Perimeter, matched);
|
||||
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
{
|
||||
matched = FindMatchingEntity(cutout, clickedEntity);
|
||||
if (matched != null)
|
||||
return (cutout, matched);
|
||||
}
|
||||
|
||||
// Fallback: closest shape, use closest point to find entity
|
||||
var best = profile.Perimeter;
|
||||
var bestPt = profile.Perimeter.ClosestPointTo(point, out var bestEntity);
|
||||
var bestDist = bestPt.DistanceTo(point);
|
||||
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
{
|
||||
var pt = cutout.ClosestPointTo(point, out var cutoutEntity);
|
||||
var dist = pt.DistanceTo(point);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
best = cutout;
|
||||
bestEntity = cutoutEntity;
|
||||
bestDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
return (best, bestEntity);
|
||||
}
|
||||
|
||||
private static Entity FindMatchingEntity(Shape shape, Entity clickedEntity)
|
||||
{
|
||||
foreach (var shapeEntity in shape.Entities)
|
||||
{
|
||||
if (shapeEntity.GetType() != clickedEntity.GetType())
|
||||
continue;
|
||||
|
||||
if (shapeEntity is Line sLine && clickedEntity is Line cLine)
|
||||
{
|
||||
if (sLine.StartPoint.DistanceTo(cLine.StartPoint) < Math.Tolerance.Epsilon
|
||||
&& sLine.EndPoint.DistanceTo(cLine.EndPoint) < Math.Tolerance.Epsilon)
|
||||
return shapeEntity;
|
||||
}
|
||||
else if (shapeEntity is Arc sArc && clickedEntity is Arc cArc)
|
||||
{
|
||||
if (System.Math.Abs(sArc.Radius - cArc.Radius) < Math.Tolerance.Epsilon
|
||||
&& sArc.Center.DistanceTo(cArc.Center) < Math.Tolerance.Epsilon)
|
||||
return shapeEntity;
|
||||
}
|
||||
else if (shapeEntity is Circle sCircle && clickedEntity is Circle cCircle)
|
||||
{
|
||||
if (System.Math.Abs(sCircle.Radius - cCircle.Radius) < Math.Tolerance.Epsilon
|
||||
&& sCircle.Center.DistanceTo(cCircle.Center) < Math.Tolerance.Epsilon)
|
||||
return shapeEntity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void EmitRawContour(Program program, Shape shape)
|
||||
{
|
||||
var startPoint = GetShapeStartPoint(shape);
|
||||
program.Codes.Add(new RapidMove(startPoint));
|
||||
program.Codes.AddRange(ConvertShapeToMoves(shape, startPoint));
|
||||
}
|
||||
|
||||
private static List<ContourEntry> ResolveLeadInPoints(List<Shape> cutouts, Vector startPoint)
|
||||
{
|
||||
var entries = new ContourEntry[cutouts.Count];
|
||||
|
||||
@@ -72,6 +72,18 @@ namespace OpenNest
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
public void ApplySingleLeadIn(CNC.CuttingStrategy.CuttingParameters parameters,
|
||||
Geometry.Vector point, Geometry.Entity entity, CNC.CuttingStrategy.ContourType contourType)
|
||||
{
|
||||
preLeadInRotation = Rotation;
|
||||
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy { Parameters = parameters };
|
||||
var result = strategy.ApplySingle(Program, point, entity, contourType);
|
||||
Program = result.Program;
|
||||
CuttingParameters = parameters;
|
||||
HasManualLeadIns = true;
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
public void RemoveLeadIns()
|
||||
{
|
||||
var rotation = preLeadInRotation;
|
||||
|
||||
130
OpenNest.Tests/CuttingStrategy/ApplySingleTests.cs
Normal file
130
OpenNest.Tests/CuttingStrategy/ApplySingleTests.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingStrategy;
|
||||
|
||||
public class ApplySingleTests
|
||||
{
|
||||
private static Program MakeSquareProgram()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return pgm;
|
||||
}
|
||||
|
||||
private static Program MakeSquareWithHoleProgram()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 20)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(20, 20)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(20, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new RapidMove(new Vector(12, 10)));
|
||||
pgm.Codes.Add(new ArcMove(new Vector(12, 10), new Vector(10, 10), RotationType.CW));
|
||||
return pgm;
|
||||
}
|
||||
|
||||
private static List<ICode> GetLeadInCodes(Program program)
|
||||
{
|
||||
var result = new List<ICode>();
|
||||
foreach (var code in program.Codes)
|
||||
{
|
||||
if (code is LinearMove lm && lm.Layer == LayerType.Leadin)
|
||||
result.Add(lm);
|
||||
else if (code is ArcMove am && am.Layer == LayerType.Leadin)
|
||||
result.Add(am);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingle_ExternalContour_PlacesLeadInAtExactPoint()
|
||||
{
|
||||
var pgm = MakeSquareProgram();
|
||||
var strategy = new ContourCuttingStrategy
|
||||
{
|
||||
Parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
|
||||
}
|
||||
};
|
||||
|
||||
var clickPoint = new Vector(5, 0);
|
||||
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
|
||||
var result = strategy.ApplySingle(pgm, clickPoint, entity, ContourType.External);
|
||||
|
||||
var hasLeadin = result.Program.Codes.OfType<LinearMove>().Any(m => m.Layer == LayerType.Leadin);
|
||||
Assert.True(hasLeadin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingle_ContourStartsAtClickPoint()
|
||||
{
|
||||
var pgm = MakeSquareProgram();
|
||||
var strategy = new ContourCuttingStrategy
|
||||
{
|
||||
Parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
|
||||
}
|
||||
};
|
||||
|
||||
var clickPoint = new Vector(5, 0);
|
||||
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
|
||||
var result = strategy.ApplySingle(pgm, clickPoint, entity, ContourType.External);
|
||||
|
||||
// Convert back to absolute to check positions
|
||||
result.Program.Mode = Mode.Absolute;
|
||||
|
||||
var firstLinear = result.Program.Codes.OfType<LinearMove>()
|
||||
.First(m => m.Layer == LayerType.Leadin);
|
||||
Assert.Equal(clickPoint.X, firstLinear.EndPoint.X, 4);
|
||||
Assert.Equal(clickPoint.Y, firstLinear.EndPoint.Y, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingle_ProgramModeIsIncremental()
|
||||
{
|
||||
var pgm = MakeSquareProgram();
|
||||
var strategy = new ContourCuttingStrategy
|
||||
{
|
||||
Parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
|
||||
}
|
||||
};
|
||||
|
||||
var clickPoint = new Vector(5, 0);
|
||||
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
|
||||
var result = strategy.ApplySingle(pgm, clickPoint, entity, ContourType.External);
|
||||
|
||||
Assert.Equal(Mode.Incremental, result.Program.Mode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingle_OnlyTargetContourGetsLeadIn()
|
||||
{
|
||||
var pgm = MakeSquareWithHoleProgram();
|
||||
var strategy = new ContourCuttingStrategy
|
||||
{
|
||||
Parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }
|
||||
}
|
||||
};
|
||||
|
||||
var clickPoint = new Vector(10, 0);
|
||||
var entity = new Line(new Vector(20, 0), new Vector(0, 0));
|
||||
var result = strategy.ApplySingle(pgm, clickPoint, entity, ContourType.External);
|
||||
|
||||
var leadinMoves = GetLeadInCodes(result.Program);
|
||||
Assert.Single(leadinMoves);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user