mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 07:11:06 -04:00
feat(engine): live sheet preview while Irregular packs
The frontier packer reports its placements after each choice; the engine turns them into preview parts at most four times a second and sends them as NestProgress.BestParts with the stock work area, which desktop Auto Nest already draws on the plate view. Reporting only reads finished placements, so layouts are unchanged with or without a progress sink. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0baa737162
commit
c997b2e588
5 files changed
+114
-5
No files matched your search
@@ -1,13 +1,13 @@
|
||||
using OpenNest.Engine.NestingEngines.Irregular;
|
||||
using static OpenNest.Engine.Tests.NestingEngines.JobBuilder;
|
||||
using static OpenNest.Engine.Tests.NestingEngines.Shapes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
using OpenNest.Engine.NestingEngines.Irregular;
|
||||
using OpenNest.Geometry;
|
||||
using static OpenNest.Engine.Tests.NestingEngines.JobBuilder;
|
||||
using static OpenNest.Engine.Tests.NestingEngines.Shapes;
|
||||
|
||||
namespace OpenNest.Engine.Tests.NestingEngines;
|
||||
|
||||
@@ -211,6 +211,42 @@ public class IrregularNestingEngineTests
|
||||
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReportsLiveSheetPreviewWithoutChangingTheLayout()
|
||||
{
|
||||
var job = Job(new[] { Part("disc", Disc(3), 10), Part("ell", LShape(12, 8, 4), 10) },
|
||||
new[] { Stock("sheet", 40, 60, spacing: 0.5) });
|
||||
var reports = new List<NestJobProgress>();
|
||||
|
||||
var withPreview = new IrregularNestingEngine().Solve(job, new SyncProgress(reports.Add));
|
||||
var without = new IrregularNestingEngine().Solve(job);
|
||||
|
||||
var previews = reports.Where(r => r.LegacyProgress?.BestParts is { Count: > 0 }).ToList();
|
||||
Assert.NotEmpty(previews);
|
||||
var work = job.Plates[0].WorkArea;
|
||||
foreach (var preview in previews.Select(r => r.LegacyProgress))
|
||||
{
|
||||
Assert.True(preview.IsOverallBest);
|
||||
Assert.Equal((work.Left, work.Bottom, work.Right, work.Top), (preview.ActiveWorkArea.Left,
|
||||
preview.ActiveWorkArea.Bottom, preview.ActiveWorkArea.Right, preview.ActiveWorkArea.Top));
|
||||
foreach (var part in preview.BestParts)
|
||||
{
|
||||
Assert.InRange(part.BoundingBox.Left, work.Left - 1e-6, work.Right);
|
||||
Assert.InRange(part.BoundingBox.Right, work.Left, work.Right + 1e-6);
|
||||
Assert.InRange(part.BoundingBox.Bottom, work.Bottom - 1e-6, work.Top);
|
||||
Assert.InRange(part.BoundingBox.Top, work.Bottom, work.Top + 1e-6);
|
||||
}
|
||||
}
|
||||
Assert.Equal(
|
||||
without.Plates.SelectMany(p => p.Placements),
|
||||
withPreview.Plates.SelectMany(p => p.Placements));
|
||||
}
|
||||
|
||||
private sealed class SyncProgress(Action<NestJobProgress> report) : IProgress<NestJobProgress>
|
||||
{
|
||||
public void Report(NestJobProgress value) => report(value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasPublicParameterlessConstructorForPluginDiscovery()
|
||||
{
|
||||
|
||||
@@ -64,6 +64,9 @@ internal sealed class FrontierPacker
|
||||
private readonly BlockCatalog? blocks;
|
||||
private readonly ClampObstacles? clamps;
|
||||
|
||||
/// <summary>Observer of the placements so far (live preview); read-only, it never steers packing.</summary>
|
||||
private readonly Action<IReadOnlyList<Placed>>? placedChanged;
|
||||
|
||||
public FrontierPacker(
|
||||
IReadOnlyList<PartType> types,
|
||||
NoFitCache nfps,
|
||||
@@ -73,9 +76,11 @@ internal sealed class FrontierPacker
|
||||
double beta,
|
||||
WorkCounter counter,
|
||||
BlockCatalog? blocks = null,
|
||||
ClampObstacles? clamps = null
|
||||
ClampObstacles? clamps = null,
|
||||
Action<IReadOnlyList<Placed>>? placedChanged = null
|
||||
)
|
||||
{
|
||||
this.placedChanged = placedChanged;
|
||||
this.blocks = blocks;
|
||||
this.clamps = stock.ClampZones.Count == 0 ? null : clamps ?? new ClampObstacles(stock);
|
||||
this.counter = counter;
|
||||
@@ -182,6 +187,7 @@ internal sealed class FrontierPacker
|
||||
partArea += types[typeIndex].Area;
|
||||
front = System.Math.Max(front, axis == PackAxis.X ? part.Right : part.Top);
|
||||
}
|
||||
placedChanged?.Invoke(placed);
|
||||
|
||||
left[typeIndex] -= choice.Count;
|
||||
blockStates.RemoveAll(b => b.Members.Count > left[b.Members[0].Orientation.TypeIndex]);
|
||||
|
||||
@@ -131,6 +131,7 @@ public sealed class IrregularNestingEngine : INestingEngine
|
||||
}
|
||||
|
||||
private const int MaxTail = 3;
|
||||
private readonly SheetPreview? preview = progress == null ? null : new SheetPreview(progress, types, drawingFor);
|
||||
private readonly Dictionary<double, NoFitCache> caches = new();
|
||||
private readonly Dictionary<NestPlateStock, ClampObstacles> clamps = new();
|
||||
private readonly Dictionary<double, IReadOnlyDictionary<int, IReadOnlyList<PairPose>>> pairs = new();
|
||||
@@ -289,8 +290,10 @@ public sealed class IrregularNestingEngine : INestingEngine
|
||||
if (stock.Quantity is int available && used[stock.Id] >= available)
|
||||
continue;
|
||||
progress?.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stock.Id, sheets.Count, 0, 0));
|
||||
var plateIndex = sheets.Count;
|
||||
var packer = new FrontierPacker(types, CacheFor(stock), PairsFor(stock), stock, axis, beta, Work,
|
||||
BlocksFor(stock), ClampsFor(stock));
|
||||
BlocksFor(stock), ClampsFor(stock),
|
||||
preview == null ? null : placed => preview.Report(stock, plateIndex, placed));
|
||||
var fill = packer.Fill(remaining, token);
|
||||
sheetPacked?.Invoke();
|
||||
if (fill.Parts.Count > 0)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.NestingEngines.Irregular;
|
||||
|
||||
/// <summary>
|
||||
/// Live preview of the sheet a packer is filling, sent as job progress detail
|
||||
/// (<see cref="NestJobProgress.LegacyProgress"/>) for front ends that draw it. Reports are
|
||||
/// throttled to one per <see cref="Interval"/>; the clock only gates reporting and never feeds
|
||||
/// back into packing, so the solve stays deterministic.
|
||||
/// </summary>
|
||||
internal sealed class SheetPreview(
|
||||
IProgress<NestJobProgress> progress,
|
||||
IReadOnlyList<PartType> types,
|
||||
Func<NestJobPart, Drawing> drawingFor
|
||||
)
|
||||
{
|
||||
internal static readonly TimeSpan Interval = TimeSpan.FromMilliseconds(250);
|
||||
|
||||
private readonly Stopwatch clock = Stopwatch.StartNew();
|
||||
private TimeSpan? last;
|
||||
|
||||
/// <summary>Called after each placement on a sheet trial; reports when the interval has passed.</summary>
|
||||
public void Report(NestPlateStock stock, int plateIndex, IReadOnlyList<Placed> placed)
|
||||
{
|
||||
var now = clock.Elapsed;
|
||||
if (last is TimeSpan previous && now - previous < Interval)
|
||||
return;
|
||||
last = now;
|
||||
|
||||
var parts = new List<Part>(placed.Count);
|
||||
foreach (var pose in placed)
|
||||
{
|
||||
// Same pose mapping as the materialized result: rotate about the snapshot origin, then move.
|
||||
var part = new Part(drawingFor(types[pose.Orientation.TypeIndex].Part));
|
||||
part.Rotate(pose.Orientation.Rotation);
|
||||
part.Location = new Vector(pose.X, pose.Y);
|
||||
part.UpdateBounds();
|
||||
parts.Add(part);
|
||||
}
|
||||
|
||||
progress.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stock.Id, plateIndex, 0, 0,
|
||||
new NestProgress
|
||||
{
|
||||
Phase = NestPhase.Custom,
|
||||
PlateNumber = plateIndex + 1,
|
||||
BestParts = parts,
|
||||
ActiveWorkArea = stock.WorkArea,
|
||||
IsOverallBest = true,
|
||||
Description = $"Irregular: packing plate {plateIndex + 1} on stock {stock.Id} ({parts.Count} parts)",
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,13 @@ Curved outlines retain conservative flattening-error padding. This does not rela
|
||||
footprints, no-fit polygons or layout validation tolerances; a part extending beyond the accepted
|
||||
work-area bounds remains invalid.
|
||||
|
||||
While it runs with a progress sink, Irregular previews the sheet it is packing: at most four
|
||||
times a second it reports the placements so far as `NestProgress.BestParts` with the stock's
|
||||
work area, and desktop Auto Nest draws them on the plate view. The preview follows every trial
|
||||
(each stock, sheet, strategy variant and tail re-plan), so it shows the search at work, not the
|
||||
layout that will be kept. Reporting only reads finished placements, so layouts are identical
|
||||
with or without a sink.
|
||||
|
||||
Irregular fills gaps and open notches using outer profiles; it does not yet place parts inside
|
||||
enclosed cutouts. For a part with two or more copies it also offers its best-fit pairs (two copies
|
||||
interlocked, as the Best Fit viewer shows them) alongside the single copies, and places a pair
|
||||
|
||||
Reference in new issue
Block a user