Compare commits

...

4 Commits

Author SHA1 Message Date
aj 9f76659d5d refactor: two-pass lead-in placement in ContourCuttingStrategy
Resolve lead-in points by walking backward through cutting order (from
perimeter outward) so each lead-in faces the next cutout to be cut
rather than pointing back at the previous lead-out. Extract EmitContour
and EmitScribeContours to eliminate duplicated cutout/perimeter logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:26:47 -04:00
aj a8341e9e99 fix: preserve leading rapid in programs to prevent missing contour segment
The CAD converter and BOM import were stripping the leading RapidMove
after normalizing program coordinates to origin. This left programs
starting with a LinearMove, causing the post-processor to use that
endpoint as the pierce point — making the first contour edge zero-length
and losing the closing segment (e.g. the bottom line on curved parts).

Root cause: CadConverterForm.GetDrawings(), OnSplitClicked(), and
BomImportForm all called pgm.Codes.RemoveAt(0) after offsetting the
rapid to origin. The rapid at (0,0) is a harmless no-op that marks the
contour start point for downstream processing.

Also adds EnsureLeadingRapid() safety net in the Cincinnati post for
existing nest files that already have the rapid stripped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:33:59 -04:00
aj fb067187b4 fix: ensure absolute coordinates and .lib extension in post output
Convert programs to absolute mode before extracting features for
Cincinnati post output, fixing incorrect coordinates when programs
are stored in incremental mode. Also ensure G89 library names
always end with .lib extension.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:24:32 -04:00
aj 5c66fb3b72 feat: add snap-to-endpoint/midpoint for lead-in placement
Priority-based snapping: when the cursor is within 10px of an entity
endpoint or midpoint, snaps to it instead of the nearest contour point.
Diamond marker (endpoint) or triangle marker (midpoint) replaces the
lime dot to indicate active snap. Also refactors OnPaint into focused
helper methods and adds Arc.MidPoint().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 08:42:13 -04:00
9 changed files with 351 additions and 147 deletions
@@ -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);
+11
View File
@@ -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));
}
}
+25 -2
View File
@@ -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");
+153 -60
View File
@@ -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
-1
View File
@@ -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;
+2 -2
View File
@@ -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)