Files
OpenNest/OpenNest.Core/Part.cs
T
ajandClaude Opus 5.5 1c363504f5 perf(engine): key fill caches by source drawing so they hit across trials
Every DefaultPlateFiller.Fill makes a fresh canonical copy of the drawing,
and BestFitCache/FillResultCache keyed by drawing reference, so fills never
shared results and the static caches grew without bound.

CanonicalFrame now records which drawing each canonical copy came from.
Both caches key weakly on that source drawing, so every canonical copy
shares one entry and released drawings can be collected. An entry is
dropped when the drawing's Program instance or canonical angle changes.
Best-fit candidates are computed once per (drawing, spacing) through
BestFitFinder.FindCandidates and filtered per plate size with the same
filter FindBestFits uses. FillResultCache keeps canonical and
non-canonical callers apart.

Adds Debug-only PerfCounters for best-fit runs, offset perimeter builds
and Part.Intersects calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:29:43 -04:00

356 lines
11 KiB
C#

using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
{
public interface IPart : IBoundable
{
Vector Location { get; set; }
double Rotation { get; }
void Rotate(double angle);
void Rotate(double angle, Vector origin);
void Offset(double x, double y);
void Offset(Vector voffset);
void Update();
}
public class Part : IPart, IBoundable
{
/// <summary>
/// Chord tolerance used to polygonize arcs/circles for overlap testing in
/// <see cref="Intersects"/>. Matches the tolerance used elsewhere for
/// geometry-sensitive checks (e.g. <see cref="PartGeometry"/>).
/// </summary>
private const double IntersectsChordTolerance = 0.001;
private Vector location;
private bool ownsProgram;
private double preLeadInRotation;
public readonly Drawing BaseDrawing;
public Part(Drawing baseDrawing)
: this(baseDrawing, new Vector()) { }
public Part(Drawing baseDrawing, Vector location)
{
BaseDrawing = baseDrawing;
Program = baseDrawing.Program.Clone() as Program;
ownsProgram = true;
this.location = location;
UpdateBounds();
}
/// <summary>
/// Location of the part.
/// </summary>
public Vector Location
{
get { return location; }
set
{
BoundingBox.Offset(value - location);
location = value;
}
}
public Program Program { get; private set; }
public bool HasManualLeadIns { get; set; }
public bool LeadInsLocked { get; set; }
public CNC.CuttingStrategy.CuttingParameters CuttingParameters { get; set; }
public void ApplyLeadIns(
CNC.CuttingStrategy.CuttingParameters parameters,
Vector approachPoint
)
{
ApplyLeadIns(parameters, approachPoint, Geometry.Vector.Invalid);
}
public void ApplyLeadIns(
CNC.CuttingStrategy.CuttingParameters parameters,
Vector approachPoint,
Vector nextPartStart
)
{
preLeadInRotation = Rotation;
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy
{
Parameters = parameters,
};
var result = strategy.Apply(Program, approachPoint, nextPartStart);
Program = result.Program;
CuttingParameters = parameters;
HasManualLeadIns = true;
UpdateBounds();
}
public void ApplySingleLeadIn(
CNC.CuttingStrategy.CuttingParameters parameters,
Geometry.Vector point,
Geometry.Entity entity,
CNC.CuttingStrategy.ContourType contourType
)
{
preLeadInRotation = Rotation;
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy
{
Parameters = parameters,
};
var result = strategy.ApplySingle(Program, point, entity, contourType);
Program = result.Program;
CuttingParameters = parameters;
HasManualLeadIns = true;
UpdateBounds();
}
public void RemoveLeadIns()
{
var rotation = preLeadInRotation;
var location = Location;
Program = BaseDrawing.Program.Clone() as Program;
ownsProgram = true;
if (!Math.Tolerance.IsEqualTo(rotation, 0))
Program.Rotate(rotation);
Location = location;
HasManualLeadIns = false;
LeadInsLocked = false;
CuttingParameters = null;
UpdateBounds();
}
/// <summary>
/// Gets the rotation of the part in radians.
/// </summary>
public double Rotation
{
get { return HasManualLeadIns ? preLeadInRotation : Program.Rotation; }
}
/// <summary>
/// Rotates the part.
/// </summary>
/// <param name="angle">Angle of rotation in radians.</param>
public void Rotate(double angle)
{
EnsureOwnedProgram();
Program.Rotate(angle);
location = Location.Rotate(angle);
preLeadInRotation = Program.Rotation;
UpdateBounds();
}
/// <summary>
/// Rotates the part around the specified origin.
/// </summary>
/// <param name="angle">Angle of rotation in radians.</param>
/// <param name="origin">The origin to rotate the part around.</param>
public void Rotate(double angle, Vector origin)
{
EnsureOwnedProgram();
Program.Rotate(angle);
location = Location.Rotate(angle, origin);
preLeadInRotation = Program.Rotation;
UpdateBounds();
}
/// <summary>
/// Offsets the part.
/// </summary>
/// <param name="x">The x-axis offset distance.</param>
/// <param name="y">The y-axis offset distance.</param>
public void Offset(double x, double y)
{
location = new Vector(location.X + x, location.Y + y);
BoundingBox.Offset(x, y);
}
/// <summary>
/// Offsets the part.
/// </summary>
/// <param name="voffset">The vector containing the x-axis & y-axis offset distances.</param>
public void Offset(Vector voffset)
{
location += voffset;
BoundingBox.Offset(voffset);
}
/// <summary>
/// Creates a part normalized to the origin with optional rotation.
/// </summary>
public static Part CreateAtOrigin(Drawing drawing, double rotation = 0)
{
var part = new Part(drawing);
if (!Math.Tolerance.IsEqualTo(rotation, 0))
part.Rotate(rotation);
var bbox = part.Program.BoundingBox();
part.Offset(-bbox.Location.X, -bbox.Location.Y);
part.UpdateBounds();
return part;
}
/// <summary>
/// Updates the bounding box of the part.
/// </summary>
public void UpdateBounds()
{
BoundingBox = Program.BoundingBox();
BoundingBox.Offset(Location);
}
/// <summary>
/// Updates the part from the drawing it was derived from.
/// </summary>
public void Update()
{
var rotation = Rotation;
Program = BaseDrawing.Program.Clone() as Program;
if (!Math.Tolerance.IsEqualTo(rotation, 0))
Program.Rotate(rotation);
HasManualLeadIns = false;
LeadInsLocked = false;
CuttingParameters = null;
UpdateBounds();
}
/// <summary>
/// The smallest box that contains the part.
/// </summary>
public Box BoundingBox { get; protected set; }
public bool Intersects(Part part, out List<Vector> pts)
{
PerfCounters.CountPartIntersects();
pts = new List<Vector>();
var entities1 = ConvertProgram
.ToGeometry(Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
var entities2 = ConvertProgram
.ToGeometry(part.Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
if (entities1.Count == 0 || entities2.Count == 0)
return false;
var perimeter1 = new ShapeProfile(entities1).Perimeter;
var perimeter2 = new ShapeProfile(entities2).Perimeter;
if (perimeter1 == null || perimeter2 == null)
return false;
var polygon1 = perimeter1.ToPolygonWithTolerance(IntersectsChordTolerance);
var polygon2 = perimeter2.ToPolygonWithTolerance(IntersectsChordTolerance);
if (polygon1 == null || polygon2 == null)
return false;
polygon1.Offset(Location);
polygon2.Offset(part.Location);
var result = Geometry.Collision.Check(polygon1, polygon2);
pts = result.IntersectionPoints.ToList();
return result.Overlaps;
}
public double Left
{
get { return BoundingBox.Left; }
}
public double Right
{
get { return BoundingBox.Right; }
}
public double Top
{
get { return BoundingBox.Top; }
}
public double Bottom
{
get { return BoundingBox.Bottom; }
}
/// <summary>
/// Gets a deep copy of the part.
/// </summary>
/// <returns></returns>
public object Clone()
{
// Clone the current Program directly rather than rebuilding from BaseDrawing and
// re-rotating by the absolute Rotation: when BaseDrawing.Program.Rotation is nonzero
// (e.g. a canonical-frame copy used internally during fill), `new Part(BaseDrawing)`
// already carries that baked rotation, so re-applying the full absolute Rotation on
// top of it double-counts the baseline and corrupts the clone's orientation.
var part = new Part(
BaseDrawing,
(Program)Program.Clone(),
Location,
new Box(BoundingBox.X, BoundingBox.Y, BoundingBox.Length, BoundingBox.Width)
);
part.ownsProgram = true;
return part;
}
/// <summary>
/// Creates an offset copy of the part. Clones from the already-rotated
/// program (skips re-rotation) and computes the bounding box arithmetically
/// (skips Program.BoundingBox walk).
/// </summary>
public Part CloneAtOffset(Vector offset)
{
// Share the Program instance — offset-only copies don't modify the program codes.
// This is a major performance win for tiling large patterns.
var part = new Part(
BaseDrawing,
Program,
location + offset,
new Box(
BoundingBox.X + offset.X,
BoundingBox.Y + offset.Y,
BoundingBox.Length,
BoundingBox.Width
)
);
return part;
}
private void EnsureOwnedProgram()
{
if (!ownsProgram)
{
Program = Program.Clone() as Program;
ownsProgram = true;
}
}
private Part(Drawing baseDrawing, Program program, Vector location, Box boundingBox)
{
BaseDrawing = baseDrawing;
Program = program;
this.location = location;
BoundingBox = boundingBox;
}
}
}