feat: geometry-aware convergence, both-axis search, remnant engine, fill cache
- Convergence loop now uses FillLinear internally to measure actual waste with geometry-aware spacing instead of bounding-box arithmetic - Each candidate pair is tried in both Row and Column orientations to find the shortest perpendicular dimension (more complete stripes) - CompleteStripesOnly flag drops partial stripes; remnant strip is filled by a full engine run (injected via CreateRemnantEngine) - ConvergeStripeAngleShrink tries N+1 narrower pairs as alternative - FillResultCache avoids redundant engine runs on same-sized remnants - CLAUDE.md: note to not commit specs/plans Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -100,6 +100,8 @@ Always use Roslyn Bridge MCP tools (`mcp__RoslynBridge__*`) as the primary metho
|
|||||||
|
|
||||||
Always keep `README.md` and `CLAUDE.md` up to date when making changes that affect project structure, architecture, build instructions, dependencies, or key patterns. If you add a new project, change a namespace, modify the build process, or alter significant behavior, update both files as part of the same change.
|
Always keep `README.md` and `CLAUDE.md` up to date when making changes that affect project structure, architecture, build instructions, dependencies, or key patterns. If you add a new project, change a namespace, modify the build process, or alter significant behavior, update both files as part of the same change.
|
||||||
|
|
||||||
|
**Do not commit** design specs, implementation plans, or other temporary planning documents (`docs/superpowers/` etc.) to the repository. These are working documents only — keep them local and untracked.
|
||||||
|
|
||||||
## Key Patterns
|
## Key Patterns
|
||||||
|
|
||||||
- OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`.
|
- OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`.
|
||||||
|
|||||||
97
OpenNest.Engine/Fill/FillResultCache.cs
Normal file
97
OpenNest.Engine/Fill/FillResultCache.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Fill;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Caches fill results by drawing and box dimensions so repeated fills
|
||||||
|
/// of the same size don't recompute. Parts are stored normalized to origin
|
||||||
|
/// and offset to the actual location on retrieval.
|
||||||
|
/// </summary>
|
||||||
|
public static class FillResultCache
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<CacheKey, List<Part>> _cache = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a cached fill result for the given drawing and box dimensions,
|
||||||
|
/// offset to the target location. Returns null on cache miss.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Part> Get(Drawing drawing, Box targetBox, double spacing)
|
||||||
|
{
|
||||||
|
var key = new CacheKey(drawing, targetBox.Width, targetBox.Length, spacing);
|
||||||
|
|
||||||
|
if (!_cache.TryGetValue(key, out var cached) || cached.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var offset = targetBox.Location;
|
||||||
|
var result = new List<Part>(cached.Count);
|
||||||
|
|
||||||
|
foreach (var part in cached)
|
||||||
|
result.Add(part.CloneAtOffset(offset));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores a fill result normalized to origin (0,0).
|
||||||
|
/// </summary>
|
||||||
|
public static void Store(Drawing drawing, Box sourceBox, double spacing, List<Part> parts)
|
||||||
|
{
|
||||||
|
if (parts == null || parts.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var key = new CacheKey(drawing, sourceBox.Width, sourceBox.Length, spacing);
|
||||||
|
|
||||||
|
if (_cache.ContainsKey(key))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var offset = new Vector(-sourceBox.X, -sourceBox.Y);
|
||||||
|
var normalized = new List<Part>(parts.Count);
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
normalized.Add(part.CloneAtOffset(offset));
|
||||||
|
|
||||||
|
_cache.TryAdd(key, normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Clear() => _cache.Clear();
|
||||||
|
|
||||||
|
public static int Count => _cache.Count;
|
||||||
|
|
||||||
|
private readonly struct CacheKey : System.IEquatable<CacheKey>
|
||||||
|
{
|
||||||
|
public readonly Drawing Drawing;
|
||||||
|
public readonly double Width;
|
||||||
|
public readonly double Height;
|
||||||
|
public readonly double Spacing;
|
||||||
|
|
||||||
|
public CacheKey(Drawing drawing, double width, double height, double spacing)
|
||||||
|
{
|
||||||
|
Drawing = drawing;
|
||||||
|
Width = System.Math.Round(width, 2);
|
||||||
|
Height = System.Math.Round(height, 2);
|
||||||
|
Spacing = spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(CacheKey other) =>
|
||||||
|
ReferenceEquals(Drawing, other.Drawing) &&
|
||||||
|
Width == other.Width && Height == other.Height &&
|
||||||
|
Spacing == other.Spacing;
|
||||||
|
|
||||||
|
public override bool Equals(object obj) => obj is CacheKey other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
var hash = RuntimeHelpers.GetHashCode(Drawing);
|
||||||
|
hash = hash * 397 ^ Width.GetHashCode();
|
||||||
|
hash = hash * 397 ^ Height.GetHashCode();
|
||||||
|
hash = hash * 397 ^ Spacing.GetHashCode();
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -18,6 +19,18 @@ public class StripeFiller
|
|||||||
private readonly FillContext _context;
|
private readonly FillContext _context;
|
||||||
private readonly NestDirection _primaryAxis;
|
private readonly NestDirection _primaryAxis;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, only complete stripes are placed — no partial rows/columns.
|
||||||
|
/// </summary>
|
||||||
|
public bool CompleteStripesOnly { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory to create the engine used for filling the remnant strip.
|
||||||
|
/// Defaults to NestEngineRegistry.Create (uses the user's selected engine).
|
||||||
|
/// </summary>
|
||||||
|
public Func<Plate, NestEngineBase> CreateRemnantEngine { get; set; }
|
||||||
|
= NestEngineRegistry.Create;
|
||||||
|
|
||||||
public StripeFiller(FillContext context, NestDirection primaryAxis)
|
public StripeFiller(FillContext context, NestDirection primaryAxis)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
@@ -33,10 +46,6 @@ public class StripeFiller
|
|||||||
var workArea = _context.WorkArea;
|
var workArea = _context.WorkArea;
|
||||||
var spacing = _context.Plate.PartSpacing;
|
var spacing = _context.Plate.PartSpacing;
|
||||||
var drawing = _context.Item.Drawing;
|
var drawing = _context.Item.Drawing;
|
||||||
var perpAxis = _primaryAxis == NestDirection.Horizontal
|
|
||||||
? NestDirection.Vertical
|
|
||||||
: NestDirection.Horizontal;
|
|
||||||
var sheetSpan = GetDimension(workArea, _primaryAxis);
|
|
||||||
var strategyName = _primaryAxis == NestDirection.Horizontal ? "Row" : "Column";
|
var strategyName = _primaryAxis == NestDirection.Horizontal ? "Row" : "Column";
|
||||||
|
|
||||||
List<Part> bestParts = null;
|
List<Part> bestParts = null;
|
||||||
@@ -49,25 +58,34 @@ public class StripeFiller
|
|||||||
var candidate = bestFits[i];
|
var candidate = bestFits[i];
|
||||||
var pairParts = candidate.BuildParts(drawing);
|
var pairParts = candidate.BuildParts(drawing);
|
||||||
|
|
||||||
// Try both expand (N wider pairs) and shrink (N+1 narrower pairs)
|
// Try both directions for each candidate to find the shortest
|
||||||
var expandResult = ConvergeStripeAngle(
|
// perpendicular dimension (more complete stripes → larger remnant)
|
||||||
pairParts, sheetSpan, spacing, _primaryAxis, _context.Token);
|
foreach (var axis in new[] { NestDirection.Horizontal, NestDirection.Vertical })
|
||||||
var shrinkResult = ConvergeStripeAngleShrink(
|
{
|
||||||
pairParts, sheetSpan, spacing, _primaryAxis, _context.Token);
|
var perpAxis = axis == NestDirection.Horizontal
|
||||||
|
? NestDirection.Vertical : NestDirection.Horizontal;
|
||||||
|
var sheetSpan = GetDimension(workArea, axis);
|
||||||
|
var dirLabel = axis == NestDirection.Horizontal ? "Row" : "Col";
|
||||||
|
|
||||||
|
var expandResult = ConvergeStripeAngle(
|
||||||
|
pairParts, sheetSpan, spacing, axis, _context.Token);
|
||||||
|
var shrinkResult = ConvergeStripeAngleShrink(
|
||||||
|
pairParts, sheetSpan, spacing, axis, _context.Token);
|
||||||
|
|
||||||
// Evaluate both convergence results
|
|
||||||
foreach (var (angle, waste, count) in new[] { expandResult, shrinkResult })
|
foreach (var (angle, waste, count) in new[] { expandResult, shrinkResult })
|
||||||
{
|
{
|
||||||
if (count <= 0)
|
if (count <= 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var result = BuildGrid(pairParts, angle, workArea, spacing, perpAxis, drawing);
|
var result = BuildGrid(pairParts, angle, workArea, spacing, axis, perpAxis, drawing);
|
||||||
|
|
||||||
if (result == null || result.Count == 0)
|
if (result == null || result.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var score = FillScore.Compute(result, workArea);
|
var score = FillScore.Compute(result, workArea);
|
||||||
Debug.WriteLine($"[StripeFiller] {strategyName} candidate {i}: angle={Angle.ToDegrees(angle):F1}°, " +
|
Debug.WriteLine($"[StripeFiller] {strategyName} candidate {i} {dirLabel}: " +
|
||||||
$"pairsAcross={count}, waste={waste:F2}, grid={result.Count} parts");
|
$"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, " +
|
||||||
|
$"grid={result.Count} parts");
|
||||||
|
|
||||||
if (bestParts == null || score > bestScore)
|
if (bestParts == null || score > bestScore)
|
||||||
{
|
{
|
||||||
@@ -75,6 +93,7 @@ public class StripeFiller
|
|||||||
bestScore = score;
|
bestScore = score;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NestEngineBase.ReportProgress(_context.Progress, NestPhase.Custom,
|
NestEngineBase.ReportProgress(_context.Progress, NestPhase.Custom,
|
||||||
_context.PlateNumber, bestParts, workArea,
|
_context.PlateNumber, bestParts, workArea,
|
||||||
@@ -85,18 +104,21 @@ public class StripeFiller
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<Part> BuildGrid(List<Part> pairParts, double angle,
|
private List<Part> BuildGrid(List<Part> pairParts, double angle,
|
||||||
Box workArea, double spacing, NestDirection perpAxis, Drawing drawing)
|
Box workArea, double spacing, NestDirection primaryAxis,
|
||||||
|
NestDirection perpAxis, Drawing drawing)
|
||||||
{
|
{
|
||||||
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
||||||
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
|
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
|
||||||
var stripeBox = MakeStripeBox(workArea, perpDim, _primaryAxis);
|
var stripeBox = MakeStripeBox(workArea, perpDim, primaryAxis);
|
||||||
var stripeEngine = new FillLinear(stripeBox, spacing);
|
var stripeEngine = new FillLinear(stripeBox, spacing);
|
||||||
var stripeParts = stripeEngine.Fill(rotatedPattern, _primaryAxis);
|
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
|
||||||
|
|
||||||
if (stripeParts == null || stripeParts.Count == 0)
|
if (stripeParts == null || stripeParts.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
Debug.WriteLine($"[StripeFiller] Stripe: {stripeParts.Count} parts, " +
|
var partsPerStripe = stripeParts.Count;
|
||||||
|
|
||||||
|
Debug.WriteLine($"[StripeFiller] Stripe: {partsPerStripe} parts, " +
|
||||||
$"box={stripeBox.Width:F2}x{stripeBox.Length:F2}");
|
$"box={stripeBox.Width:F2}x{stripeBox.Length:F2}");
|
||||||
|
|
||||||
var stripePattern = new Pattern();
|
var stripePattern = new Pattern();
|
||||||
@@ -109,10 +131,26 @@ public class StripeFiller
|
|||||||
if (gridParts == null || gridParts.Count == 0)
|
if (gridParts == null || gridParts.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
if (CompleteStripesOnly)
|
||||||
|
{
|
||||||
|
// Keep only complete stripes — discard partial copies
|
||||||
|
var completeCount = gridParts.Count / partsPerStripe * partsPerStripe;
|
||||||
|
if (completeCount < gridParts.Count)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[StripeFiller] CompleteOnly: {gridParts.Count} → {completeCount} " +
|
||||||
|
$"(dropped {gridParts.Count - completeCount} partial)");
|
||||||
|
gridParts = gridParts.GetRange(0, completeCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Debug.WriteLine($"[StripeFiller] Grid: {gridParts.Count} parts");
|
Debug.WriteLine($"[StripeFiller] Grid: {gridParts.Count} parts");
|
||||||
|
|
||||||
|
if (gridParts.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
var allParts = new List<Part>(gridParts);
|
var allParts = new List<Part>(gridParts);
|
||||||
var remnantParts = FillRemnant(gridParts, drawing, angle, workArea, spacing);
|
|
||||||
|
var remnantParts = FillRemnant(gridParts, drawing, angle, primaryAxis, workArea, spacing);
|
||||||
if (remnantParts != null)
|
if (remnantParts != null)
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"[StripeFiller] Remnant: {remnantParts.Count} parts");
|
Debug.WriteLine($"[StripeFiller] Remnant: {remnantParts.Count} parts");
|
||||||
@@ -150,7 +188,7 @@ public class StripeFiller
|
|||||||
|
|
||||||
private List<Part> FillRemnant(
|
private List<Part> FillRemnant(
|
||||||
List<Part> gridParts, Drawing drawing, double angle,
|
List<Part> gridParts, Drawing drawing, double angle,
|
||||||
Box workArea, double spacing)
|
NestDirection primaryAxis, Box workArea, double spacing)
|
||||||
{
|
{
|
||||||
var gridBox = gridParts.GetBoundingBox();
|
var gridBox = gridParts.GetBoundingBox();
|
||||||
var minDim = System.Math.Min(
|
var minDim = System.Math.Min(
|
||||||
@@ -159,7 +197,7 @@ public class StripeFiller
|
|||||||
|
|
||||||
Box remnantBox;
|
Box remnantBox;
|
||||||
|
|
||||||
if (_primaryAxis == NestDirection.Horizontal)
|
if (primaryAxis == NestDirection.Horizontal)
|
||||||
{
|
{
|
||||||
var remnantY = gridBox.Top + spacing;
|
var remnantY = gridBox.Top + spacing;
|
||||||
var remnantLength = workArea.Top - remnantY;
|
var remnantLength = workArea.Top - remnantY;
|
||||||
@@ -176,9 +214,39 @@ public class StripeFiller
|
|||||||
remnantBox = new Box(remnantX, workArea.Y, remnantWidth, workArea.Length);
|
remnantBox = new Box(remnantX, workArea.Y, remnantWidth, workArea.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
var engine = new FillLinear(remnantBox, spacing);
|
Debug.WriteLine($"[StripeFiller] Remnant box: {remnantBox.Width:F2}x{remnantBox.Length:F2}");
|
||||||
var parts = engine.Fill(drawing, angle, _primaryAxis);
|
|
||||||
return parts != null && parts.Count > 0 ? parts : null;
|
// Check cache first
|
||||||
|
var cached = FillResultCache.Get(drawing, remnantBox, spacing);
|
||||||
|
if (cached != null)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[StripeFiller] Remnant CACHE HIT: {cached.Count} parts");
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude Row/Column from remnant fill to prevent recursion
|
||||||
|
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var engine = CreateRemnantEngine(_context.Plate);
|
||||||
|
var item = new NestItem { Drawing = drawing };
|
||||||
|
var parts = engine.Fill(item, remnantBox, _context.Progress, _context.Token);
|
||||||
|
|
||||||
|
Debug.WriteLine($"[StripeFiller] Remnant engine ({engine.Name}): {parts?.Count ?? 0} parts, " +
|
||||||
|
$"winner={engine.WinnerPhase}");
|
||||||
|
|
||||||
|
if (parts != null && parts.Count > 0)
|
||||||
|
{
|
||||||
|
FillResultCache.Store(drawing, remnantBox, spacing, parts);
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
FillStrategyRegistry.SetEnabled(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double FindAngleForTargetSpan(
|
public static double FindAngleForTargetSpan(
|
||||||
@@ -261,23 +329,39 @@ public class StripeFiller
|
|||||||
var bestCount = 0;
|
var bestCount = 0;
|
||||||
var tolerance = sheetSpan * 0.001;
|
var tolerance = sheetSpan * 0.001;
|
||||||
|
|
||||||
|
Debug.WriteLine($"[Converge] Start: orient={Angle.ToDegrees(currentAngle):F1}°, sheetSpan={sheetSpan:F2}, spacing={spacing}");
|
||||||
|
|
||||||
for (var iteration = 0; iteration < MaxConvergenceIterations; iteration++)
|
for (var iteration = 0; iteration < MaxConvergenceIterations; iteration++)
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var rotated = FillHelpers.BuildRotatedPattern(patternParts, currentAngle);
|
var rotated = FillHelpers.BuildRotatedPattern(patternParts, currentAngle);
|
||||||
var pairSpan = GetDimension(rotated.BoundingBox, axis);
|
var pairSpan = GetDimension(rotated.BoundingBox, axis);
|
||||||
|
var perpDim = axis == NestDirection.Horizontal
|
||||||
|
? rotated.BoundingBox.Length : rotated.BoundingBox.Width;
|
||||||
|
|
||||||
if (pairSpan + spacing <= 0)
|
if (pairSpan + spacing <= 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var n = (int)System.Math.Floor((sheetSpan + spacing) / (pairSpan + spacing));
|
// Use FillLinear to get the ACTUAL part count with geometry-aware spacing
|
||||||
|
var stripeBox = axis == NestDirection.Horizontal
|
||||||
|
? new Box(0, 0, sheetSpan, perpDim)
|
||||||
|
: new Box(0, 0, perpDim, sheetSpan);
|
||||||
|
var engine = new FillLinear(stripeBox, spacing);
|
||||||
|
var filled = engine.Fill(rotated, axis);
|
||||||
|
var n = filled?.Count ?? 0;
|
||||||
|
|
||||||
if (n <= 0)
|
if (n <= 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var usedSpan = n * (pairSpan + spacing) - spacing;
|
// Measure actual waste from the placed parts
|
||||||
|
var filledBox = ((IEnumerable<IBoundable>)filled).GetBoundingBox();
|
||||||
|
var usedSpan = GetDimension(filledBox, axis);
|
||||||
var remaining = sheetSpan - usedSpan;
|
var remaining = sheetSpan - usedSpan;
|
||||||
|
|
||||||
|
Debug.WriteLine($"[Converge] iter={iteration}: angle={Angle.ToDegrees(currentAngle):F2}°, " +
|
||||||
|
$"pairSpan={pairSpan:F4}, perpDim={perpDim:F4}, N={n}, waste={remaining:F3}");
|
||||||
|
|
||||||
if (remaining < bestWaste)
|
if (remaining < bestWaste)
|
||||||
{
|
{
|
||||||
bestWaste = remaining;
|
bestWaste = remaining;
|
||||||
@@ -288,12 +372,27 @@ public class StripeFiller
|
|||||||
if (remaining <= tolerance)
|
if (remaining <= tolerance)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var delta = remaining / n;
|
// Estimate pairs from bounding box to compute delta
|
||||||
|
var bboxN = (int)System.Math.Floor((sheetSpan + spacing) / (pairSpan + spacing));
|
||||||
|
if (bboxN <= 0) bboxN = 1;
|
||||||
|
var delta = remaining / bboxN;
|
||||||
var targetSpan = pairSpan + delta;
|
var targetSpan = pairSpan + delta;
|
||||||
|
|
||||||
|
Debug.WriteLine($"[Converge] delta={delta:F4}, targetSpan={targetSpan:F4}");
|
||||||
|
|
||||||
|
var prevAngle = currentAngle;
|
||||||
currentAngle = FindAngleForTargetSpan(patternParts, targetSpan, axis);
|
currentAngle = FindAngleForTargetSpan(patternParts, targetSpan, axis);
|
||||||
|
|
||||||
|
Debug.WriteLine($"[Converge] newAngle={Angle.ToDegrees(currentAngle):F2}° (was {Angle.ToDegrees(prevAngle):F2}°)");
|
||||||
|
|
||||||
|
if (System.Math.Abs(currentAngle - prevAngle) < Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[Converge] STUCK — angle unchanged, breaking");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[Converge] Result: angle={Angle.ToDegrees(bestAngle):F2}°, N={bestCount}, waste={bestWaste:F3}");
|
||||||
return (bestAngle, bestWaste, bestCount);
|
return (bestAngle, bestWaste, bestCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,7 +435,7 @@ public class StripeFiller
|
|||||||
var usedSpan = actualN * (actualSpan + spacing) - spacing;
|
var usedSpan = actualN * (actualSpan + spacing) - spacing;
|
||||||
var waste = sheetSpan - usedSpan;
|
var waste = sheetSpan - usedSpan;
|
||||||
|
|
||||||
// Now converge from this starting point
|
// Now converge from this starting point using geometry-aware fill
|
||||||
var bestWaste = waste;
|
var bestWaste = waste;
|
||||||
var bestAngle = angle;
|
var bestAngle = angle;
|
||||||
var bestCount = actualN;
|
var bestCount = actualN;
|
||||||
@@ -352,13 +451,21 @@ public class StripeFiller
|
|||||||
|
|
||||||
rotated = FillHelpers.BuildRotatedPattern(patternParts, currentAngle);
|
rotated = FillHelpers.BuildRotatedPattern(patternParts, currentAngle);
|
||||||
var pairSpan = GetDimension(rotated.BoundingBox, axis);
|
var pairSpan = GetDimension(rotated.BoundingBox, axis);
|
||||||
|
var perpDim = axis == NestDirection.Horizontal
|
||||||
|
? rotated.BoundingBox.Length : rotated.BoundingBox.Width;
|
||||||
|
|
||||||
|
var stripeBox = axis == NestDirection.Horizontal
|
||||||
|
? new Box(0, 0, sheetSpan, perpDim)
|
||||||
|
: new Box(0, 0, perpDim, sheetSpan);
|
||||||
|
var engine = new FillLinear(stripeBox, spacing);
|
||||||
|
var filled = engine.Fill(rotated, axis);
|
||||||
|
var n = filled?.Count ?? 0;
|
||||||
|
|
||||||
var n = (int)System.Math.Floor((sheetSpan + spacing) / (pairSpan + spacing));
|
|
||||||
if (n <= 0)
|
if (n <= 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
usedSpan = n * (pairSpan + spacing) - spacing;
|
var filledBox = ((IEnumerable<IBoundable>)filled).GetBoundingBox();
|
||||||
var remaining = sheetSpan - usedSpan;
|
var remaining = sheetSpan - GetDimension(filledBox, axis);
|
||||||
|
|
||||||
if (remaining < bestWaste)
|
if (remaining < bestWaste)
|
||||||
{
|
{
|
||||||
@@ -370,7 +477,9 @@ public class StripeFiller
|
|||||||
if (remaining <= tolerance)
|
if (remaining <= tolerance)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var delta = remaining / n;
|
var bboxN = (int)System.Math.Floor((sheetSpan + spacing) / (pairSpan + spacing));
|
||||||
|
if (bboxN <= 0) bboxN = 1;
|
||||||
|
var delta = remaining / bboxN;
|
||||||
var newTarget = pairSpan + delta;
|
var newTarget = pairSpan + delta;
|
||||||
|
|
||||||
currentAngle = FindAngleForTargetSpan(patternParts, newTarget, axis);
|
currentAngle = FindAngleForTargetSpan(patternParts, newTarget, axis);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class ColumnFillStrategy : IFillStrategy
|
|||||||
|
|
||||||
public List<Part> Fill(FillContext context)
|
public List<Part> Fill(FillContext context)
|
||||||
{
|
{
|
||||||
var filler = new StripeFiller(context, NestDirection.Vertical);
|
var filler = new StripeFiller(context, NestDirection.Vertical) { CompleteStripesOnly = true };
|
||||||
return filler.Fill();
|
return filler.Fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class RowFillStrategy : IFillStrategy
|
|||||||
|
|
||||||
public List<Part> Fill(FillContext context)
|
public List<Part> Fill(FillContext context)
|
||||||
{
|
{
|
||||||
var filler = new StripeFiller(context, NestDirection.Horizontal);
|
var filler = new StripeFiller(context, NestDirection.Horizontal) { CompleteStripesOnly = true };
|
||||||
return filler.Fill();
|
return filler.Fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user