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)
|
private static List<ContourEntry> ResolveLeadInPoints(List<Shape> cutouts, Vector startPoint)
|
||||||
{
|
{
|
||||||
var entries = new ContourEntry[cutouts.Count];
|
var entries = new ContourEntry[cutouts.Count];
|
||||||
|
|||||||
@@ -72,6 +72,18 @@ namespace OpenNest
|
|||||||
UpdateBounds();
|
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()
|
public void RemoveLeadIns()
|
||||||
{
|
{
|
||||||
var rotation = preLeadInRotation;
|
var rotation = preLeadInRotation;
|
||||||
|
|||||||
@@ -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