Polygon.OffsetEntity now computes proper miter-join offsets using edge normals and winding direction, with self-intersection cleanup. CutOff exclusion zones use geometric perimeter offset instead of scalar padding, giving uniform clearance around parts regardless of cut angle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
212 lines
7.5 KiB
C#
212 lines
7.5 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.Geometry;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace OpenNest
|
|
{
|
|
public enum CutOffAxis
|
|
{
|
|
Horizontal,
|
|
Vertical
|
|
}
|
|
|
|
public class CutOff
|
|
{
|
|
public Vector Position { get; set; }
|
|
public CutOffAxis Axis { get; set; }
|
|
public double? StartLimit { get; set; }
|
|
public double? EndLimit { get; set; }
|
|
public Drawing Drawing { get; private set; }
|
|
|
|
public CutOff(Vector position, CutOffAxis axis)
|
|
{
|
|
Position = position;
|
|
Axis = axis;
|
|
Drawing = new Drawing(GetName()) { IsCutOff = true };
|
|
}
|
|
|
|
public void Regenerate(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache = null)
|
|
{
|
|
var segments = ComputeSegments(plate, settings, cache);
|
|
var program = BuildProgram(segments, settings);
|
|
Drawing.Program = program;
|
|
}
|
|
|
|
private string GetName()
|
|
{
|
|
var axisChar = Axis == CutOffAxis.Vertical ? "V" : "H";
|
|
var coord = Axis == CutOffAxis.Vertical ? Position.X : Position.Y;
|
|
return $"CutOff-{axisChar}-{coord:F2}";
|
|
}
|
|
|
|
private List<(double Start, double End)> ComputeSegments(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache)
|
|
{
|
|
var bounds = plate.BoundingBox(includeParts: false);
|
|
|
|
double lineStart, lineEnd, cutPosition;
|
|
|
|
if (Axis == CutOffAxis.Vertical)
|
|
{
|
|
cutPosition = Position.X;
|
|
lineStart = StartLimit ?? bounds.Y;
|
|
lineEnd = EndLimit ?? (bounds.Y + bounds.Length + settings.Overtravel);
|
|
}
|
|
else
|
|
{
|
|
cutPosition = Position.Y;
|
|
lineStart = StartLimit ?? bounds.X;
|
|
lineEnd = EndLimit ?? (bounds.X + bounds.Width + settings.Overtravel);
|
|
}
|
|
|
|
var exclusions = new List<(double Start, double End)>();
|
|
|
|
foreach (var part in plate.Parts)
|
|
{
|
|
if (part.BaseDrawing.IsCutOff)
|
|
continue;
|
|
|
|
Entity perimeter = null;
|
|
cache?.TryGetValue(part, out perimeter);
|
|
var partExclusions = GetPartExclusions(part, perimeter, cutPosition, lineStart, lineEnd, settings.PartClearance);
|
|
exclusions.AddRange(partExclusions);
|
|
}
|
|
|
|
exclusions.Sort((a, b) => a.Start.CompareTo(b.Start));
|
|
var merged = new List<(double Start, double End)>();
|
|
foreach (var ex in exclusions)
|
|
{
|
|
if (merged.Count > 0 && ex.Start <= merged[^1].End)
|
|
merged[^1] = (merged[^1].Start, System.Math.Max(merged[^1].End, ex.End));
|
|
else
|
|
merged.Add(ex);
|
|
}
|
|
|
|
var segments = new List<(double Start, double End)>();
|
|
var current = lineStart;
|
|
|
|
foreach (var ex in merged)
|
|
{
|
|
var clampedStart = System.Math.Max(ex.Start, lineStart);
|
|
var clampedEnd = System.Math.Min(ex.End, lineEnd);
|
|
|
|
if (clampedStart > current)
|
|
segments.Add((current, clampedStart));
|
|
|
|
current = System.Math.Max(current, clampedEnd);
|
|
}
|
|
|
|
if (current < lineEnd)
|
|
segments.Add((current, lineEnd));
|
|
|
|
segments = segments.Where(s => (s.End - s.Start) >= settings.MinSegmentLength).ToList();
|
|
|
|
return segments;
|
|
}
|
|
|
|
private static readonly List<(double Start, double End)> EmptyExclusions = new();
|
|
|
|
private List<(double Start, double End)> GetPartExclusions(
|
|
Part part, Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
|
{
|
|
var bb = part.BoundingBox;
|
|
var (partMin, partMax) = AxisBounds(bb, clearance);
|
|
var (partStart, partEnd) = CrossAxisBounds(bb, clearance);
|
|
|
|
if (cutPosition < partMin || cutPosition > partMax)
|
|
return EmptyExclusions;
|
|
|
|
if (perimeter != null)
|
|
{
|
|
var perimeterExclusions = IntersectPerimeter(perimeter, cutPosition, lineStart, lineEnd, clearance);
|
|
if (perimeterExclusions != null)
|
|
return perimeterExclusions;
|
|
}
|
|
|
|
return new List<(double Start, double End)> { (partStart, partEnd) };
|
|
}
|
|
|
|
private List<(double Start, double End)> IntersectPerimeter(
|
|
Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
|
{
|
|
var target = OffsetOutward(perimeter, clearance) ?? perimeter;
|
|
var usedOffset = target != perimeter;
|
|
var cutLine = new Line(MakePoint(cutPosition, lineStart), MakePoint(cutPosition, lineEnd));
|
|
|
|
if (!target.Intersects(cutLine, out var pts) || pts.Count < 2)
|
|
return null;
|
|
|
|
var coords = pts
|
|
.Select(pt => Axis == CutOffAxis.Vertical ? pt.Y : pt.X)
|
|
.OrderBy(c => c)
|
|
.ToList();
|
|
|
|
if (coords.Count % 2 != 0)
|
|
return null;
|
|
|
|
var padding = usedOffset ? 0 : clearance;
|
|
var result = new List<(double Start, double End)>();
|
|
for (var i = 0; i < coords.Count; i += 2)
|
|
result.Add((coords[i] - padding, coords[i + 1] + padding));
|
|
|
|
return result;
|
|
}
|
|
|
|
private static Entity OffsetOutward(Entity perimeter, double clearance)
|
|
{
|
|
if (clearance <= 0)
|
|
return null;
|
|
|
|
try
|
|
{
|
|
var offset = perimeter.OffsetEntity(clearance, OffsetSide.Left);
|
|
offset?.UpdateBounds();
|
|
return offset;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private Vector MakePoint(double cutCoord, double lineCoord) =>
|
|
Axis == CutOffAxis.Vertical
|
|
? new Vector(cutCoord, lineCoord)
|
|
: new Vector(lineCoord, cutCoord);
|
|
|
|
private (double Min, double Max) AxisBounds(Box bb, double clearance) =>
|
|
Axis == CutOffAxis.Vertical
|
|
? (bb.X - clearance, bb.X + bb.Width + clearance)
|
|
: (bb.Y - clearance, bb.Y + bb.Length + clearance);
|
|
|
|
private (double Start, double End) CrossAxisBounds(Box bb, double clearance) =>
|
|
Axis == CutOffAxis.Vertical
|
|
? (bb.Y - clearance, bb.Y + bb.Length + clearance)
|
|
: (bb.X - clearance, bb.X + bb.Width + clearance);
|
|
|
|
private Program BuildProgram(List<(double Start, double End)> segments, CutOffSettings settings)
|
|
{
|
|
var program = new Program();
|
|
|
|
if (segments.Count == 0)
|
|
return program;
|
|
|
|
var toward = settings.CutDirection == CutDirection.TowardOrigin;
|
|
segments = toward
|
|
? segments.OrderByDescending(s => s.Start).ToList()
|
|
: segments.OrderBy(s => s.Start).ToList();
|
|
|
|
var cutPos = Axis == CutOffAxis.Vertical ? Position.X : Position.Y;
|
|
|
|
foreach (var seg in segments)
|
|
{
|
|
var (from, to) = toward ? (seg.End, seg.Start) : (seg.Start, seg.End);
|
|
program.Codes.Add(new RapidMove(MakePoint(cutPos, from)));
|
|
program.Codes.Add(new LinearMove(MakePoint(cutPos, to)));
|
|
}
|
|
|
|
return program;
|
|
}
|
|
}
|
|
}
|