Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests pass after reformat, full solution builds 0 errors. Added .csharpierignore so csproj/config XML keeps its existing layout (CSharpier's XML wrapping churns attributes with zero benefit). Formatting is now enforceable: dotnet csharpier check . passes.
174 lines
5.8 KiB
C#
174 lines
5.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.RectanglePacking;
|
|
|
|
namespace OpenNest.Engine.Fill
|
|
{
|
|
public enum ShrinkAxis
|
|
{
|
|
Width,
|
|
Length,
|
|
}
|
|
|
|
public class ShrinkResult
|
|
{
|
|
public List<Part> Parts { get; set; }
|
|
public double Dimension { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fills a box and trims excess parts by removing those farthest from
|
|
/// the origin along the shrink axis.
|
|
/// </summary>
|
|
public static class ShrinkFiller
|
|
{
|
|
public static ShrinkResult Shrink(
|
|
Func<NestItem, Box, List<Part>> fillFunc,
|
|
NestItem item,
|
|
Box box,
|
|
double spacing,
|
|
ShrinkAxis axis,
|
|
CancellationToken token = default,
|
|
int targetCount = 0,
|
|
IProgress<NestProgress> progress = null,
|
|
int plateNumber = 0,
|
|
List<Part> placedParts = null
|
|
)
|
|
{
|
|
var startBox = box;
|
|
if (targetCount > 0)
|
|
startBox = EstimateStartBox(item, box, spacing, axis, targetCount);
|
|
|
|
var parts = fillFunc(item, startBox);
|
|
|
|
if (targetCount > 0 && startBox != box && (parts == null || parts.Count < targetCount))
|
|
{
|
|
parts = fillFunc(item, box);
|
|
}
|
|
|
|
if (parts == null || parts.Count == 0)
|
|
return new ShrinkResult { Parts = parts ?? new List<Part>(), Dimension = 0 };
|
|
|
|
var shrinkTarget =
|
|
targetCount > 0 ? System.Math.Min(targetCount, parts.Count) : parts.Count;
|
|
|
|
if (parts.Count > shrinkTarget)
|
|
parts = TrimToCount(parts, shrinkTarget, axis);
|
|
|
|
var dim = MeasureDimension(parts, box, axis);
|
|
|
|
ReportShrinkProgress(progress, plateNumber, placedParts, parts, box, axis, dim);
|
|
|
|
return new ShrinkResult { Parts = parts, Dimension = dim };
|
|
}
|
|
|
|
private static void ReportShrinkProgress(
|
|
IProgress<NestProgress> progress,
|
|
int plateNumber,
|
|
List<Part> placedParts,
|
|
List<Part> bestParts,
|
|
Box workArea,
|
|
ShrinkAxis axis,
|
|
double dim
|
|
)
|
|
{
|
|
if (progress == null)
|
|
return;
|
|
|
|
var allParts =
|
|
placedParts != null && placedParts.Count > 0
|
|
? new List<Part>(placedParts.Count + bestParts.Count)
|
|
: new List<Part>(bestParts.Count);
|
|
|
|
if (placedParts != null && placedParts.Count > 0)
|
|
allParts.AddRange(placedParts);
|
|
allParts.AddRange(bestParts);
|
|
|
|
var desc = $"Shrink {axis}: {bestParts.Count} parts, dim={dim:F1}";
|
|
|
|
NestEngineBase.ReportProgress(
|
|
progress,
|
|
new ProgressReport
|
|
{
|
|
Phase = NestPhase.Custom,
|
|
PlateNumber = plateNumber,
|
|
Parts = allParts,
|
|
WorkArea = workArea,
|
|
Description = desc,
|
|
}
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses FillBestFit (fast rectangle packing) to estimate a starting box
|
|
/// that fits roughly the target count. Scales the shrink axis proportionally
|
|
/// from the full-area count down to the target, with margin.
|
|
/// </summary>
|
|
internal static Box EstimateStartBox(
|
|
NestItem item,
|
|
Box box,
|
|
double spacing,
|
|
ShrinkAxis axis,
|
|
int targetCount,
|
|
double marginFactor = 1.3
|
|
)
|
|
{
|
|
var bbox = item.Drawing.Program.BoundingBox();
|
|
if (bbox.Width <= 0 || bbox.Length <= 0)
|
|
return box;
|
|
|
|
var maxDim = axis == ShrinkAxis.Length ? box.Length : box.Width;
|
|
|
|
// Use FillBestFit for a fast, accurate rectangle count on the full box.
|
|
var bin = new Bin { Size = new Size(box.Width, box.Length) };
|
|
var packItem = new Item
|
|
{
|
|
Size = new Size(bbox.Width + spacing, bbox.Length + spacing),
|
|
};
|
|
var packer = new FillBestFit(bin);
|
|
packer.Fill(packItem);
|
|
var fullCount = bin.Items.Count;
|
|
|
|
if (fullCount <= 0 || fullCount <= targetCount)
|
|
return box;
|
|
|
|
// Scale dimension proportionally: target/full * maxDim, with margin.
|
|
var ratio = (double)targetCount / fullCount;
|
|
var estimate = maxDim * ratio * marginFactor;
|
|
estimate = System.Math.Min(estimate, maxDim);
|
|
|
|
if (estimate <= 0 || estimate >= maxDim)
|
|
return box;
|
|
|
|
return axis == ShrinkAxis.Length
|
|
? new Box(box.X, box.Y, box.Width, estimate)
|
|
: new Box(box.X, box.Y, estimate, box.Length);
|
|
}
|
|
|
|
private static double MeasureDimension(List<Part> parts, Box box, ShrinkAxis axis)
|
|
{
|
|
var placedBox = parts.Cast<IBoundable>().GetBoundingBox();
|
|
|
|
return axis == ShrinkAxis.Width ? placedBox.Right - box.X : placedBox.Top - box.Y;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Keeps the <paramref name="targetCount"/> parts nearest to the origin
|
|
/// along the given axis, discarding parts farthest from the origin.
|
|
/// Returns the input list unchanged if count is already at or below target.
|
|
/// </summary>
|
|
internal static List<Part> TrimToCount(List<Part> parts, int targetCount, ShrinkAxis axis)
|
|
{
|
|
if (parts == null || parts.Count <= targetCount)
|
|
return parts;
|
|
|
|
return axis == ShrinkAxis.Width
|
|
? parts.OrderBy(p => p.BoundingBox.Right).Take(targetCount).ToList()
|
|
: parts.OrderBy(p => p.BoundingBox.Top).Take(targetCount).ToList();
|
|
}
|
|
}
|
|
}
|