Part.Rotation is cumulative, so rebinding canonical parts with CreateAtOrigin(original, p.Rotation) double-counted the drawing's own rotation, and FromCanonical rotated each part about its Location, which moved it off its slot and out of the work area. Add CanonicalFrame.RebindToOriginal (rotation = part - original program rotation, footprint aligned to the canonical part) and use it in the three places that duplicated the old logic. The MBR only fixes the frame modulo 90 degrees and nest results are not 90-degree symmetric (an L gave 56/43/42/42 parts by orientation). CanonicalAngle.Compute now picks one of the four orientations from the centroid offset; symmetric shapes keep the MBR orientation. Fixes the three NestInvarianceTests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
129 lines
3.7 KiB
C#
129 lines
3.7 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Engine;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest.Engine.BestFit
|
|
{
|
|
public class BestFitResult
|
|
{
|
|
public PairCandidate Candidate { get; set; }
|
|
public double RotatedArea { get; set; }
|
|
public double BoundingWidth { get; set; }
|
|
public double BoundingHeight { get; set; }
|
|
public double OptimalRotation { get; set; }
|
|
public bool Keep { get; set; }
|
|
public string Reason { get; set; }
|
|
public double TrueArea { get; set; }
|
|
public List<double> HullAngles { get; set; }
|
|
|
|
public double Utilization
|
|
{
|
|
get { return RotatedArea > 0 ? TrueArea / RotatedArea : 0; }
|
|
}
|
|
|
|
public double LongestSide
|
|
{
|
|
get { return System.Math.Max(BoundingWidth, BoundingHeight); }
|
|
}
|
|
|
|
public double ShortestSide
|
|
{
|
|
get { return System.Math.Min(BoundingWidth, BoundingHeight); }
|
|
}
|
|
|
|
public List<Part> BuildParts(Drawing drawing)
|
|
{
|
|
var part1 = Part.CreateAtOrigin(drawing);
|
|
|
|
var part2 = Part.CreateAtOrigin(drawing, Candidate.Part2Rotation);
|
|
part2.Location = Candidate.Part2Offset;
|
|
part2.UpdateBounds();
|
|
|
|
if (!OptimalRotation.IsEqualTo(0))
|
|
{
|
|
var pairBounds = (
|
|
(IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }
|
|
).GetBoundingBox();
|
|
var center = pairBounds.Center;
|
|
part1.Rotate(-OptimalRotation, center);
|
|
part2.Rotate(-OptimalRotation, center);
|
|
}
|
|
|
|
var finalBounds = (
|
|
(IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }
|
|
).GetBoundingBox();
|
|
var offset = new Vector(-finalBounds.Left, -finalBounds.Bottom);
|
|
part1.Offset(offset);
|
|
part2.Offset(offset);
|
|
|
|
return new List<Part> { part1, part2 };
|
|
}
|
|
|
|
public List<Part> BuildCanonicalParts()
|
|
{
|
|
return NormalizeToCutOrigin(BuildParts(Candidate.Drawing));
|
|
}
|
|
|
|
public List<Part> BuildSourceParts(Drawing drawing)
|
|
{
|
|
var parts = BuildCanonicalParts();
|
|
|
|
return NormalizeToCutOrigin(CanonicalFrame.RebindToOriginal(parts, drawing));
|
|
}
|
|
|
|
public Box GetCutBounds(List<Part> parts)
|
|
{
|
|
return GetCutBoundingBox(parts);
|
|
}
|
|
|
|
private static List<Part> NormalizeToCutOrigin(List<Part> parts)
|
|
{
|
|
if (parts == null || parts.Count == 0)
|
|
return parts;
|
|
|
|
var bounds = GetCutBoundingBox(parts);
|
|
var offset = new Vector(-bounds.Left, -bounds.Bottom);
|
|
|
|
foreach (var part in parts)
|
|
part.Offset(offset);
|
|
|
|
return parts;
|
|
}
|
|
|
|
private static Box GetCutBoundingBox(List<Part> parts)
|
|
{
|
|
var entities = new List<IBoundable>();
|
|
|
|
foreach (var part in parts)
|
|
{
|
|
var partEntities = ConvertProgram
|
|
.ToGeometry(part.Program)
|
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
|
.ToList();
|
|
|
|
foreach (var entity in partEntities)
|
|
{
|
|
entity.Offset(part.Location);
|
|
entities.Add(entity);
|
|
}
|
|
}
|
|
|
|
return entities.GetBoundingBox();
|
|
}
|
|
}
|
|
|
|
public enum BestFitSortField
|
|
{
|
|
Area,
|
|
LongestSide,
|
|
ShortestSide,
|
|
Type,
|
|
OriginalSequence,
|
|
Keep,
|
|
WhyKeepDrop,
|
|
}
|
|
}
|