Per-entity offsetting left spikes and inverted loops wherever a feature is narrower than the spacing (1.nest), and RemoveSelfIntersections only caught proper crossings. The callers that already flatten to polygons now take a single Clipper region offset instead: - PolygonHelper (BestFit) and PartBoundary use the conservative mode, which keeps their never-under-estimate guarantee. PartBoundary also keeps holes that appear when a perimeter curls back on itself. - NestValidator offsets perimeter and cutouts in one region; Clipper drops collapsed cutouts, so the collapsed-or-flipped heuristic goes away. - CutOff.IntersectPerimeter offsets through the bridge. The old OffsetEntity(Left) grew CW perimeters but shrank CCW ones, so with the plate's perimeter cache a cut-off ran through the part; slots narrower than twice the clearance now close up instead of leaving a gap. - GetOffsetPartLines (3 overloads) and the AddOffset* helpers had no callers and are removed, as is EntityView's never-defined DRAW_OFFSET block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
6.2 KiB
C#
181 lines
6.2 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.Fill
|
|
{
|
|
/// <summary>
|
|
/// Pre-computed offset boundary polygons for a part's geometry.
|
|
/// Polygons are stored at program-local origin (no location applied)
|
|
/// and can be efficiently translated to any location when extracting lines.
|
|
/// Directional edge filtering is pre-computed once in the constructor.
|
|
/// </summary>
|
|
public class PartBoundary
|
|
{
|
|
private const double PolygonTolerance = 0.01;
|
|
|
|
private readonly List<Polygon> _polygons;
|
|
private readonly (Vector start, Vector end)[] _leftEdges;
|
|
private readonly (Vector start, Vector end)[] _rightEdges;
|
|
private readonly (Vector start, Vector end)[] _upEdges;
|
|
private readonly (Vector start, Vector end)[] _downEdges;
|
|
|
|
public PartBoundary(Part part, double spacing)
|
|
{
|
|
var entities = ConvertProgram
|
|
.ToGeometry(part.Program)
|
|
.Where(e => e.Layer == SpecialLayers.Cut)
|
|
.ToList();
|
|
|
|
var definedShape = new ShapeProfile(entities);
|
|
var perimeter = definedShape.Perimeter;
|
|
_polygons = new List<Polygon>();
|
|
|
|
if (perimeter != null)
|
|
{
|
|
// Conservative offset: the boundary never under-estimates the spacing.
|
|
// Holes appear only where the perimeter curls back on itself.
|
|
var offset = ClipperBridge.OffsetPerimeter(
|
|
perimeter,
|
|
spacing,
|
|
PolygonTolerance,
|
|
circumscribe: true
|
|
);
|
|
_polygons.AddRange(offset.Outers);
|
|
_polygons.AddRange(offset.Holes);
|
|
}
|
|
|
|
PrecomputeDirectionalEdges(
|
|
out _leftEdges,
|
|
out _rightEdges,
|
|
out _upEdges,
|
|
out _downEdges
|
|
);
|
|
}
|
|
|
|
private void PrecomputeDirectionalEdges(
|
|
out (Vector start, Vector end)[] leftEdges,
|
|
out (Vector start, Vector end)[] rightEdges,
|
|
out (Vector start, Vector end)[] upEdges,
|
|
out (Vector start, Vector end)[] downEdges
|
|
)
|
|
{
|
|
var left = new List<(Vector, Vector)>();
|
|
var right = new List<(Vector, Vector)>();
|
|
var up = new List<(Vector, Vector)>();
|
|
var down = new List<(Vector, Vector)>();
|
|
|
|
foreach (var polygon in _polygons)
|
|
{
|
|
var verts = polygon.Vertices;
|
|
|
|
if (verts.Count < 3)
|
|
{
|
|
for (var i = 1; i < verts.Count; i++)
|
|
{
|
|
var edge = (verts[i - 1], verts[i]);
|
|
left.Add(edge);
|
|
right.Add(edge);
|
|
up.Add(edge);
|
|
down.Add(edge);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
var sign = polygon.RotationDirection() == RotationType.CCW ? 1.0 : -1.0;
|
|
|
|
for (var i = 1; i < verts.Count; i++)
|
|
{
|
|
var dx = verts[i].X - verts[i - 1].X;
|
|
var dy = verts[i].Y - verts[i - 1].Y;
|
|
var edge = (verts[i - 1], verts[i]);
|
|
|
|
if (-sign * dy > 0)
|
|
left.Add(edge);
|
|
if (sign * dy > 0)
|
|
right.Add(edge);
|
|
if (-sign * dx > 0)
|
|
up.Add(edge);
|
|
if (sign * dx > 0)
|
|
down.Add(edge);
|
|
}
|
|
}
|
|
|
|
leftEdges = left.OrderBy(e => System.Math.Min(e.Item1.Y, e.Item2.Y)).ToArray();
|
|
rightEdges = right.OrderBy(e => System.Math.Min(e.Item1.Y, e.Item2.Y)).ToArray();
|
|
upEdges = up.OrderBy(e => System.Math.Min(e.Item1.X, e.Item2.X)).ToArray();
|
|
downEdges = down.OrderBy(e => System.Math.Min(e.Item1.X, e.Item2.X)).ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns offset boundary lines translated to the given location,
|
|
/// filtered to edges whose outward normal faces the specified direction.
|
|
/// </summary>
|
|
public List<Line> GetLines(Vector location, PushDirection facingDirection)
|
|
{
|
|
var edges = GetDirectionalEdges(facingDirection);
|
|
var lines = new List<Line>(edges.Length);
|
|
|
|
foreach (var (start, end) in edges)
|
|
lines.Add(new Line(start.Offset(location), end.Offset(location)));
|
|
|
|
return lines;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all offset boundary lines translated to the given location.
|
|
/// </summary>
|
|
public List<Line> GetLines(Vector location)
|
|
{
|
|
var lines = new List<Line>();
|
|
|
|
foreach (var polygon in _polygons)
|
|
{
|
|
var verts = polygon.Vertices;
|
|
|
|
if (verts.Count < 2)
|
|
continue;
|
|
|
|
var last = verts[0].Offset(location);
|
|
|
|
for (var i = 1; i < verts.Count; i++)
|
|
{
|
|
var current = verts[i].Offset(location);
|
|
lines.Add(new Line(last, current));
|
|
last = current;
|
|
}
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
private (Vector start, Vector end)[] GetDirectionalEdges(PushDirection direction)
|
|
{
|
|
switch (direction)
|
|
{
|
|
case PushDirection.Left:
|
|
return _leftEdges;
|
|
case PushDirection.Right:
|
|
return _rightEdges;
|
|
case PushDirection.Up:
|
|
return _upEdges;
|
|
case PushDirection.Down:
|
|
return _downEdges;
|
|
default:
|
|
return _leftEdges;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the pre-computed edge arrays for the given direction.
|
|
/// These are in part-local coordinates (no translation applied).
|
|
/// </summary>
|
|
public (Vector start, Vector end)[] GetEdges(PushDirection direction)
|
|
{
|
|
return GetDirectionalEdges(direction);
|
|
}
|
|
}
|
|
}
|