Compare commits
4 Commits
5bd4c89999
...
9f76659d5d
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f76659d5d | |||
| a8341e9e99 | |||
| fb067187b4 | |||
| 5c66fb3b72 |
@@ -8,113 +8,101 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public CuttingParameters Parameters { get; set; }
|
||||
|
||||
private record ContourEntry(Shape Shape, Vector Point, Entity Entity);
|
||||
|
||||
public CuttingResult Apply(Program partProgram, Vector approachPoint)
|
||||
{
|
||||
var exitPoint = approachPoint;
|
||||
var entities = partProgram.ToGeometry();
|
||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||
|
||||
// Separate scribe/etch entities — they don't get lead-ins or kerf
|
||||
var scribeEntities = entities.FindAll(e => e.Layer == SpecialLayers.Scribe);
|
||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
|
||||
|
||||
var profile = new ShapeProfile(entities);
|
||||
|
||||
// Find closest point on perimeter from exit point
|
||||
var perimeterPoint = profile.Perimeter.ClosestPointTo(exitPoint, out var perimeterEntity);
|
||||
|
||||
// Chain cutouts by nearest-neighbor from perimeter point, then reverse
|
||||
// so farthest cutouts are cut first, nearest-to-perimeter cut last
|
||||
// Forward pass: sequence cutouts nearest-neighbor from perimeter
|
||||
var perimeterPoint = profile.Perimeter.ClosestPointTo(approachPoint, out _);
|
||||
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
|
||||
orderedCutouts.Reverse();
|
||||
|
||||
// Build output program: scribe first, cutouts second, perimeter last
|
||||
// Backward pass: walk from perimeter back through cutting order
|
||||
// so each lead-in faces the next cutout to be cut, not the previous
|
||||
var cutoutEntries = ResolveLeadInPoints(orderedCutouts, perimeterPoint);
|
||||
|
||||
var result = new Program(Mode.Absolute);
|
||||
var currentPoint = exitPoint;
|
||||
|
||||
// Emit scribe/etch contours first (no lead-ins, no kerf)
|
||||
if (scribeEntities.Count > 0)
|
||||
{
|
||||
var scribeShapes = ShapeBuilder.GetShapes(scribeEntities);
|
||||
foreach (var scribe in scribeShapes)
|
||||
{
|
||||
var startPt = GetShapeStartPoint(scribe);
|
||||
result.Codes.Add(new RapidMove(startPt));
|
||||
result.Codes.AddRange(ConvertShapeToMoves(scribe, startPt, LayerType.Scribe));
|
||||
currentPoint = startPt;
|
||||
}
|
||||
}
|
||||
EmitScribeContours(result, scribeEntities);
|
||||
|
||||
foreach (var cutout in orderedCutouts)
|
||||
{
|
||||
var contourType = DetectContourType(cutout);
|
||||
var closestPt = cutout.ClosestPointTo(currentPoint, out var entity);
|
||||
var normal = ComputeNormal(closestPt, entity, contourType);
|
||||
var winding = DetermineWinding(cutout);
|
||||
|
||||
var leadIn = SelectLeadIn(contourType);
|
||||
var leadOut = SelectLeadOut(contourType);
|
||||
|
||||
if (contourType == ContourType.ArcCircle && entity is Circle circle)
|
||||
leadIn = ClampLeadInForCircle(leadIn, circle, closestPt, normal);
|
||||
|
||||
result.Codes.AddRange(leadIn.Generate(closestPt, normal, winding));
|
||||
var reindexed = cutout.ReindexAt(closestPt, entity);
|
||||
|
||||
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
||||
{
|
||||
var trimmed = TrimShapeForTab(reindexed, closestPt, Parameters.TabConfig.Size);
|
||||
result.Codes.AddRange(ConvertShapeToMoves(trimmed, closestPt));
|
||||
result.Codes.AddRange(leadOut.Generate(closestPt, normal, winding));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
|
||||
result.Codes.AddRange(leadOut.Generate(closestPt, normal, winding));
|
||||
}
|
||||
|
||||
currentPoint = closestPt;
|
||||
}
|
||||
|
||||
var lastCutPoint = exitPoint;
|
||||
foreach (var entry in cutoutEntries)
|
||||
EmitContour(result, entry.Shape, entry.Point, entry.Entity);
|
||||
|
||||
// Perimeter last
|
||||
{
|
||||
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
||||
lastCutPoint = perimeterPt;
|
||||
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
||||
var winding = DetermineWinding(profile.Perimeter);
|
||||
var lastRefPoint = cutoutEntries.Count > 0 ? cutoutEntries[cutoutEntries.Count - 1].Point : approachPoint;
|
||||
var perimeterPt = profile.Perimeter.ClosestPointTo(lastRefPoint, out var perimeterEntity);
|
||||
EmitContour(result, profile.Perimeter, perimeterPt, perimeterEntity, ContourType.External);
|
||||
|
||||
var leadIn = SelectLeadIn(ContourType.External);
|
||||
var leadOut = SelectLeadOut(ContourType.External);
|
||||
|
||||
result.Codes.AddRange(leadIn.Generate(perimeterPt, normal, winding));
|
||||
var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
|
||||
|
||||
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
||||
{
|
||||
var trimmed = TrimShapeForTab(reindexed, perimeterPt, Parameters.TabConfig.Size);
|
||||
result.Codes.AddRange(ConvertShapeToMoves(trimmed, perimeterPt));
|
||||
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
|
||||
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to incremental mode to match the convention used by
|
||||
// the rest of the system (rendering, bounding box, drag, etc.).
|
||||
result.Mode = Mode.Incremental;
|
||||
|
||||
return new CuttingResult
|
||||
{
|
||||
Program = result,
|
||||
LastCutPoint = lastCutPoint
|
||||
LastCutPoint = perimeterPt
|
||||
};
|
||||
}
|
||||
|
||||
private static List<ContourEntry> ResolveLeadInPoints(List<Shape> cutouts, Vector startPoint)
|
||||
{
|
||||
var entries = new ContourEntry[cutouts.Count];
|
||||
var currentPoint = startPoint;
|
||||
|
||||
// Walk backward through cutting order (from perimeter outward)
|
||||
// so each cutout's lead-in point faces the next cutout to be cut
|
||||
for (var i = cutouts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var closestPt = cutouts[i].ClosestPointTo(currentPoint, out var entity);
|
||||
entries[i] = new ContourEntry(cutouts[i], closestPt, entity);
|
||||
currentPoint = closestPt;
|
||||
}
|
||||
|
||||
return new List<ContourEntry>(entries);
|
||||
}
|
||||
|
||||
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
|
||||
{
|
||||
var contourType = forceType ?? DetectContourType(shape);
|
||||
var normal = ComputeNormal(point, entity, contourType);
|
||||
var winding = DetermineWinding(shape);
|
||||
|
||||
var leadIn = SelectLeadIn(contourType);
|
||||
var leadOut = SelectLeadOut(contourType);
|
||||
|
||||
if (contourType == ContourType.ArcCircle && entity is Circle circle)
|
||||
leadIn = ClampLeadInForCircle(leadIn, circle, point, normal);
|
||||
|
||||
program.Codes.AddRange(leadIn.Generate(point, normal, winding));
|
||||
|
||||
var reindexed = shape.ReindexAt(point, entity);
|
||||
|
||||
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
||||
reindexed = TrimShapeForTab(reindexed, point, Parameters.TabConfig.Size);
|
||||
|
||||
program.Codes.AddRange(ConvertShapeToMoves(reindexed, point));
|
||||
program.Codes.AddRange(leadOut.Generate(point, normal, winding));
|
||||
}
|
||||
|
||||
private void EmitScribeContours(Program program, List<Entity> scribeEntities)
|
||||
{
|
||||
if (scribeEntities.Count == 0) return;
|
||||
|
||||
var shapes = ShapeBuilder.GetShapes(scribeEntities);
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var startPt = GetShapeStartPoint(shape);
|
||||
program.Codes.Add(new RapidMove(startPt));
|
||||
program.Codes.AddRange(ConvertShapeToMoves(shape, startPt, LayerType.Scribe));
|
||||
}
|
||||
}
|
||||
|
||||
private List<Shape> SequenceCutouts(List<Shape> cutouts, Vector startPoint)
|
||||
{
|
||||
var remaining = new List<Shape>(cutouts);
|
||||
|
||||
@@ -155,6 +155,17 @@ namespace OpenNest.Geometry
|
||||
Center.Y + Radius * System.Math.Sin(EndAngle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mid point of the arc (point at the angle midway between start and end).
|
||||
/// </summary>
|
||||
public Vector MidPoint()
|
||||
{
|
||||
var midAngle = StartAngle + (IsReversed ? -SweepAngle() / 2 : SweepAngle() / 2);
|
||||
return new Vector(
|
||||
Center.X + Radius * System.Math.Cos(midAngle),
|
||||
Center.Y + Radius * System.Math.Sin(midAngle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the arc at the given point, returning two sub-arcs.
|
||||
/// Either half may be null if the split point coincides with an endpoint.
|
||||
|
||||
@@ -70,6 +70,26 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
w.WriteLine($"M99 (END OF {drawingName})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the program has no leading rapid, inserts a synthetic rapid at the
|
||||
/// last motion endpoint (the contour return point). This ensures the feature
|
||||
/// writer knows the true pierce location and preserves the first contour segment.
|
||||
/// </summary>
|
||||
internal static void EnsureLeadingRapid(Program pgm)
|
||||
{
|
||||
if (pgm.Codes.Count == 0 || pgm.Codes[0] is RapidMove)
|
||||
return;
|
||||
|
||||
for (var i = pgm.Codes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (pgm.Codes[i] is Motion lastMotion)
|
||||
{
|
||||
pgm.Codes.Insert(0, new RapidMove(lastMotion.EndPoint));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a sub-program key for matching parts to their sub-programs.
|
||||
/// </summary>
|
||||
@@ -99,9 +119,17 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
mapping[key] = subNum;
|
||||
|
||||
var pgm = part.Program.Clone() as Program;
|
||||
pgm.Mode = Mode.Absolute;
|
||||
var bbox = pgm.BoundingBox();
|
||||
pgm.Offset(-bbox.Location.X, -bbox.Location.Y);
|
||||
|
||||
// If the program has no leading rapid, the feature writer
|
||||
// will use the first motion endpoint as the pierce point,
|
||||
// losing the first contour segment. Insert a synthetic rapid
|
||||
// at the contour's return point (last motion endpoint) so
|
||||
// the full contour is preserved.
|
||||
EnsureLeadingRapid(pgm);
|
||||
|
||||
entries.Add((subNum, part.BaseDrawing.Name, pgm));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,8 +69,31 @@ public static class FeatureUtils
|
||||
/// Splits a part's program into features by rapids, classifies each as etch or cut,
|
||||
/// and orders etch features before cut features.
|
||||
/// </summary>
|
||||
public static List<(List<ICode> codes, bool isEtch)> SplitAndClassify(Part part) =>
|
||||
ClassifyAndOrder(SplitByRapids(part.Program.Codes));
|
||||
public static List<(List<ICode> codes, bool isEtch)> SplitAndClassify(Part part)
|
||||
{
|
||||
part.Program.Mode = Mode.Absolute;
|
||||
var codes = part.Program.Codes;
|
||||
|
||||
// If no leading rapid, the first contour segment would be lost because
|
||||
// the feature writer pierces at the first motion endpoint. Insert a
|
||||
// synthetic rapid at the contour's return point to preserve closure.
|
||||
if (codes.Count > 0 && codes[0] is not RapidMove)
|
||||
{
|
||||
for (var i = codes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (codes[i] is Motion lastMotion)
|
||||
{
|
||||
var withRapid = new List<ICode>(codes.Count + 1);
|
||||
withRapid.Add(new RapidMove(lastMotion.EndPoint));
|
||||
withRapid.AddRange(codes);
|
||||
codes = withRapid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ClassifyAndOrder(SplitByRapids(codes));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if any non-rapid move in the feature has LayerType.Scribe.
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class MaterialLibraryResolver
|
||||
System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance &&
|
||||
string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return entry?.Library ?? "";
|
||||
return EnsureLibExtension(entry?.Library ?? "");
|
||||
}
|
||||
|
||||
public string ResolveEtchLibrary(string gas)
|
||||
@@ -32,7 +32,18 @@ public sealed class MaterialLibraryResolver
|
||||
var entry = _etchLibraries.FirstOrDefault(e =>
|
||||
string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return entry?.Library ?? "";
|
||||
return EnsureLibExtension(entry?.Library ?? "");
|
||||
}
|
||||
|
||||
private static string EnsureLibExtension(string library)
|
||||
{
|
||||
if (string.IsNullOrEmpty(library))
|
||||
return library;
|
||||
|
||||
if (!library.EndsWith(".lib", StringComparison.OrdinalIgnoreCase))
|
||||
return library + ".lib";
|
||||
|
||||
return library;
|
||||
}
|
||||
|
||||
public static string ResolveGas(Nest nest, CincinnatiPostConfig config)
|
||||
|
||||
@@ -388,6 +388,57 @@ public class CincinnatiPostProcessorTests
|
||||
Assert.Equal(300, deserialized.PartSubprogramStart);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_ClosedContourWithoutRapid_OutputsAllSegments()
|
||||
{
|
||||
// Reproduce bug: a closed contour with no leading rapid loses its
|
||||
// first segment in the CNC output because the feature writer uses
|
||||
// the first LinearMove endpoint as the pierce point.
|
||||
var pgm = new Program(Mode.Incremental);
|
||||
pgm.Codes.Add(new LinearMove(0, 2)); // (0,0) → (0,2)
|
||||
pgm.Codes.Add(new LinearMove(2, 0)); // (0,2) → (2,2)
|
||||
pgm.Codes.Add(new LinearMove(0, -2)); // (2,2) → (2,0)
|
||||
pgm.Codes.Add(new LinearMove(-2, 0)); // (2,0) → (0,0)
|
||||
|
||||
var drawing = new Drawing("ClosedSquare", pgm);
|
||||
var nest = new Nest("TestClosure");
|
||||
nest.Drawings.Add(drawing);
|
||||
var plate = new Plate(24, 24);
|
||||
plate.Parts.Add(new Part(drawing, new Vector(1, 1)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
UsePartSubprograms = true,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
|
||||
// The subprogram should contain all 4 segments of the square.
|
||||
// Without the fix, the first segment (0,0)→(0,2) was lost
|
||||
// because the feature writer pierced at (0,2) making it zero-length.
|
||||
var lines = output.Split('\n').Select(l => l.Trim()).ToArray();
|
||||
|
||||
// Count G1 moves in the subprogram (should be 4 for a square)
|
||||
var subStart = Array.FindIndex(lines, l => l.StartsWith(":200") || l.StartsWith(":300"));
|
||||
Assert.True(subStart >= 0, "Expected a part subprogram");
|
||||
|
||||
var g1Count = 0;
|
||||
for (var i = subStart; i < lines.Length; i++)
|
||||
{
|
||||
if (lines[i].StartsWith("M99"))
|
||||
break;
|
||||
if (lines[i].Contains("G1 "))
|
||||
g1Count++;
|
||||
}
|
||||
|
||||
Assert.Equal(4, g1Count);
|
||||
}
|
||||
|
||||
private static Nest CreateTestNest()
|
||||
{
|
||||
var nest = new Nest("TestNest");
|
||||
|
||||
@@ -14,6 +14,10 @@ namespace OpenNest.Actions
|
||||
[DisplayName("Place Lead-in")]
|
||||
public class ActionLeadIn : Action
|
||||
{
|
||||
private enum SnapType { None, Endpoint, Midpoint }
|
||||
|
||||
private const double SnapCapturePixels = 10.0;
|
||||
|
||||
private LayoutPart selectedLayoutPart;
|
||||
private Part selectedPart;
|
||||
private ShapeProfile profile;
|
||||
@@ -23,6 +27,7 @@ namespace OpenNest.Actions
|
||||
private ContourType snapContourType;
|
||||
private double snapNormal;
|
||||
private bool hasSnap;
|
||||
private SnapType activeSnapType;
|
||||
private ShapeInfo hoveredContour;
|
||||
private ContextMenuStrip contextMenu;
|
||||
private static readonly Brush grayOverlay = new SolidBrush(Color.FromArgb(160, 180, 180, 180));
|
||||
@@ -57,6 +62,7 @@ namespace OpenNest.Actions
|
||||
profile = null;
|
||||
contours = null;
|
||||
hasSnap = false;
|
||||
activeSnapType = SnapType.None;
|
||||
hoveredContour = null;
|
||||
plateView.Invalidate();
|
||||
}
|
||||
@@ -81,6 +87,7 @@ namespace OpenNest.Actions
|
||||
// Find closest contour and point
|
||||
var bestDist = double.MaxValue;
|
||||
hasSnap = false;
|
||||
activeSnapType = SnapType.None;
|
||||
hoveredContour = null;
|
||||
|
||||
foreach (var info in contours)
|
||||
@@ -100,6 +107,10 @@ namespace OpenNest.Actions
|
||||
}
|
||||
}
|
||||
|
||||
// Check endpoint/midpoint snaps on the hovered contour
|
||||
if (hoveredContour != null)
|
||||
TrySnapToEntityPoints(localPt);
|
||||
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
@@ -142,33 +153,39 @@ namespace OpenNest.Actions
|
||||
{
|
||||
var g = e.Graphics;
|
||||
|
||||
// Gray overlay on all parts except the selected one
|
||||
DrawOverlay(g);
|
||||
DrawHoveredContour(g);
|
||||
DrawLeadInPreview(g);
|
||||
}
|
||||
|
||||
private void DrawOverlay(Graphics g)
|
||||
{
|
||||
foreach (var lp in plateView.LayoutParts)
|
||||
{
|
||||
if (lp == selectedLayoutPart)
|
||||
continue;
|
||||
|
||||
if (lp.Path != null)
|
||||
if (lp != selectedLayoutPart && lp.Path != null)
|
||||
g.FillPath(grayOverlay, lp.Path);
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight the hovered contour
|
||||
if (hoveredContour != null && selectedPart != null)
|
||||
{
|
||||
using var contourPath = hoveredContour.Shape.GetGraphicsPath();
|
||||
private void DrawHoveredContour(Graphics g)
|
||||
{
|
||||
if (hoveredContour == null || selectedPart == null)
|
||||
return;
|
||||
|
||||
// Translate from local part space to world space, then apply view transform
|
||||
using var contourMatrix = new Matrix();
|
||||
contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y);
|
||||
contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append);
|
||||
contourPath.Transform(contourMatrix);
|
||||
using var contourPath = hoveredContour.Shape.GetGraphicsPath();
|
||||
using var contourMatrix = new Matrix();
|
||||
contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y);
|
||||
contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append);
|
||||
contourPath.Transform(contourMatrix);
|
||||
|
||||
var prevSmooth = g.SmoothingMode;
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
g.DrawPath(highlightPen, contourPath);
|
||||
g.SmoothingMode = prevSmooth;
|
||||
}
|
||||
var prevSmooth = g.SmoothingMode;
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
g.DrawPath(highlightPen, contourPath);
|
||||
g.SmoothingMode = prevSmooth;
|
||||
}
|
||||
|
||||
private void DrawLeadInPreview(Graphics g)
|
||||
{
|
||||
if (!hasSnap || selectedPart == null)
|
||||
return;
|
||||
|
||||
@@ -176,67 +193,113 @@ namespace OpenNest.Actions
|
||||
if (parameters == null)
|
||||
return;
|
||||
|
||||
// Transform snap point from local part space to world space
|
||||
var worldSnap = TransformToWorld(snapPoint);
|
||||
|
||||
// Get the appropriate lead-in for this contour type
|
||||
var leadIn = SelectLeadIn(parameters, snapContourType);
|
||||
if (leadIn == null)
|
||||
return;
|
||||
|
||||
// Clamp lead-in for circle contours so it stays inside the hole
|
||||
if (snapContourType == ContourType.ArcCircle && snapEntity is Circle snapCircle
|
||||
&& parameters.PierceClearance > 0)
|
||||
{
|
||||
var pierceCheck = leadIn.GetPiercePoint(snapPoint, snapNormal);
|
||||
var distFromCenter = pierceCheck.DistanceTo(snapCircle.Center);
|
||||
var maxRadius = snapCircle.Radius - parameters.PierceClearance;
|
||||
if (maxRadius > 0 && distFromCenter > maxRadius)
|
||||
{
|
||||
var currentDist = snapPoint.DistanceTo(pierceCheck);
|
||||
if (currentDist > Tolerance.Epsilon)
|
||||
{
|
||||
var dx = (pierceCheck.X - snapPoint.X) / currentDist;
|
||||
var dy = (pierceCheck.Y - snapPoint.Y) / currentDist;
|
||||
var vx = snapPoint.X - snapCircle.Center.X;
|
||||
var vy = snapPoint.Y - snapCircle.Center.Y;
|
||||
var b = 2.0 * (vx * dx + vy * dy);
|
||||
var c = vx * vx + vy * vy - maxRadius * maxRadius;
|
||||
var disc = b * b - 4.0 * c;
|
||||
if (disc >= 0)
|
||||
{
|
||||
var t = (-b + System.Math.Sqrt(disc)) / 2.0;
|
||||
if (t > 0 && t < currentDist)
|
||||
leadIn = leadIn.Scale(t / currentDist);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
leadIn = ClampLeadInForCircle(leadIn, parameters);
|
||||
|
||||
// Get the pierce point (in local space)
|
||||
var piercePoint = leadIn.GetPiercePoint(snapPoint, snapNormal);
|
||||
var worldPierce = TransformToWorld(piercePoint);
|
||||
var pt1 = plateView.PointWorldToGraph(TransformToWorld(piercePoint));
|
||||
var pt2 = plateView.PointWorldToGraph(TransformToWorld(snapPoint));
|
||||
|
||||
var oldSmooth = g.SmoothingMode;
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
|
||||
// Draw the lead-in preview as a line from pierce point to contour point
|
||||
var pt1 = plateView.PointWorldToGraph(worldPierce);
|
||||
var pt2 = plateView.PointWorldToGraph(worldSnap);
|
||||
|
||||
using var previewPen = new Pen(Color.Magenta, 2.0f / plateView.ViewScale);
|
||||
g.DrawLine(previewPen, pt1, pt2);
|
||||
|
||||
// Draw a small circle at the pierce point
|
||||
var radius = 3.0f / plateView.ViewScale;
|
||||
g.FillEllipse(Brushes.Magenta, pt1.X - radius, pt1.Y - radius, radius * 2, radius * 2);
|
||||
|
||||
// Draw a small circle at the contour start point
|
||||
g.FillEllipse(Brushes.Lime, pt2.X - radius, pt2.Y - radius, radius * 2, radius * 2);
|
||||
if (activeSnapType != SnapType.None)
|
||||
DrawSnapMarker(g, pt2, activeSnapType);
|
||||
else
|
||||
g.FillEllipse(Brushes.Lime, pt2.X - radius, pt2.Y - radius, radius * 2, radius * 2);
|
||||
|
||||
g.SmoothingMode = oldSmooth;
|
||||
}
|
||||
|
||||
private LeadIn ClampLeadInForCircle(LeadIn leadIn, CuttingParameters parameters)
|
||||
{
|
||||
if (snapContourType != ContourType.ArcCircle
|
||||
|| !(snapEntity is Circle snapCircle)
|
||||
|| parameters.PierceClearance <= 0)
|
||||
return leadIn;
|
||||
|
||||
var pierceCheck = leadIn.GetPiercePoint(snapPoint, snapNormal);
|
||||
var maxRadius = snapCircle.Radius - parameters.PierceClearance;
|
||||
|
||||
if (maxRadius <= 0 || pierceCheck.DistanceTo(snapCircle.Center) <= maxRadius)
|
||||
return leadIn;
|
||||
|
||||
var currentDist = snapPoint.DistanceTo(pierceCheck);
|
||||
if (currentDist <= Tolerance.Epsilon)
|
||||
return leadIn;
|
||||
|
||||
var dx = (pierceCheck.X - snapPoint.X) / currentDist;
|
||||
var dy = (pierceCheck.Y - snapPoint.Y) / currentDist;
|
||||
var vx = snapPoint.X - snapCircle.Center.X;
|
||||
var vy = snapPoint.Y - snapCircle.Center.Y;
|
||||
var b = 2.0 * (vx * dx + vy * dy);
|
||||
var c = vx * vx + vy * vy - maxRadius * maxRadius;
|
||||
var disc = b * b - 4.0 * c;
|
||||
|
||||
if (disc < 0)
|
||||
return leadIn;
|
||||
|
||||
var t = (-b + System.Math.Sqrt(disc)) / 2.0;
|
||||
return (t > 0 && t < currentDist) ? leadIn.Scale(t / currentDist) : leadIn;
|
||||
}
|
||||
|
||||
private void TrySnapToEntityPoints(Vector localPt)
|
||||
{
|
||||
var captureRadius = SnapCapturePixels / plateView.ViewScale;
|
||||
var bestDist = captureRadius;
|
||||
var bestPoint = default(Vector);
|
||||
var bestEntity = default(Entity);
|
||||
var bestType = SnapType.None;
|
||||
|
||||
foreach (var entity in hoveredContour.Shape.Entities)
|
||||
{
|
||||
switch (entity)
|
||||
{
|
||||
case Line line:
|
||||
TryCandidate(line.StartPoint, line, SnapType.Endpoint);
|
||||
TryCandidate(line.EndPoint, line, SnapType.Endpoint);
|
||||
TryCandidate(line.MidPoint, line, SnapType.Midpoint);
|
||||
break;
|
||||
case Arc arc:
|
||||
TryCandidate(arc.StartPoint(), arc, SnapType.Endpoint);
|
||||
TryCandidate(arc.EndPoint(), arc, SnapType.Endpoint);
|
||||
TryCandidate(arc.MidPoint(), arc, SnapType.Midpoint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestType != SnapType.None)
|
||||
{
|
||||
snapPoint = bestPoint;
|
||||
snapEntity = bestEntity;
|
||||
snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType);
|
||||
activeSnapType = bestType;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
void TryCandidate(Vector pt, Entity ent, SnapType type)
|
||||
{
|
||||
var dist = pt.DistanceTo(localPt);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestPoint = pt;
|
||||
bestEntity = ent;
|
||||
bestType = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectPartAtCursor()
|
||||
{
|
||||
var layoutPart = plateView.GetPartAtPoint(plateView.CurrentPoint);
|
||||
@@ -342,6 +405,7 @@ namespace OpenNest.Actions
|
||||
profile = null;
|
||||
contours = null;
|
||||
hasSnap = false;
|
||||
activeSnapType = SnapType.None;
|
||||
hoveredContour = null;
|
||||
plateView.Invalidate();
|
||||
}
|
||||
@@ -365,6 +429,35 @@ namespace OpenNest.Actions
|
||||
contextMenu.Show(plateView, location);
|
||||
}
|
||||
|
||||
private void DrawSnapMarker(Graphics g, PointF pt, SnapType type)
|
||||
{
|
||||
var size = 5f;
|
||||
|
||||
if (type == SnapType.Endpoint)
|
||||
{
|
||||
// Diamond
|
||||
var points = new[]
|
||||
{
|
||||
new PointF(pt.X, pt.Y - size),
|
||||
new PointF(pt.X + size, pt.Y),
|
||||
new PointF(pt.X, pt.Y + size),
|
||||
new PointF(pt.X - size, pt.Y)
|
||||
};
|
||||
g.FillPolygon(Brushes.Red, points);
|
||||
}
|
||||
else if (type == SnapType.Midpoint)
|
||||
{
|
||||
// Triangle
|
||||
var points = new[]
|
||||
{
|
||||
new PointF(pt.X, pt.Y - size),
|
||||
new PointF(pt.X + size, pt.Y + size),
|
||||
new PointF(pt.X - size, pt.Y + size)
|
||||
};
|
||||
g.FillPolygon(Brushes.Red, points);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector TransformToWorld(Vector localPt)
|
||||
{
|
||||
// The contours are already in rotated local space (we rotated the program
|
||||
|
||||
@@ -432,7 +432,6 @@ namespace OpenNest.Forms
|
||||
var rapid = (RapidMove)pgm[0];
|
||||
drawing.Source.Offset = rapid.EndPoint;
|
||||
pgm.Offset(-rapid.EndPoint);
|
||||
pgm.Codes.RemoveAt(0);
|
||||
}
|
||||
|
||||
drawing.Program = pgm;
|
||||
|
||||
@@ -362,7 +362,6 @@ namespace OpenNest.Forms
|
||||
var rapid = (RapidMove)pgm[0];
|
||||
originOffset = rapid.EndPoint;
|
||||
pgm.Offset(-originOffset);
|
||||
pgm.Codes.RemoveAt(0);
|
||||
}
|
||||
|
||||
var drawing = new Drawing(item.Name, pgm);
|
||||
@@ -660,7 +659,8 @@ namespace OpenNest.Forms
|
||||
var rapid = (RapidMove)firstCode;
|
||||
drawing.Source.Offset = rapid.EndPoint;
|
||||
pgm.Offset(-rapid.EndPoint);
|
||||
pgm.Codes.RemoveAt(0);
|
||||
// Keep the rapid (now at origin) — it marks the contour
|
||||
// start and is needed by the post for correct pierce placement.
|
||||
}
|
||||
|
||||
if (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null)
|
||||
|
||||
Reference in New Issue
Block a user