Files
OpenNest/OpenNest.Core/Geometry/ShapeProfile.cs
T
ajandClaude Opus 5.5 1a46d9896f perf(geometry): speed up labels and shape chaining on many-hole parts
The Drawing Library Grid check found seven drawings with 1,000-1,700
holes (90-6138-26/27, Reeves bakery vent pans, 4938 A15) that took
0.5-3.6 s to preview, all of it in the part label point.

- PolyLabel skips a hole's containment test outside its bounds and its
  edge distances when its bounds are farther than the nearest edge so
  far. Both skips leave the result exactly as before; label points on
  all seven drawings are unchanged and the search is 4-35x faster.
- ShapeBuilder.GetShapes rescanned every remaining entity at each
  chaining step. An endpoint grid now finds the candidates, tested in
  list order with the same checks, so chosen entities, reversals and
  shapes are identical (compared against the old loop on the library
  drawings and 300 random entity soups). 90-6138: 700 ms -> 40 ms.
- The label builds its shapes once: ShapeProfile.FromShapes reuses the
  ones it already chained instead of chaining them again.

Previews of all seven now take 57-166 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 07:50:06 -05:00

113 lines
3.5 KiB
C#

using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Geometry
{
public class ShapeProfile
{
public ShapeProfile(Shape shape)
{
Update(shape.Entities);
}
public ShapeProfile(List<Entity> entities)
{
Update(entities);
}
private ShapeProfile()
{
}
/// <summary>A profile of shapes already built, for callers that need them too; at least one.</summary>
public static ShapeProfile FromShapes(List<Shape> shapes)
{
var profile = new ShapeProfile();
profile.Update(shapes);
return profile;
}
private void Update(List<Entity> entities) => Update(ShapeBuilder.GetShapes(entities));
private void Update(List<Shape> shapes)
{
Perimeter = shapes[0];
Cutouts = new List<Shape>();
for (var i = 1; i < shapes.Count; i++)
{
var bb = shapes[i].BoundingBox;
var perimBB = Perimeter.BoundingBox;
if (bb.Width * bb.Length > perimBB.Width * perimBB.Length)
{
Cutouts.Add(Perimeter);
Perimeter = shapes[i];
}
else
{
Cutouts.Add(shapes[i]);
}
}
}
public Shape Perimeter { get; set; }
public List<Shape> Cutouts { get; set; }
/// <summary>
/// Ensures CNC-standard winding: perimeter CW (kerf left = outward),
/// cutouts CCW (kerf left = inward). Reverses contours in-place as needed.
/// </summary>
public void NormalizeWinding()
{
EnsureWinding(Perimeter, RotationType.CW);
foreach (var cutout in Cutouts)
EnsureWinding(cutout, RotationType.CCW);
}
/// <summary>
/// Returns the entities in normalized winding order (perimeter first, then cutouts).
/// </summary>
public List<Entity> ToNormalizedEntities()
{
NormalizeWinding();
var result = new List<Entity>(Perimeter.Entities);
foreach (var cutout in Cutouts)
result.AddRange(cutout.Entities);
return result;
}
/// <summary>
/// Convenience method: builds a ShapeProfile from raw entities,
/// normalizes winding, and returns the corrected entity list.
/// </summary>
public static List<Entity> NormalizeEntities(IEnumerable<Entity> entities)
{
var cloned = entities.CloneAll();
// Break cuts are open and their direction says which side is scrap: never re-chain or
// re-wind them. They follow the contours, in their original order.
var breakCuts = cloned.Where(e => e.Layer == SpecialLayers.Break).ToList();
if (breakCuts.Count > 0)
cloned.RemoveAll(e => e.Layer == SpecialLayers.Break);
var profile = new ShapeProfile(cloned);
var result = profile.ToNormalizedEntities();
result.AddRange(breakCuts);
return result;
}
private static void EnsureWinding(Shape shape, RotationType desired)
{
var poly = shape.ToPolygon();
if (poly != null && poly.Vertices.Count >= 3 && poly.RotationDirection() != desired)
{
shape.Reverse();
}
}
}
}