merge: resolve conflicts from remote nesting progress changes

Kept using OpenNest.Api in Timing.cs and EditNestForm.cs alongside
remote's reorganized usings and namespace changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 09:35:25 -04:00
211 changed files with 4751 additions and 1632 deletions

View File

@@ -37,16 +37,15 @@ Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the
- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`.
- **NestEngineRegistry**: Static registry — `Create(Plate)` factory, `ActiveEngineName` global selection, `LoadPlugins(directory)` for DLL discovery. All callsites use `NestEngineRegistry.Create(plate)` except `BruteForceRunner` which uses `new DefaultNestEngine(plate)` directly for training consistency.
- **BestFit/**: NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups.
- **RectanglePacking/**: `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions.
- **CirclePacking/**: Alternative packing for circular parts.
- **ML/**: `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data).
- `FillLinear`: Grid-based fill with directional sliding.
- `Compactor`: Post-fill gravity compaction — pushes parts toward a plate edge to close gaps.
- `FillScore`: Lexicographic comparison struct for fill results (count > utilization > compactness).
- **Fill/** (`namespace OpenNest.Engine.Fill`): Fill algorithms — `FillLinear` (grid-based), `FillExtents` (extents-based pair tiling), `PairFiller` (interlocking pairs), `ShrinkFiller`, `RemnantFiller`/`RemnantFinder`, `Compactor` (post-fill gravity compaction), `FillScore` (lexicographic comparison: count > utilization > compactness), `Pattern`/`PatternTiler`, `PartBoundary`, `RotationAnalysis`, `AngleCandidateBuilder`, `BestCombination`, `AccumulatingProgress`.
- **Strategies/** (`namespace OpenNest.Engine.Strategies`): Pluggable fill strategy layer — `IFillStrategy` interface, `FillContext`, `FillStrategyRegistry` (auto-discovers strategies via reflection, supports plugin DLLs), `FillHelpers`. Built-in strategies: `LinearFillStrategy`, `PairsFillStrategy`, `RectBestFitStrategy`, `ExtentsFillStrategy`.
- **BestFit/** (`namespace OpenNest.Engine.BestFit`): NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups.
- **RectanglePacking/** (`namespace OpenNest.RectanglePacking`): `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions.
- **CirclePacking/** (`namespace OpenNest.CirclePacking`): Alternative packing for circular parts.
- **Nfp/** (`namespace OpenNest.Engine.Nfp`): NFP-based nesting (not yet integrated) — `AutoNester` (mixed-part nesting with simulated annealing), `BottomLeftFill` (BLF placement), `NfpCache` (computed NFP caching), `SimulatedAnnealing` (optimizer), `INestOptimizer`/`NestResult`.
- **ML/** (`namespace OpenNest.Engine.ML`): `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data).
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
- `NestProgress`: Progress reporting model with `NestPhase` enum for UI feedback.
- `RotationAnalysis`: Analyzes part geometry to determine valid rotation angles.
### OpenNest.IO (class library, depends on Core)
File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
@@ -99,9 +98,14 @@ Always use Roslyn Bridge MCP tools (`mcp__RoslynBridge__*`) as the primary metho
- Always use `var` instead of explicit types (e.g., `var parts = new List<Part>();` not `List<Part> parts = new List<Part>();`).
## Documentation Maintenance
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.
## Key Patterns
- OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`.
- OpenNest.Engine uses sub-namespaces: `OpenNest.Engine.Fill` (fill algorithms), `OpenNest.Engine.Strategies` (pluggable strategy layer), `OpenNest.Engine.BestFit`, `OpenNest.Engine.Nfp` (NFP-based nesting, not yet integrated), `OpenNest.Engine.ML`, `OpenNest.Engine.RapidPlanning`, `OpenNest.Engine.Sequencing`.
- `ObservableList<T>` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings.
- Angles throughout the codebase are in **radians** (use `Angle.ToRadians()`/`Angle.ToDegrees()` for conversion).
- `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations.

View File

@@ -1,13 +1,13 @@
using OpenNest;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using OpenNest;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
return NestConsole.Run(args);

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,4 +1,3 @@
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{

View File

@@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC
{

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic;
using OpenNest;
using OpenNest.CNC;
using OpenNest.CNC;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Converters
{

View File

@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using OpenNest;
using OpenNest.CNC;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Converters
{

View File

@@ -1,9 +1,9 @@
using System.Drawing;
using System.Linq;
using System.Threading;
using OpenNest.CNC;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
using System.Drawing;
using System.Linq;
using System.Threading;
namespace OpenNest
{

View File

@@ -8,10 +8,10 @@
public int Remaining
{
get
get
{
var x = Required - Nested;
return x < 0 ? 0: x;
return x < 0 ? 0 : x;
}
}
}

View File

@@ -1,6 +1,6 @@
using System;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{

View File

@@ -1,6 +1,5 @@
using System;
using OpenNest.Math;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Math;
using System.Collections.Generic;
using System.Drawing;
using OpenNest.Math;
namespace OpenNest.Geometry
{

View File

@@ -1,7 +1,7 @@
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OpenNest.Math;
namespace OpenNest.Geometry
{

View File

@@ -1,6 +1,6 @@
using OpenNest.Math;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{

View File

@@ -1,6 +1,6 @@
using System;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using Clipper2Lib;
using System.Collections.Generic;
namespace OpenNest.Geometry
{

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
namespace OpenNest.Geometry

View File

@@ -1,7 +1,7 @@
using System;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Geometry
{

View File

@@ -1,6 +1,6 @@
using OpenNest.Math;
using System.Collections.Generic;
using System.Diagnostics;
using OpenNest.Math;
namespace OpenNest.Geometry
{

View File

@@ -43,7 +43,7 @@ namespace OpenNest.Geometry
}
public override string ToString() => $"{Width} x {Length}";
public string ToString(int decimalPlaces) => $"{System.Math.Round(Width, decimalPlaces)} x {System.Math.Round(Length, decimalPlaces)}";
}
}

View File

@@ -1,7 +1,6 @@
using System;
using OpenNest.Math;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -30,41 +29,41 @@ namespace OpenNest.Geometry
{
case PushDirection.Left:
case PushDirection.Right:
{
var dy = p2y - p1y;
if (System.Math.Abs(dy) < Tolerance.Epsilon)
{
var dy = p2y - p1y;
if (System.Math.Abs(dy) < Tolerance.Epsilon)
return double.MaxValue;
var t = (vy - p1y) / dy;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var ix = p1x + t * (p2x - p1x);
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0;
return double.MaxValue;
var t = (vy - p1y) / dy;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var ix = p1x + t * (p2x - p1x);
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0;
return double.MaxValue;
}
}
case PushDirection.Down:
case PushDirection.Up:
{
var dx = p2x - p1x;
if (System.Math.Abs(dx) < Tolerance.Epsilon)
{
var dx = p2x - p1x;
if (System.Math.Abs(dx) < Tolerance.Epsilon)
return double.MaxValue;
var t = (vx - p1x) / dx;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var iy = p1y + t * (p2y - p1y);
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0;
return double.MaxValue;
var t = (vx - p1x) / dx;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var iy = p1y + t * (p2y - p1y);
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0;
return double.MaxValue;
}
}
default:
return double.MaxValue;
@@ -363,10 +362,10 @@ namespace OpenNest.Geometry
{
switch (direction)
{
case PushDirection.Left: return box.Left - boundary.Left;
case PushDirection.Left: return box.Left - boundary.Left;
case PushDirection.Right: return boundary.Right - box.Right;
case PushDirection.Up: return boundary.Top - box.Top;
case PushDirection.Down: return box.Bottom - boundary.Bottom;
case PushDirection.Up: return boundary.Top - box.Top;
case PushDirection.Down: return box.Bottom - boundary.Bottom;
default: return double.MaxValue;
}
}
@@ -375,10 +374,10 @@ namespace OpenNest.Geometry
{
switch (direction)
{
case PushDirection.Left: return new Vector(-distance, 0);
case PushDirection.Left: return new Vector(-distance, 0);
case PushDirection.Right: return new Vector(distance, 0);
case PushDirection.Up: return new Vector(0, distance);
case PushDirection.Down: return new Vector(0, -distance);
case PushDirection.Up: return new Vector(0, distance);
case PushDirection.Down: return new Vector(0, -distance);
default: return new Vector();
}
}
@@ -387,10 +386,10 @@ namespace OpenNest.Geometry
{
switch (direction)
{
case PushDirection.Left: return from.Left - to.Right;
case PushDirection.Left: return from.Left - to.Right;
case PushDirection.Right: return to.Left - from.Right;
case PushDirection.Up: return to.Bottom - from.Top;
case PushDirection.Down: return from.Bottom - to.Top;
case PushDirection.Up: return to.Bottom - from.Top;
case PushDirection.Down: return from.Bottom - to.Top;
default: return double.MaxValue;
}
}

View File

@@ -1,5 +1,5 @@
using System;
using OpenNest.Math;
using OpenNest.Math;
using System;
namespace OpenNest.Geometry
{

View File

@@ -1,6 +1,4 @@
using System;
namespace OpenNest.Math
namespace OpenNest.Math
{
public static class Angle
{

View File

@@ -1,6 +1,4 @@
using System;
namespace OpenNest.Math
namespace OpenNest.Math
{
public static class Tolerance
{

View File

@@ -1,6 +1,4 @@
using System;
namespace OpenNest.Math
namespace OpenNest.Math
{
public static class Trigonometry
{

View File

@@ -1,6 +1,6 @@
using System;
using OpenNest.Collections;
using OpenNest.Collections;
using OpenNest.Geometry;
using System;
namespace OpenNest
{

View File

@@ -1,5 +1,4 @@
using System;
using OpenNest.Math;
using OpenNest.Math;
namespace OpenNest
{

View File

@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest
{

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest.Converters;
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest
{
@@ -174,10 +174,10 @@ namespace OpenNest
switch (facingDirection)
{
case PushDirection.Left: keep = -sign * dy > 0; break;
case PushDirection.Right: keep = sign * dy > 0; break;
case PushDirection.Up: keep = -sign * dx > 0; break;
case PushDirection.Down: keep = sign * dx > 0; break;
case PushDirection.Left: keep = -sign * dy > 0; break;
case PushDirection.Right: keep = sign * dy > 0; break;
case PushDirection.Up: keep = -sign * dx > 0; break;
case PushDirection.Down: keep = sign * dx > 0; break;
default: keep = true; break;
}

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Collections;
using OpenNest.Collections;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,9 +1,9 @@
using OpenNest.Converters;
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest.Shapes
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{

View File

@@ -1,9 +1,9 @@
using System;
using System.Linq;
using OpenNest.Api;
using OpenNest.Api;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
using System;
using System.Linq;
namespace OpenNest
{
@@ -84,7 +84,7 @@ namespace OpenNest
time += TimeSpan.FromSeconds(info.TravelDistance / cutParams.RapidTravelRate);
break;
}
time += TimeSpan.FromTicks(info.PierceCount * cutParams.PierceTime.Ticks);
return time;

View File

@@ -19,7 +19,7 @@ namespace OpenNest
case Units.Millimeters:
return "mm";
default:
default:
return string.Empty;
}
}
@@ -34,7 +34,7 @@ namespace OpenNest
case Units.Millimeters:
return "millimeters";
default:
default:
return string.Empty;
}
}
@@ -49,7 +49,7 @@ namespace OpenNest
case Units.Millimeters:
return "sec";
default:
default:
return string.Empty;
}
}

View File

@@ -1,97 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenNest.Engine.ML;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
{
/// <summary>
/// Builds candidate rotation angles for single-item fill. Encapsulates the
/// full pipeline: base angles, narrow-area sweep, ML prediction, and
/// known-good pruning across fills.
/// </summary>
public class AngleCandidateBuilder
{
private readonly HashSet<double> knownGoodAngles = new();
public bool ForceFullSweep { get; set; }
public List<double> Build(NestItem item, double bestRotation, Box workArea)
{
var angles = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
var testPart = new Part(item.Drawing);
if (!bestRotation.IsEqualTo(0))
testPart.Rotate(bestRotation);
testPart.UpdateBounds();
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
var needsSweep = workAreaShortSide < partLongestSide || ForceFullSweep;
if (needsSweep)
{
var step = Angle.ToRadians(5);
for (var a = 0.0; a < System.Math.PI; a += step)
{
if (!angles.Any(existing => existing.IsEqualTo(a)))
angles.Add(a);
}
}
if (!ForceFullSweep && angles.Count > 2)
{
var features = FeatureExtractor.Extract(item.Drawing);
if (features != null)
{
var predicted = AnglePredictor.PredictAngles(
features, workArea.Width, workArea.Length);
if (predicted != null)
{
var mlAngles = new List<double>(predicted);
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation)))
mlAngles.Add(bestRotation);
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation + Angle.HalfPI)))
mlAngles.Add(bestRotation + Angle.HalfPI);
Debug.WriteLine($"[AngleCandidateBuilder] ML: {angles.Count} angles -> {mlAngles.Count} predicted");
angles = mlAngles;
}
}
}
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
{
var pruned = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
foreach (var a in knownGoodAngles)
{
if (!pruned.Any(existing => existing.IsEqualTo(a)))
pruned.Add(a);
}
Debug.WriteLine($"[AngleCandidateBuilder] Pruned: {angles.Count} -> {pruned.Count} angles (known-good)");
return pruned;
}
return angles;
}
/// <summary>
/// Records angles that produced results. These are used to prune
/// subsequent Build() calls.
/// </summary>
public void RecordProductive(List<AngleResult> angleResults)
{
foreach (var ar in angleResults)
{
if (ar.PartCount > 0)
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
}
}
}
}

View File

@@ -1,11 +1,11 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using OpenNest.Converters;
using OpenNest.Engine.BestFit.Tiling;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace OpenNest.Engine.BestFit
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Engine.BestFit
{

View File

@@ -1,9 +1,10 @@
using OpenNest.Converters;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest.Engine.BestFit
{

View File

@@ -1,6 +1,6 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.Engine.BestFit
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Engine.BestFit.Tiling
{

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Engine.BestFit.Tiling
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.CirclePacking
{

View File

@@ -1,6 +1,6 @@
using System;
using OpenNest.Geometry;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
namespace OpenNest.CirclePacking
{

View File

@@ -1,6 +1,6 @@
using System;
using OpenNest.Geometry;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
namespace OpenNest.CirclePacking
{

View File

@@ -1,363 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest
{
/// <summary>
/// Pushes a group of parts left and down to close gaps after placement.
/// Uses the same directional-distance logic as PlateView.PushSelected
/// but operates on Part objects directly.
/// </summary>
public static class Compactor
{
private const double ChordTolerance = 0.001;
/// <summary>
/// Compacts movingParts toward the bottom-left of the plate work area.
/// Everything already on the plate (excluding movingParts) is treated
/// as stationary obstacles.
/// </summary>
private const double RepeatThreshold = 0.01;
private const int MaxIterations = 20;
public static void Compact(List<Part> movingParts, Plate plate)
{
if (movingParts == null || movingParts.Count == 0)
return;
var savedPositions = SavePositions(movingParts);
// Try left-first.
var leftFirst = CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down);
// Restore and try down-first.
RestorePositions(movingParts, savedPositions);
var downFirst = CompactLoop(movingParts, plate, PushDirection.Down, PushDirection.Left);
// Keep left-first if it traveled further.
if (leftFirst > downFirst)
{
RestorePositions(movingParts, savedPositions);
CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down);
}
}
private static double CompactLoop(List<Part> parts, Plate plate,
PushDirection first, PushDirection second)
{
var total = 0.0;
for (var i = 0; i < MaxIterations; i++)
{
var a = Push(parts, plate, first);
var b = Push(parts, plate, second);
total += a + b;
if (a <= RepeatThreshold && b <= RepeatThreshold)
break;
}
return total;
}
private static Vector[] SavePositions(List<Part> parts)
{
var positions = new Vector[parts.Count];
for (var i = 0; i < parts.Count; i++)
positions[i] = parts[i].Location;
return positions;
}
private static void RestorePositions(List<Part> parts, Vector[] positions)
{
for (var i = 0; i < parts.Count; i++)
parts[i].Location = positions[i];
}
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
/// <summary>
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
/// </summary>
public static double Push(List<Part> movingParts, Plate plate, double angle)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle);
}
/// <summary>
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
/// </summary>
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, double angle)
{
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
var opposite = -direction;
var obstacleBoxes = new Box[obstacleParts.Count];
var obstacleLines = new List<Line>[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var halfSpacing = partSpacing / 2;
var distance = double.MaxValue;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
List<Line> movingLines = null;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
if (gap >= distance)
continue;
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
continue;
movingLines ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
obstacleLines[i] ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = direction * distance;
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var obstacleBoxes = new Box[obstacleParts.Count];
var obstacleLines = new List<Line>[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var opposite = SpatialQuery.OppositeDirection(direction);
var halfSpacing = partSpacing / 2;
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
var distance = double.MaxValue;
// BB gap at which offset geometries are expected to be touching.
var contactGap = (halfSpacing + ChordTolerance) * 2;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
List<Line> movingLines = null;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
// Use the reverse-direction gap to check if the obstacle is entirely
// behind the moving part. The forward gap (gap < 0) is unreliable for
// irregular shapes whose bounding boxes overlap even when the actual
// geometry still has a valid contact in the push direction.
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
if (gap >= distance)
continue;
var perpOverlap = isHorizontal
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
if (!perpOverlap)
continue;
movingLines ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
obstacleLines[i] ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = SpatialQuery.DirectionToOffset(direction, distance);
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
/// <summary>
/// Pushes movingParts using bounding-box distances only (no geometry lines).
/// Much faster but less precise — use as a coarse positioning pass before
/// a full geometry Push.
/// </summary>
public static double PushBoundingBox(List<Part> movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
public static double PushBoundingBox(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var obstacleBoxes = new Box[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var opposite = SpatialQuery.OppositeDirection(direction);
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
var distance = double.MaxValue;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var perpOverlap = isHorizontal
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
if (!perpOverlap)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
var d = gap - partSpacing;
if (d < 0) d = 0;
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = SpatialQuery.DirectionToOffset(direction, distance);
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
/// <summary>
/// Compacts parts individually toward the bottom-left of the work area.
/// Each part is pushed against all others as obstacles, closing geometry-based gaps.
/// Does not require parts to be on a plate.
/// </summary>
public static void CompactIndividual(List<Part> parts, Box workArea, double partSpacing)
{
if (parts == null || parts.Count < 2)
return;
var savedPositions = SavePositions(parts);
var leftFirst = CompactIndividualLoop(parts, workArea, partSpacing,
PushDirection.Left, PushDirection.Down);
RestorePositions(parts, savedPositions);
var downFirst = CompactIndividualLoop(parts, workArea, partSpacing,
PushDirection.Down, PushDirection.Left);
if (leftFirst > downFirst)
{
RestorePositions(parts, savedPositions);
CompactIndividualLoop(parts, workArea, partSpacing,
PushDirection.Left, PushDirection.Down);
}
}
private static double CompactIndividualLoop(List<Part> parts, Box workArea,
double partSpacing, PushDirection first, PushDirection second)
{
var total = 0.0;
for (var pass = 0; pass < MaxIterations; pass++)
{
var moved = 0.0;
foreach (var part in parts)
{
var single = new List<Part>(1) { part };
var obstacles = new List<Part>(parts.Count - 1);
foreach (var p in parts)
if (p != part) obstacles.Add(p);
moved += Push(single, obstacles, workArea, partSpacing, first);
moved += Push(single, obstacles, workArea, partSpacing, second);
}
total += moved;
if (moved <= RepeatThreshold)
break;
}
return total;
}
}
}

View File

@@ -1,13 +1,12 @@
using OpenNest.Engine.Fill;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.RectanglePacking;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.Math;
using OpenNest.RectanglePacking;
namespace OpenNest
{
@@ -56,7 +55,8 @@ namespace OpenNest
if (item.Quantity > 0 && best.Count > item.Quantity)
best = best.Take(item.Quantity).ToList();
ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary());
ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary(),
isOverallBest: true);
return best;
}
@@ -67,89 +67,24 @@ namespace OpenNest
if (groupParts == null || groupParts.Count == 0)
return new List<Part>();
// Single part: delegate to the strategy pipeline.
if (groupParts.Count == 1)
{
var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
return Fill(nestItem, workArea, progress, token);
}
// Multi-part group: linear pattern fill only.
PhaseResults.Clear();
var engine = new FillLinear(workArea, Plate.PartSpacing);
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
var best = FillPattern(engine, groupParts, angles, workArea);
var best = FillHelpers.FillPattern(engine, groupParts, angles, workArea);
PhaseResults.Add(new PhaseResult(NestPhase.Linear, best?.Count ?? 0, 0));
Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
Debug.WriteLine($"[Fill(groupParts,Box)] Linear pattern: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary());
if (groupParts.Count == 1)
{
try
{
token.ThrowIfCancellationRequested();
var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
var binItem = BinConverter.ToItem(nestItem, Plate.PartSpacing);
var bin = BinConverter.CreateBin(workArea, Plate.PartSpacing);
var rectEngine = new FillBestFit(bin);
rectEngine.Fill(binItem);
var rectResult = BinConverter.ToParts(bin, new List<NestItem> { nestItem });
PhaseResults.Add(new PhaseResult(NestPhase.RectBestFit, rectResult?.Count ?? 0, 0));
Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts");
if (IsBetterFill(rectResult, best, workArea))
{
best = rectResult;
ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea, BuildProgressSummary());
}
token.ThrowIfCancellationRequested();
var pairFiller = new PairFiller(Plate.Size, Plate.PartSpacing);
var pairResult = pairFiller.Fill(nestItem, workArea, PlateNumber, token, progress);
PhaseResults.Add(new PhaseResult(NestPhase.Pairs, pairResult.Count, 0));
Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best, workArea) ? "Pair" : "Linear")}");
if (IsBetterFill(pairResult, best, workArea))
{
best = pairResult;
ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea, BuildProgressSummary());
}
token.ThrowIfCancellationRequested();
var extentsFiller = new FillExtents(workArea, Plate.PartSpacing);
var bestFits2 = BestFitCache.GetOrCompute(
groupParts[0].BaseDrawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
var extentsAngles2 = new[] { groupParts[0].Rotation, groupParts[0].Rotation + Angle.HalfPI };
List<Part> bestExtents2 = null;
foreach (var angle in extentsAngles2)
{
token.ThrowIfCancellationRequested();
var result = extentsFiller.Fill(groupParts[0].BaseDrawing, angle, PlateNumber, token, progress, bestFits2);
if (result != null && result.Count > (bestExtents2?.Count ?? 0))
bestExtents2 = result;
}
PhaseResults.Add(new PhaseResult(NestPhase.Extents, bestExtents2?.Count ?? 0, 0));
Debug.WriteLine($"[Fill(groupParts,Box)] Extents: {bestExtents2?.Count ?? 0} parts");
if (IsBetterFill(bestExtents2, best, workArea))
{
best = bestExtents2;
ReportProgress(progress, NestPhase.Extents, PlateNumber, best, workArea, BuildProgressSummary());
}
}
catch (OperationCanceledException)
{
Debug.WriteLine("[Fill(groupParts,Box)] Cancelled, returning current best");
}
}
// Always report the final winner so the UI's temporary parts
// match the returned result.
var winPhase = PhaseResults.Count > 0
? PhaseResults.OrderByDescending(r => r.PartCount).First().Phase
: NestPhase.Linear;
ReportProgress(progress, winPhase, PlateNumber, best, workArea, BuildProgressSummary());
ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary(),
isOverallBest: true);
return best ?? new List<Part>();
}
@@ -201,8 +136,13 @@ namespace OpenNest
context.CurrentBest = result;
context.CurrentBestScore = FillScore.Compute(result, context.WorkArea);
context.WinnerPhase = strategy.Phase;
ReportProgress(context.Progress, strategy.Phase, PlateNumber,
result, context.WorkArea, BuildProgressSummary());
}
if (context.CurrentBest != null && context.CurrentBest.Count > 0)
{
ReportProgress(context.Progress, context.WinnerPhase, PlateNumber,
context.CurrentBest, context.WorkArea, BuildProgressSummary(),
isOverallBest: true);
}
}
}
@@ -214,13 +154,5 @@ namespace OpenNest
angleBuilder.RecordProductive(context.AngleResults);
}
// --- Pattern helpers ---
internal static Pattern BuildRotatedPattern(List<Part> groupParts, double angle)
=> FillHelpers.BuildRotatedPattern(groupParts, angle);
internal static List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles, Box workArea)
=> FillHelpers.FillPattern(engine, groupParts, angles, workArea);
}
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Wraps an IProgress to prepend previously placed parts to each report,

View File

@@ -0,0 +1,114 @@
using OpenNest.Engine.ML;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Builds candidate rotation angles for single-item fill. Encapsulates the
/// full pipeline: base angles, narrow-area sweep, ML prediction, and
/// known-good pruning across fills.
/// </summary>
public class AngleCandidateBuilder
{
private readonly HashSet<double> knownGoodAngles = new();
public bool ForceFullSweep { get; set; }
public List<double> Build(NestItem item, double bestRotation, Box workArea)
{
var baseAngles = new[] { bestRotation, bestRotation + Angle.HalfPI };
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
return BuildPrunedList(baseAngles);
var angles = new List<double>(baseAngles);
if (NeedsSweep(item, bestRotation, workArea))
AddSweepAngles(angles);
if (!ForceFullSweep && angles.Count > 2)
angles = ApplyMlPrediction(item, workArea, baseAngles, angles);
return angles;
}
private bool NeedsSweep(NestItem item, double bestRotation, Box workArea)
{
var testPart = new Part(item.Drawing);
if (!bestRotation.IsEqualTo(0))
testPart.Rotate(bestRotation);
testPart.UpdateBounds();
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
return workAreaShortSide < partLongestSide || ForceFullSweep;
}
private static void AddSweepAngles(List<double> angles)
{
var step = Angle.ToRadians(5);
for (var a = 0.0; a < System.Math.PI; a += step)
{
if (!ContainsAngle(angles, a))
angles.Add(a);
}
}
private static List<double> ApplyMlPrediction(
NestItem item, Box workArea, double[] baseAngles, List<double> fallback)
{
var features = FeatureExtractor.Extract(item.Drawing);
if (features == null)
return fallback;
var predicted = AnglePredictor.PredictAngles(features, workArea.Width, workArea.Length);
if (predicted == null)
return fallback;
var mlAngles = new List<double>(predicted);
foreach (var b in baseAngles)
{
if (!ContainsAngle(mlAngles, b))
mlAngles.Add(b);
}
Debug.WriteLine($"[AngleCandidateBuilder] ML: {fallback.Count} angles -> {mlAngles.Count} predicted");
return mlAngles;
}
private List<double> BuildPrunedList(double[] baseAngles)
{
var pruned = new List<double>(baseAngles);
foreach (var a in knownGoodAngles)
{
if (!ContainsAngle(pruned, a))
pruned.Add(a);
}
Debug.WriteLine($"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)");
return pruned;
}
private static bool ContainsAngle(List<double> angles, double angle)
{
return angles.Any(existing => existing.IsEqualTo(angle));
}
/// <summary>
/// Records angles that produced results. These are used to prune
/// subsequent Build() calls.
/// </summary>
public void RecordProductive(List<AngleResult> angleResults)
{
foreach (var ar in angleResults)
{
if (ar.PartCount > 0)
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
}
}
}
}

View File

@@ -1,5 +1,4 @@
using System;
using OpenNest.Math;
using OpenNest.Math;
namespace OpenNest
{

View File

@@ -0,0 +1,178 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Pushes a group of parts left and down to close gaps after placement.
/// Uses the same directional-distance logic as PlateView.PushSelected
/// but operates on Part objects directly.
/// </summary>
public static class Compactor
{
private const double ChordTolerance = 0.001;
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
/// <summary>
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
/// </summary>
public static double Push(List<Part> movingParts, Plate plate, double angle)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
/// <summary>
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
/// </summary>
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, Vector direction)
{
var opposite = -direction;
var obstacleBoxes = new Box[obstacleParts.Count];
var obstacleLines = new List<Line>[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var halfSpacing = partSpacing / 2;
var distance = double.MaxValue;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
List<Line> movingLines = null;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
if (gap >= distance)
continue;
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
continue;
movingLines ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
obstacleLines[i] ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = direction * distance;
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var vector = SpatialQuery.DirectionToOffset(direction, 1.0);
return Push(movingParts, obstacleParts, workArea, partSpacing, vector);
}
/// <summary>
/// Pushes movingParts using bounding-box distances only (no geometry lines).
/// Much faster but less precise — use as a coarse positioning pass before
/// a full geometry Push.
/// </summary>
public static double PushBoundingBox(List<Part> movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
public static double PushBoundingBox(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var obstacleBoxes = new Box[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var opposite = SpatialQuery.OppositeDirection(direction);
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
var distance = double.MaxValue;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var perpOverlap = isHorizontal
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
if (!perpOverlap)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
var d = gap - partSpacing;
if (d < 0) d = 0;
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = SpatialQuery.DirectionToOffset(direction, distance);
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
}
}

View File

@@ -1,11 +1,11 @@
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
public class FillExtents
{
@@ -173,18 +173,12 @@ namespace OpenNest
if (minSlide >= double.MaxValue || minSlide < 0)
return pairHeight + partSpacing;
// Boundaries are inflated by halfSpacing, so when inflated edges touch
// the actual parts have partSpacing gap. Match FillLinear's pattern:
// startOffset = pairHeight (no extra spacing), copyDist = height - slide.
// Match FillLinear.ComputeCopyDistance: copyDist = startOffset - slide,
// clamped so it never goes below pairHeight + partSpacing to prevent
// bounding-box overlap from spurious slide values.
var copyDist = pairHeight - minSlide;
// Boundaries are inflated by halfSpacing, so the geometry-aware
// distance already guarantees partSpacing gap. Only fall back to
// bounding-box distance if the calculation produced a non-positive value.
if (copyDist <= Tolerance.Epsilon)
return pairHeight + partSpacing;
return copyDist;
return System.Math.Max(copyDist, pairHeight + partSpacing);
}
private static double SlideDistance(

View File

@@ -1,9 +1,9 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
public class FillLinear
{
@@ -16,7 +16,7 @@ namespace OpenNest
public Box WorkArea { get; }
public double PartSpacing { get; }
public double HalfSpacing => PartSpacing / 2;
/// <summary>
@@ -110,47 +110,40 @@ namespace OpenNest
var pushDir = GetPushDirection(direction);
var opposite = SpatialQuery.OppositeDirection(pushDir);
// Compute a starting offset large enough that every part-pair in
// patternB has its offset geometry beyond patternA's offset geometry.
var maxUpper = double.MinValue;
var minLower = double.MaxValue;
for (var i = 0; i < patternA.Parts.Count; i++)
{
var bb = patternA.Parts[i].BoundingBox;
var upper = direction == NestDirection.Horizontal ? bb.Right : bb.Top;
var lower = direction == NestDirection.Horizontal ? bb.Left : bb.Bottom;
if (upper > maxUpper) maxUpper = upper;
if (lower < minLower) minLower = lower;
}
var startOffset = System.Math.Max(bboxDim,
maxUpper - minLower + PartSpacing + Tolerance.Epsilon);
// bboxDim already spans max(upper) - min(lower) across all parts,
// so the start offset just needs to push beyond that plus spacing.
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
var offset = MakeOffset(direction, startOffset);
// Pre-cache edge arrays.
var movingEdges = new (Vector start, Vector end)[patternA.Parts.Count][];
var stationaryEdges = new (Vector start, Vector end)[patternA.Parts.Count][];
var maxCopyDistance = FindMaxPairDistance(
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
for (var i = 0; i < patternA.Parts.Count; i++)
{
movingEdges[i] = boundaries[i].GetEdges(pushDir);
stationaryEdges[i] = boundaries[i].GetEdges(opposite);
}
if (maxCopyDistance < Tolerance.Epsilon)
return bboxDim + PartSpacing;
return maxCopyDistance;
}
/// <summary>
/// Tests every pair of parts across adjacent pattern copies and returns the
/// maximum copy distance found. Returns 0 if no valid slide was found.
/// </summary>
private static double FindMaxPairDistance(
List<Part> parts, PartBoundary[] boundaries, Vector offset,
PushDirection pushDir, PushDirection opposite, double startOffset)
{
var maxCopyDistance = 0.0;
for (var j = 0; j < patternA.Parts.Count; j++)
for (var j = 0; j < parts.Count; j++)
{
var locationB = patternA.Parts[j].Location + offset;
var movingEdges = boundaries[j].GetEdges(pushDir);
var locationB = parts[j].Location + offset;
for (var i = 0; i < patternA.Parts.Count; i++)
for (var i = 0; i < parts.Count; i++)
{
var slideDistance = SpatialQuery.DirectionalDistance(
movingEdges[j], locationB,
stationaryEdges[i], patternA.Parts[i].Location,
movingEdges, locationB,
boundaries[i].GetEdges(opposite), parts[i].Location,
pushDir);
if (slideDistance >= double.MaxValue || slideDistance < 0)
@@ -163,9 +156,6 @@ namespace OpenNest
}
}
if (maxCopyDistance < Tolerance.Epsilon)
return bboxDim + PartSpacing;
return maxCopyDistance;
}

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
public readonly struct FillScore : System.IComparable<FillScore>
{

View File

@@ -1,23 +1,35 @@
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Fills a work area using interlocking part pairs from BestFitCache.
/// Extracted from DefaultNestEngine.FillWithPairs.
/// </summary>
public class PairFiller
{
private const int MaxTopCandidates = 50;
private const int MaxStripCandidates = 100;
private const double MinStripUtilization = 0.3;
private const int EarlyExitMinTried = 10;
private const int EarlyExitStaleLimit = 10;
private readonly Size plateSize;
private readonly double partSpacing;
/// <summary>
/// The best-fit results computed during the last Fill call.
/// Available after Fill returns so callers can reuse without recomputing.
/// </summary>
public List<BestFitResult> BestFits { get; private set; }
public PairFiller(Size plateSize, double partSpacing)
{
this.plateSize = plateSize;
@@ -29,11 +41,11 @@ namespace OpenNest
CancellationToken token = default,
IProgress<NestProgress> progress = null)
{
var bestFits = BestFitCache.GetOrCompute(
BestFits = BestFitCache.GetOrCompute(
item.Drawing, plateSize.Length, plateSize.Width, partSpacing);
var candidates = SelectPairCandidates(bestFits, workArea);
Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}");
var candidates = SelectPairCandidates(BestFits, workArea);
Debug.WriteLine($"[PairFiller] Total: {BestFits.Count}, Kept: {BestFits.Count(r => r.Keep)}, Trying: {candidates.Count}");
Debug.WriteLine($"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}");
List<Part> best = null;
@@ -46,17 +58,7 @@ namespace OpenNest
{
token.ThrowIfCancellationRequested();
var result = candidates[i];
var pairParts = result.BuildParts(item.Drawing);
var angles = result.HullAngles;
var engine = new FillLinear(workArea, partSpacing);
// Let the remainder strip try pair-based filling too.
var p0 = DefaultNestEngine.BuildRotatedPattern(pairParts, 0);
var p90 = DefaultNestEngine.BuildRotatedPattern(pairParts, Angle.HalfPI);
engine.RemainderPatterns = new List<Pattern> { p0, p90 };
var filled = DefaultNestEngine.FillPattern(engine, pairParts, angles, workArea);
var filled = EvaluateCandidate(candidates[i], item.Drawing, workArea);
if (filled != null && filled.Count > 0)
{
@@ -80,8 +82,7 @@ namespace OpenNest
NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea,
$"Pairs: {i + 1}/{candidates.Count} candidates, best = {bestScore.Count} parts");
// Early exit: stop if we've tried enough candidates without improvement.
if (i >= 9 && sinceImproved >= 10)
if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
{
Debug.WriteLine($"[PairFiller] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
break;
@@ -97,10 +98,22 @@ namespace OpenNest
return best ?? new List<Part>();
}
private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing, Box workArea)
{
var pairParts = candidate.BuildParts(drawing);
var engine = new FillLinear(workArea, partSpacing);
var p0 = FillHelpers.BuildRotatedPattern(pairParts, 0);
var p90 = FillHelpers.BuildRotatedPattern(pairParts, Angle.HalfPI);
engine.RemainderPatterns = new List<Pattern> { p0, p90 };
return FillHelpers.FillPattern(engine, pairParts, candidate.HullAngles, workArea);
}
private List<BestFitResult> SelectPairCandidates(List<BestFitResult> bestFits, Box workArea)
{
var kept = bestFits.Where(r => r.Keep).ToList();
var top = kept.Take(50).ToList();
var top = kept.Take(MaxTopCandidates).ToList();
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length);
@@ -109,14 +122,14 @@ namespace OpenNest
{
var stripCandidates = bestFits
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
&& r.Utilization >= 0.3)
&& r.Utilization >= MinStripUtilization)
.OrderByDescending(r => r.Utilization);
var existing = new HashSet<BestFitResult>(top);
foreach (var r in stripCandidates)
{
if (top.Count >= 100)
if (top.Count >= MaxStripCandidates)
break;
if (existing.Add(r))

View File

@@ -1,9 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest.Converters;
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Pre-computed offset boundary polygons for a part's geometry.
@@ -87,9 +87,9 @@ namespace OpenNest
var edge = (verts[i - 1], verts[i]);
if (-sign * dy > 0) left.Add(edge);
if ( sign * dy > 0) right.Add(edge);
if (sign * dy > 0) right.Add(edge);
if (-sign * dx > 0) up.Add(edge);
if ( sign * dx > 0) down.Add(edge);
if (sign * dx > 0) down.Add(edge);
}
}
@@ -145,11 +145,11 @@ namespace OpenNest
{
switch (direction)
{
case PushDirection.Left: return _leftEdges;
case PushDirection.Left: return _leftEdges;
case PushDirection.Right: return _rightEdges;
case PushDirection.Up: return _upEdges;
case PushDirection.Down: return _downEdges;
default: return _leftEdges;
case PushDirection.Up: return _upEdges;
case PushDirection.Down: return _downEdges;
default: return _leftEdges;
}
}

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
public class Pattern
{

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Engine
namespace OpenNest.Engine.Fill
{
public static class PatternTiler
{

View File

@@ -1,9 +1,9 @@
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Iteratively fills remnant boxes with items using a RemnantFinder.

View File

@@ -1,9 +1,8 @@
using System;
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
/// <summary>
/// A remnant box with a priority tier.

View File

@@ -1,10 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
internal static class RotationAnalysis
{

View File

@@ -1,10 +1,10 @@
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
public enum ShrinkAxis { Width, Height }

View File

@@ -1,11 +1,11 @@
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using OpenNest.Math;
namespace OpenNest.Engine.ML
{

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.Engine.ML
{

View File

@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
using System.Linq;
namespace OpenNest.Engine.ML
{

View File

@@ -1,9 +1,10 @@
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest
{
@@ -190,7 +191,8 @@ namespace OpenNest
int plateNumber,
List<Part> best,
Box workArea,
string description)
string description,
bool isOverallBest = false)
{
if (progress == null || best == null || best.Count == 0)
return;
@@ -212,9 +214,13 @@ namespace OpenNest
$"PartArea={totalPartArea:F0}, Remnant={workArea.Area() - totalPartArea:F0}, " +
$"WorkArea={workArea.Width:F1}x{workArea.Length:F1} | {description}";
Debug.WriteLine(msg);
try { System.IO.File.AppendAllText(
try
{
System.IO.File.AppendAllText(
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"),
$"{DateTime.Now:HH:mm:ss.fff} {msg}\n"); } catch { }
$"{DateTime.Now:HH:mm:ss.fff} {msg}\n");
}
catch { }
progress.Report(new NestProgress
{
@@ -228,6 +234,7 @@ namespace OpenNest
BestParts = clonedParts,
Description = description,
ActiveWorkArea = workArea,
IsOverallBest = isOverallBest,
});
}

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
{
@@ -46,5 +46,6 @@ namespace OpenNest
public List<Part> BestParts { get; set; }
public string Description { get; set; }
public Box ActiveWorkArea { get; set; }
public bool IsOverallBest { get; set; }
}
}

View File

@@ -1,13 +1,12 @@
using System;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
namespace OpenNest.Engine.Nfp
{
/// <summary>
/// Mixed-part geometry-aware nesting using NFP-based collision avoidance

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
namespace OpenNest.Engine.Nfp
{
/// <summary>
/// NFP-based Bottom-Left Fill (BLF) placement engine.

View File

@@ -1,8 +1,9 @@
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest
namespace OpenNest.Engine.Nfp
{
/// <summary>
/// Result of a nest optimization run.

View File

@@ -1,8 +1,8 @@
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest
namespace OpenNest.Engine.Nfp
{
/// <summary>
/// Caches computed No-Fit Polygons keyed by (DrawingA.Id, RotationA, DrawingB.Id, RotationB).

View File

@@ -1,11 +1,12 @@
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest
namespace OpenNest.Engine.Nfp
{
/// <summary>
/// Simulated annealing optimizer for NFP-based nesting.

Some files were not shown because too many files have changed in this diff Show More