Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f76659d5d | ||
|
|
a8341e9e99 | ||
|
|
fb067187b4 | ||
|
|
5c66fb3b72 |
@@ -8,113 +8,101 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
public CuttingParameters Parameters { get; set; }
|
public CuttingParameters Parameters { get; set; }
|
||||||
|
|
||||||
|
private record ContourEntry(Shape Shape, Vector Point, Entity Entity);
|
||||||
|
|
||||||
public CuttingResult Apply(Program partProgram, Vector approachPoint)
|
public CuttingResult Apply(Program partProgram, Vector approachPoint)
|
||||||
{
|
{
|
||||||
var exitPoint = approachPoint;
|
|
||||||
var entities = partProgram.ToGeometry();
|
var entities = partProgram.ToGeometry();
|
||||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
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);
|
var scribeEntities = entities.FindAll(e => e.Layer == SpecialLayers.Scribe);
|
||||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
|
||||||
|
|
||||||
var profile = new ShapeProfile(entities);
|
var profile = new ShapeProfile(entities);
|
||||||
|
|
||||||
// Find closest point on perimeter from exit point
|
// Forward pass: sequence cutouts nearest-neighbor from perimeter
|
||||||
var perimeterPoint = profile.Perimeter.ClosestPointTo(exitPoint, out var perimeterEntity);
|
var perimeterPoint = profile.Perimeter.ClosestPointTo(approachPoint, out _);
|
||||||
|
|
||||||
// Chain cutouts by nearest-neighbor from perimeter point, then reverse
|
|
||||||
// so farthest cutouts are cut first, nearest-to-perimeter cut last
|
|
||||||
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
|
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
|
||||||
orderedCutouts.Reverse();
|
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 result = new Program(Mode.Absolute);
|
||||||
var currentPoint = exitPoint;
|
|
||||||
|
|
||||||
// Emit scribe/etch contours first (no lead-ins, no kerf)
|
EmitScribeContours(result, scribeEntities);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var cutout in orderedCutouts)
|
foreach (var entry in cutoutEntries)
|
||||||
{
|
EmitContour(result, entry.Shape, entry.Point, entry.Entity);
|
||||||
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;
|
|
||||||
|
|
||||||
// Perimeter last
|
// Perimeter last
|
||||||
{
|
var lastRefPoint = cutoutEntries.Count > 0 ? cutoutEntries[cutoutEntries.Count - 1].Point : approachPoint;
|
||||||
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
var perimeterPt = profile.Perimeter.ClosestPointTo(lastRefPoint, out var perimeterEntity);
|
||||||
lastCutPoint = perimeterPt;
|
EmitContour(result, profile.Perimeter, perimeterPt, perimeterEntity, ContourType.External);
|
||||||
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
|
||||||
var winding = DetermineWinding(profile.Perimeter);
|
|
||||||
|
|
||||||
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;
|
result.Mode = Mode.Incremental;
|
||||||
|
|
||||||
return new CuttingResult
|
return new CuttingResult
|
||||||
{
|
{
|
||||||
Program = result,
|
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)
|
private List<Shape> SequenceCutouts(List<Shape> cutouts, Vector startPoint)
|
||||||
{
|
{
|
||||||
var remaining = new List<Shape>(cutouts);
|
var remaining = new List<Shape>(cutouts);
|
||||||
|
|||||||
@@ -155,6 +155,17 @@ namespace OpenNest.Geometry
|
|||||||
Center.Y + Radius * System.Math.Sin(EndAngle));
|
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>
|
/// <summary>
|
||||||
/// Splits the arc at the given point, returning two sub-arcs.
|
/// Splits the arc at the given point, returning two sub-arcs.
|
||||||
/// Either half may be null if the split point coincides with an endpoint.
|
/// 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})");
|
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>
|
/// <summary>
|
||||||
/// Creates a sub-program key for matching parts to their sub-programs.
|
/// Creates a sub-program key for matching parts to their sub-programs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -99,9 +119,17 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
mapping[key] = subNum;
|
mapping[key] = subNum;
|
||||||
|
|
||||||
var pgm = part.Program.Clone() as Program;
|
var pgm = part.Program.Clone() as Program;
|
||||||
|
pgm.Mode = Mode.Absolute;
|
||||||
var bbox = pgm.BoundingBox();
|
var bbox = pgm.BoundingBox();
|
||||||
pgm.Offset(-bbox.Location.X, -bbox.Location.Y);
|
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));
|
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,
|
/// Splits a part's program into features by rapids, classifies each as etch or cut,
|
||||||
/// and orders etch features before cut features.
|
/// and orders etch features before cut features.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static List<(List<ICode> codes, bool isEtch)> SplitAndClassify(Part part) =>
|
public static List<(List<ICode> codes, bool isEtch)> SplitAndClassify(Part part)
|
||||||
ClassifyAndOrder(SplitByRapids(part.Program.Codes));
|
{
|
||||||
|
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>
|
/// <summary>
|
||||||
/// Returns true if any non-rapid move in the feature has LayerType.Scribe.
|
/// 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 &&
|
System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance &&
|
||||||
string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase));
|
string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
return entry?.Library ?? "";
|
return EnsureLibExtension(entry?.Library ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ResolveEtchLibrary(string gas)
|
public string ResolveEtchLibrary(string gas)
|
||||||
@@ -32,7 +32,18 @@ public sealed class MaterialLibraryResolver
|
|||||||
var entry = _etchLibraries.FirstOrDefault(e =>
|
var entry = _etchLibraries.FirstOrDefault(e =>
|
||||||
string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase));
|
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)
|
public static string ResolveGas(Nest nest, CincinnatiPostConfig config)
|
||||||
|
|||||||
@@ -388,6 +388,57 @@ public class CincinnatiPostProcessorTests
|
|||||||
Assert.Equal(300, deserialized.PartSubprogramStart);
|
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()
|
private static Nest CreateTestNest()
|
||||||
{
|
{
|
||||||
var nest = new Nest("TestNest");
|
var nest = new Nest("TestNest");
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ namespace OpenNest.Actions
|
|||||||
[DisplayName("Place Lead-in")]
|
[DisplayName("Place Lead-in")]
|
||||||
public class ActionLeadIn : Action
|
public class ActionLeadIn : Action
|
||||||
{
|
{
|
||||||
|
private enum SnapType { None, Endpoint, Midpoint }
|
||||||
|
|
||||||
|
private const double SnapCapturePixels = 10.0;
|
||||||
|
|
||||||
private LayoutPart selectedLayoutPart;
|
private LayoutPart selectedLayoutPart;
|
||||||
private Part selectedPart;
|
private Part selectedPart;
|
||||||
private ShapeProfile profile;
|
private ShapeProfile profile;
|
||||||
@@ -23,6 +27,7 @@ namespace OpenNest.Actions
|
|||||||
private ContourType snapContourType;
|
private ContourType snapContourType;
|
||||||
private double snapNormal;
|
private double snapNormal;
|
||||||
private bool hasSnap;
|
private bool hasSnap;
|
||||||
|
private SnapType activeSnapType;
|
||||||
private ShapeInfo hoveredContour;
|
private ShapeInfo hoveredContour;
|
||||||
private ContextMenuStrip contextMenu;
|
private ContextMenuStrip contextMenu;
|
||||||
private static readonly Brush grayOverlay = new SolidBrush(Color.FromArgb(160, 180, 180, 180));
|
private static readonly Brush grayOverlay = new SolidBrush(Color.FromArgb(160, 180, 180, 180));
|
||||||
@@ -57,6 +62,7 @@ namespace OpenNest.Actions
|
|||||||
profile = null;
|
profile = null;
|
||||||
contours = null;
|
contours = null;
|
||||||
hasSnap = false;
|
hasSnap = false;
|
||||||
|
activeSnapType = SnapType.None;
|
||||||
hoveredContour = null;
|
hoveredContour = null;
|
||||||
plateView.Invalidate();
|
plateView.Invalidate();
|
||||||
}
|
}
|
||||||
@@ -81,6 +87,7 @@ namespace OpenNest.Actions
|
|||||||
// Find closest contour and point
|
// Find closest contour and point
|
||||||
var bestDist = double.MaxValue;
|
var bestDist = double.MaxValue;
|
||||||
hasSnap = false;
|
hasSnap = false;
|
||||||
|
activeSnapType = SnapType.None;
|
||||||
hoveredContour = null;
|
hoveredContour = null;
|
||||||
|
|
||||||
foreach (var info in contours)
|
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();
|
plateView.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,33 +153,39 @@ namespace OpenNest.Actions
|
|||||||
{
|
{
|
||||||
var g = e.Graphics;
|
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)
|
foreach (var lp in plateView.LayoutParts)
|
||||||
{
|
{
|
||||||
if (lp == selectedLayoutPart)
|
if (lp != selectedLayoutPart && lp.Path != null)
|
||||||
continue;
|
|
||||||
|
|
||||||
if (lp.Path != null)
|
|
||||||
g.FillPath(grayOverlay, lp.Path);
|
g.FillPath(grayOverlay, lp.Path);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Highlight the hovered contour
|
private void DrawHoveredContour(Graphics g)
|
||||||
if (hoveredContour != null && selectedPart != null)
|
{
|
||||||
{
|
if (hoveredContour == null || selectedPart == null)
|
||||||
using var contourPath = hoveredContour.Shape.GetGraphicsPath();
|
return;
|
||||||
|
|
||||||
// Translate from local part space to world space, then apply view transform
|
using var contourPath = hoveredContour.Shape.GetGraphicsPath();
|
||||||
using var contourMatrix = new Matrix();
|
using var contourMatrix = new Matrix();
|
||||||
contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y);
|
contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y);
|
||||||
contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append);
|
contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append);
|
||||||
contourPath.Transform(contourMatrix);
|
contourPath.Transform(contourMatrix);
|
||||||
|
|
||||||
var prevSmooth = g.SmoothingMode;
|
var prevSmooth = g.SmoothingMode;
|
||||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||||
g.DrawPath(highlightPen, contourPath);
|
g.DrawPath(highlightPen, contourPath);
|
||||||
g.SmoothingMode = prevSmooth;
|
g.SmoothingMode = prevSmooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawLeadInPreview(Graphics g)
|
||||||
|
{
|
||||||
if (!hasSnap || selectedPart == null)
|
if (!hasSnap || selectedPart == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -176,67 +193,113 @@ namespace OpenNest.Actions
|
|||||||
if (parameters == null)
|
if (parameters == null)
|
||||||
return;
|
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);
|
var leadIn = SelectLeadIn(parameters, snapContourType);
|
||||||
if (leadIn == null)
|
if (leadIn == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Clamp lead-in for circle contours so it stays inside the hole
|
leadIn = ClampLeadInForCircle(leadIn, parameters);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the pierce point (in local space)
|
|
||||||
var piercePoint = leadIn.GetPiercePoint(snapPoint, snapNormal);
|
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;
|
var oldSmooth = g.SmoothingMode;
|
||||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
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);
|
using var previewPen = new Pen(Color.Magenta, 2.0f / plateView.ViewScale);
|
||||||
g.DrawLine(previewPen, pt1, pt2);
|
g.DrawLine(previewPen, pt1, pt2);
|
||||||
|
|
||||||
// Draw a small circle at the pierce point
|
|
||||||
var radius = 3.0f / plateView.ViewScale;
|
var radius = 3.0f / plateView.ViewScale;
|
||||||
g.FillEllipse(Brushes.Magenta, pt1.X - radius, pt1.Y - radius, radius * 2, radius * 2);
|
g.FillEllipse(Brushes.Magenta, pt1.X - radius, pt1.Y - radius, radius * 2, radius * 2);
|
||||||
|
|
||||||
// Draw a small circle at the contour start point
|
if (activeSnapType != SnapType.None)
|
||||||
g.FillEllipse(Brushes.Lime, pt2.X - radius, pt2.Y - radius, radius * 2, radius * 2);
|
DrawSnapMarker(g, pt2, activeSnapType);
|
||||||
|
else
|
||||||
|
g.FillEllipse(Brushes.Lime, pt2.X - radius, pt2.Y - radius, radius * 2, radius * 2);
|
||||||
|
|
||||||
g.SmoothingMode = oldSmooth;
|
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()
|
private void SelectPartAtCursor()
|
||||||
{
|
{
|
||||||
var layoutPart = plateView.GetPartAtPoint(plateView.CurrentPoint);
|
var layoutPart = plateView.GetPartAtPoint(plateView.CurrentPoint);
|
||||||
@@ -342,6 +405,7 @@ namespace OpenNest.Actions
|
|||||||
profile = null;
|
profile = null;
|
||||||
contours = null;
|
contours = null;
|
||||||
hasSnap = false;
|
hasSnap = false;
|
||||||
|
activeSnapType = SnapType.None;
|
||||||
hoveredContour = null;
|
hoveredContour = null;
|
||||||
plateView.Invalidate();
|
plateView.Invalidate();
|
||||||
}
|
}
|
||||||
@@ -365,6 +429,35 @@ namespace OpenNest.Actions
|
|||||||
contextMenu.Show(plateView, location);
|
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)
|
private Vector TransformToWorld(Vector localPt)
|
||||||
{
|
{
|
||||||
// The contours are already in rotated local space (we rotated the program
|
// The contours are already in rotated local space (we rotated the program
|
||||||
|
|||||||
@@ -432,7 +432,6 @@ namespace OpenNest.Forms
|
|||||||
var rapid = (RapidMove)pgm[0];
|
var rapid = (RapidMove)pgm[0];
|
||||||
drawing.Source.Offset = rapid.EndPoint;
|
drawing.Source.Offset = rapid.EndPoint;
|
||||||
pgm.Offset(-rapid.EndPoint);
|
pgm.Offset(-rapid.EndPoint);
|
||||||
pgm.Codes.RemoveAt(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drawing.Program = pgm;
|
drawing.Program = pgm;
|
||||||
|
|||||||
@@ -362,7 +362,6 @@ namespace OpenNest.Forms
|
|||||||
var rapid = (RapidMove)pgm[0];
|
var rapid = (RapidMove)pgm[0];
|
||||||
originOffset = rapid.EndPoint;
|
originOffset = rapid.EndPoint;
|
||||||
pgm.Offset(-originOffset);
|
pgm.Offset(-originOffset);
|
||||||
pgm.Codes.RemoveAt(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var drawing = new Drawing(item.Name, pgm);
|
var drawing = new Drawing(item.Name, pgm);
|
||||||
@@ -660,7 +659,8 @@ namespace OpenNest.Forms
|
|||||||
var rapid = (RapidMove)firstCode;
|
var rapid = (RapidMove)firstCode;
|
||||||
drawing.Source.Offset = rapid.EndPoint;
|
drawing.Source.Offset = rapid.EndPoint;
|
||||||
pgm.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)
|
if (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null)
|
||||||
|
|||||||
Reference in New Issue
Block a user