mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 06:51:35 -05:00
A part with lead-ins applied could not be pushed away once another part touched it. PartGeometry kept LEADIN/LEADOUT entities as material, so the leads chained onto the perimeter as open spurs and the spacing offset no longer closed. The slide-contact classifier cannot tell the material side of an unclosed boundary and blocks every contact, so parts resting against it could not move up or right off it. Leads are cut into scrap, and the spacing review, overlap analyzer and support checks already drop them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
267 lines
9.1 KiB
C#
267 lines
9.1 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest
|
|
{
|
|
public static class PartGeometry
|
|
{
|
|
/// <summary>
|
|
/// Nesting material of a program, without lead-ins and lead-outs: a lead is a cut
|
|
/// into scrap, not part material, and chaining it into the perimeter leaves an open
|
|
/// spur, so the offset contour no longer closes and every slide contact against it
|
|
/// blocks.
|
|
/// </summary>
|
|
private static List<Entity> MaterialEntities(CNC.Program program) =>
|
|
ConvertProgram.ToGeometry(program)
|
|
.Where(e => e.Layer != SpecialLayers.Leadin && e.Layer != SpecialLayers.Leadout)
|
|
.ForNesting();
|
|
|
|
public static List<Line> GetPartLines(Part part, double chordTolerance = 0.001)
|
|
{
|
|
var entities = MaterialEntities(part.Program);
|
|
var shapes = ShapeBuilder.GetShapes(entities);
|
|
var lines = new List<Line>();
|
|
|
|
foreach (var shape in shapes)
|
|
{
|
|
var polygon = shape.ToPolygonWithTolerance(chordTolerance);
|
|
polygon.Offset(part.Location);
|
|
lines.AddRange(polygon.ToLines());
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
public static List<Line> GetPartLines(
|
|
Part part,
|
|
PushDirection facingDirection,
|
|
double chordTolerance = 0.001
|
|
)
|
|
{
|
|
return GetDirectionalPartLines(
|
|
part, chordTolerance, useVector: false, facingDirection, default
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the perimeter entities (Line, Arc, Circle) with spacing offset applied,
|
|
/// without tessellation, which keeps arc-heavy parts fast in directional-distance loops.
|
|
/// </summary>
|
|
public static List<Entity> GetOffsetPerimeterEntities(Part part, double spacing)
|
|
{
|
|
var entities = GetOffsetPerimeterEntities(part.Program, spacing);
|
|
foreach (var entity in entities)
|
|
entity.Offset(part.Location);
|
|
|
|
return entities;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepares a fresh offset perimeter in the program's local frame, without translation.
|
|
/// </summary>
|
|
public static List<Entity> GetOffsetPerimeterEntities(CNC.Program program, double spacing)
|
|
{
|
|
PerfCounters.CountOffsetPerimeterEntities();
|
|
var geoEntities = MaterialEntities(program);
|
|
var profile = new ShapeProfile(geoEntities);
|
|
|
|
var offsetShape = profile.Perimeter.OffsetOutward(spacing);
|
|
if (offsetShape == null)
|
|
return new List<Entity>();
|
|
|
|
return offsetShape.Entities;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all entities (perimeter + cutouts) with spacing offset applied,
|
|
/// without tessellation. Perimeter is offset outward, cutouts inward.
|
|
/// </summary>
|
|
public static List<Entity> GetOffsetPartEntities(Part part, double spacing)
|
|
{
|
|
var geoEntities = MaterialEntities(part.Program);
|
|
var profile = new ShapeProfile(geoEntities);
|
|
var entities = new List<Entity>();
|
|
|
|
var perimeter = profile.Perimeter.OffsetOutward(spacing);
|
|
if (perimeter != null)
|
|
{
|
|
foreach (var entity in perimeter.Entities)
|
|
entity.Offset(part.Location);
|
|
entities.AddRange(perimeter.Entities);
|
|
}
|
|
|
|
foreach (var cutout in profile.Cutouts)
|
|
{
|
|
var inset = cutout.OffsetInward(spacing);
|
|
if (inset == null)
|
|
continue;
|
|
foreach (var entity in inset.Entities)
|
|
entity.Offset(part.Location);
|
|
entities.AddRange(inset.Entities);
|
|
}
|
|
|
|
return entities;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns perimeter entities at the part's world location, without tessellation
|
|
/// or spacing offset.
|
|
/// </summary>
|
|
public static List<Entity> GetPerimeterEntities(Part part)
|
|
{
|
|
var geoEntities = MaterialEntities(part.Program);
|
|
var profile = new ShapeProfile(geoEntities);
|
|
|
|
return CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all entities (perimeter + cutouts) at the part's world location,
|
|
/// without tessellation or spacing offset.
|
|
/// </summary>
|
|
public static List<Entity> GetPartEntities(Part part)
|
|
{
|
|
var geoEntities = MaterialEntities(part.Program);
|
|
var profile = new ShapeProfile(geoEntities);
|
|
var entities = CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
|
|
|
|
foreach (var cutout in profile.Cutouts)
|
|
entities.AddRange(CopyEntitiesAtLocation(cutout.Entities, part.Location));
|
|
|
|
return entities;
|
|
}
|
|
|
|
private static List<Entity> CopyEntitiesAtLocation(List<Entity> source, Vector location)
|
|
{
|
|
var result = new List<Entity>(source.Count);
|
|
|
|
foreach (var entity in source)
|
|
{
|
|
var copy = entity.Clone();
|
|
copy.Offset(location);
|
|
result.Add(copy);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public static List<Line> GetPartLines(
|
|
Part part,
|
|
Vector facingDirection,
|
|
double chordTolerance = 0.001
|
|
)
|
|
{
|
|
return GetDirectionalPartLines(
|
|
part, chordTolerance, useVector: true, default, facingDirection
|
|
);
|
|
}
|
|
|
|
private static List<Line> GetDirectionalPartLines(
|
|
Part part,
|
|
double chordTolerance,
|
|
bool useVector,
|
|
PushDirection cardinalDirection,
|
|
Vector vectorDirection
|
|
)
|
|
{
|
|
var entities = MaterialEntities(part.Program);
|
|
var shapes = ShapeBuilder.GetShapes(entities);
|
|
var lines = new List<Line>();
|
|
|
|
foreach (var shape in shapes)
|
|
{
|
|
var polygon = shape.ToPolygonWithTolerance(chordTolerance);
|
|
polygon.Offset(part.Location);
|
|
// Keep the overload-specific arithmetic and invalid-direction behavior.
|
|
lines.AddRange(useVector
|
|
? GetDirectionalLines(polygon, vectorDirection)
|
|
: GetDirectionalLines(polygon, cardinalDirection));
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns only polygon edges whose outward normal faces the specified direction vector.
|
|
/// </summary>
|
|
private static List<Line> GetDirectionalLines(Polygon polygon, Vector direction)
|
|
{
|
|
if (polygon.Vertices.Count < 3)
|
|
return polygon.ToLines();
|
|
|
|
var sign = polygon.RotationDirection() == RotationType.CCW ? 1.0 : -1.0;
|
|
var lines = new List<Line>();
|
|
var last = polygon.Vertices[0];
|
|
|
|
for (var i = 1; i < polygon.Vertices.Count; i++)
|
|
{
|
|
var current = polygon.Vertices[i];
|
|
var edx = current.X - last.X;
|
|
var edy = current.Y - last.Y;
|
|
|
|
var keep = sign * (edy * direction.X - edx * direction.Y) > 0;
|
|
|
|
if (keep)
|
|
lines.Add(new Line(last, current));
|
|
|
|
last = current;
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns only polygon edges whose outward normal faces the specified direction.
|
|
/// </summary>
|
|
private static List<Line> GetDirectionalLines(
|
|
Polygon polygon,
|
|
PushDirection facingDirection
|
|
)
|
|
{
|
|
if (polygon.Vertices.Count < 3)
|
|
return polygon.ToLines();
|
|
|
|
var sign = polygon.RotationDirection() == RotationType.CCW ? 1.0 : -1.0;
|
|
var lines = new List<Line>();
|
|
var last = polygon.Vertices[0];
|
|
|
|
for (int i = 1; i < polygon.Vertices.Count; i++)
|
|
{
|
|
var current = polygon.Vertices[i];
|
|
var dx = current.X - last.X;
|
|
var dy = current.Y - last.Y;
|
|
|
|
bool keep;
|
|
|
|
switch (facingDirection)
|
|
{
|
|
case PushDirection.Left:
|
|
keep = -sign * dy > 0;
|
|
break;
|
|
case PushDirection.Right:
|
|
keep = sign * dy > 0;
|
|
break;
|
|
case PushDirection.Up:
|
|
keep = -sign * dx > 0;
|
|
break;
|
|
case PushDirection.Down:
|
|
keep = sign * dx > 0;
|
|
break;
|
|
default:
|
|
keep = true;
|
|
break;
|
|
}
|
|
|
|
if (keep)
|
|
lines.Add(new Line(last, current));
|
|
|
|
last = current;
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
}
|
|
}
|