test(fill): characterize unchanged-row validation reuse

This commit is contained in:
aj
2026-09-27 15:10:01 -04:00
parent 1b23ad79f2
commit bc5fd86996
4 changed files with 1163 additions and 0 deletions
@@ -0,0 +1,165 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Tests.BestFit;
using Xunit.Abstractions;
namespace OpenNest.Tests.Fill;
[Collection(nameof(FillCacheCollection))]
[Trait("Category", "FillPerformance")]
public class FillLinearValidationPerformanceTests
{
private readonly ITestOutputHelper output;
public FillLinearValidationPerformanceTests(ITestOutputHelper output) => this.output = output;
[SkippableFact]
public void FillLinearValidation_ReportsStripeAndControls()
{
Skip.IfNot(Environment.GetEnvironmentVariable("OPENNEST_RUN_FILL_PERF") == "1",
"Set OPENNEST_RUN_FILL_PERF=1 to run opt-in fill microbenchmarks.");
// Self-contained, baseline-API-only harness: copy this file byte-for-byte into
// the before tree. Never time a frozen oracle or an overlap-only helper here.
var program = new Program();
program.Codes.Add(new RapidMove(new Vector(0, 0)));
foreach (var point in new[] { new Vector(10, 0), new Vector(10, 3), new Vector(4, 3),
new Vector(4, 8), new Vector(0, 8), new Vector(0, 0) })
program.Codes.Add(new LinearMove(point));
var drawing = new Drawing("concave", program);
var profile = new ShapeProfile(ConvertProgram.ToGeometry(program)
.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList());
Assert.True(profile.Perimeter.IsClosed());
Assert.Empty(profile.Cutouts);
Assert.Equal(50.0, drawing.Area);
Assert.True(profile.Perimeter.Area() > 0);
var first = Part.CreateAtOrigin(drawing, 0);
var second = Part.CreateAtOrigin(drawing, System.Math.PI);
second.Offset(new Vector(10.5, 0));
var pair = FillHelpers.BuildRotatedPattern(new List<Part> { first, second }, 0.37);
var single = FillHelpers.BuildRotatedPattern(new List<Part> { first }, 0.37);
var patterns = new[] { pair, pair, pair, pair, single };
var names = new[] { "horizontal-stripe", "vertical-stripe", "full-grid", "partial-only", "single-seed-stripe" };
var counts = new[] { 8, 19, 36, 29, 8 };
var directions = new[] { NestDirection.Horizontal, NestDirection.Vertical, NestDirection.Horizontal,
NestDirection.Vertical, NestDirection.Horizontal };
var areas = new[]
{
new Box(0, 0, 96, pair.BoundingBox.Width),
new Box(0, 0, pair.BoundingBox.Length, 96),
new Box(0, 0, 96, 48),
new Box(0, 0, 1.8 * pair.BoundingBox.Length, 96),
new Box(0, 0, 96, single.BoundingBox.Width),
};
var fills = areas.Select((area, mode) =>
{
var filler = new FillLinear(area, 0.5);
return new Func<List<Part>>(() => filler.Fill(patterns[mode], directions[mode]));
}).ToArray();
var expected = fills.Select(fill => fill()).ToArray();
for (var mode = 0; mode < fills.Length; mode++)
{
Assert.Equal(counts[mode], expected[mode].Count);
AssertValid(expected[mode], areas[mode]);
}
var warmupCalls = 100;
var callsPerBatch = 200;
var repetitions = 7;
#if DEBUG
output.WriteLine("Configuration=Debug (diagnostic only; use Release for measurements).");
#else
output.WriteLine("Configuration=Release.");
#endif
output.WriteLine($"Runtime={RuntimeInformation.FrameworkDescription}; OS={RuntimeInformation.OSDescription}; "
+ $"architecture={RuntimeInformation.ProcessArchitecture}; processors={Environment.ProcessorCount}; "
+ $"Stopwatch.Frequency={Stopwatch.Frequency} ticks/s.");
output.WriteLine("linear-validation: closed concave (0,0)-(10,0)-(10,3)-(4,3)-(4,8)-(0,8)-(0,0); "
+ "pair at 0/PI, second offset=(10.5,0), BuildRotatedPattern=0.37; spacing=0.5; origin=(0,0). "
+ "Stripe primary span=96, perpendicular span=exact pattern bbox; full-grid=(96,48); "
+ "vertical partial-only perpendicular span=1.8*pair.Length; single rotated seed H stripe. "
+ "Counts in mode order=8,19,36,29,8; full-grid row=8+28; partial-only row=19+10.");
output.WriteLine($"warmup=2 x {warmupCalls} calls/mode; measured={repetitions} x {callsPerBatch} calls/mode. "
+ "Mode order alternates forward/reverse in warmup and measurement. Real synchronous production Fill only; "
+ "setup, correctness assertions and output excluded. Geometry, tiling, overlap checks, GC, delegate/loop, "
+ "count accumulation and last-result assignment included identically in every mode. "
+ "Allocations=GC.GetAllocatedBytesForCurrentThread, not RSS. Warm JIT/drawing, no forced GC. "
+ "Baseline harness only: no reduced-work assertion, timing gate or whole-job speedup claim.");
for (var batch = 0; batch < 2; batch++)
RunBatch("warmup", batch, warmupCalls);
for (var batch = 0; batch < repetitions; batch++)
RunBatch("measured", batch, callsPerBatch);
void RunBatch(string phase, int batch, int calls)
{
// Capture all modes first; assertions/output cannot enter any timed window.
var samples = new Sample[fills.Length];
for (var slot = 0; slot < fills.Length; slot++)
{
var mode = batch % 2 == 0 ? slot : fills.Length - 1 - slot;
samples[mode] = Measure(fills[mode], calls);
}
for (var mode = 0; mode < fills.Length; mode++)
{
var sample = samples[mode];
Assert.Equal((long)calls * counts[mode], sample.PartCount);
AssertSameLayout(expected[mode], sample.LastResult);
AssertValid(sample.LastResult, areas[mode]);
output.WriteLine(FormattableString.Invariant(
$"linear-validation phase={phase} mode={names[mode]} batch={batch + 1} order={(batch % 2 == 0 ? "forward" : "reverse")} calls={calls}: ticks={sample.ElapsedTicks}; ms={sample.ElapsedTicks * 1000.0 / Stopwatch.Frequency:R}; bytes={sample.AllocatedBytes}; parts={sample.PartCount}; last={sample.LastResult.Count}."));
}
}
}
private static Sample Measure(Func<List<Part>> fill, int calls)
{
var count = 0L;
var last = new List<Part>();
var allocatedBefore = GC.GetAllocatedBytesForCurrentThread();
var start = Stopwatch.GetTimestamp();
for (var i = 0; i < calls; i++)
{
last = fill();
count += last.Count;
}
var elapsed = Stopwatch.GetTimestamp() - start;
var allocated = GC.GetAllocatedBytesForCurrentThread() - allocatedBefore;
return new Sample(elapsed, allocated, count, last);
}
private static void AssertValid(List<Part> parts, Box area)
{
Assert.All(parts, p =>
{
Assert.True(p.BaseDrawing.Area > 0);
Assert.True(p.Left >= area.Left - OpenNest.Math.Tolerance.Epsilon
&& p.Right <= area.Right + OpenNest.Math.Tolerance.Epsilon
&& p.Bottom >= area.Bottom - OpenNest.Math.Tolerance.Epsilon
&& p.Top <= area.Top + OpenNest.Math.Tolerance.Epsilon);
});
Assert.False(FillHelpers.HasOverlappingParts(parts));
}
private static void AssertSameLayout(List<Part> expected, List<Part> actual)
{
Assert.Equal(expected.Count, actual.Count);
for (var i = 0; i < expected.Count; i++)
{
Assert.Same(expected[i].BaseDrawing, actual[i].BaseDrawing);
Assert.Same(expected[i].Program, actual[i].Program);
Assert.Equal(PartBits(expected[i]), PartBits(actual[i]));
}
}
private static long[] PartBits(Part part) => new[] { part.Location.X, part.Location.Y, part.Rotation,
part.BoundingBox.X, part.BoundingBox.Y, part.BoundingBox.Length, part.BoundingBox.Width }
.Select(BitConverter.DoubleToInt64Bits).ToArray();
private readonly record struct Sample(long ElapsedTicks, long AllocatedBytes, long PartCount, List<Part> LastResult);
}
@@ -0,0 +1,439 @@
using System.Diagnostics;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Strategies;
using OpenNest.Engine.Tests.Fill;
using OpenNest.Geometry;
using OpenNest.Math;
using OpenNest.Shapes;
using OpenNest.Tests.BestFit;
using Xunit.Abstractions;
namespace OpenNest.Tests.Fill;
[Collection(nameof(FillCacheCollection))]
public class FillLinearValidationReuseTests
{
private readonly ITestOutputHelper output;
public FillLinearValidationReuseTests(ITestOutputHelper output) => this.output = output;
public static IEnumerable<object[]> DrawingCases()
{
foreach (var shape in new[] { "rectangle", "concave", "arc", "circle", "ring" })
foreach (var spacing in new[] { 0.0, 0.5 })
foreach (var angle in new[] { 0.0, 0.37, System.Math.PI / 2 })
foreach (var direction in new[] { NestDirection.Horizontal, NestDirection.Vertical })
yield return new object[] { shape, spacing, angle, direction };
}
[Theory]
[MemberData(nameof(DrawingCases))]
public void DrawingFill_MatchesPreStep3_OrderedBitsProgramsAndImmutableInput(
string shape, double spacing, double angle, NestDirection direction)
{
var drawing = MakeDrawing(shape);
var before = DrawingSnapshot(drawing);
var area = new Box(3.1, -5.3, 42, 29);
var areaBefore = BoxBits(area);
var expected = new PreStep3FillLinear(area, spacing).Fill(drawing, angle, direction);
var filler = new FillLinear(area, spacing);
AssertLayout(expected, filler.Fill(drawing, angle, direction), new[] { drawing.Program });
Assert.Equal(before, DrawingSnapshot(drawing));
AssertLayout(expected, filler.Fill(drawing, angle, direction), new[] { drawing.Program });
AssertValid(expected, area);
Assert.Equal(before, DrawingSnapshot(drawing));
Assert.Equal(areaBefore, BoxBits(area));
Assert.Equal(areaBefore, BoxBits(filler.WorkArea));
}
public static IEnumerable<object[]> PatternCases()
{
foreach (var shape in new[] { "concave", "arc", "circle", "ring" })
foreach (var kind in new[] { "single", "shared", "rotated" })
foreach (var spacing in new[] { 0.0, 0.5 })
foreach (var direction in new[] { NestDirection.Horizontal, NestDirection.Vertical })
yield return new object[] { shape, kind, spacing, direction };
}
[Theory]
[MemberData(nameof(PatternCases))]
public void PatternFill_MatchesPreStep3_SharingAndImmutableInput(
string shape, string kind, double spacing, NestDirection direction)
{
var pattern = MakePattern(shape, kind);
CheckPattern(pattern, new Box(-7.1, 11.3, 52, 39), spacing, direction);
}
[Theory]
[InlineData("horizontal-stripe", 8)]
[InlineData("vertical-stripe", 19)]
[InlineData("full-grid", 36)]
[InlineData("partial-only", 29)]
[InlineData("single-seed-stripe", 8)]
public void CharacterizedControls_MatchPreStep3(string mode, int count)
{
var pattern = MakePattern("concave", mode == "single-seed-stripe" ? "single" : "rotated");
var vertical = mode is "vertical-stripe" or "partial-only";
var direction = vertical ? NestDirection.Vertical : NestDirection.Horizontal;
var area = mode switch
{
"full-grid" => new Box(0, 0, 96, 48),
"partial-only" => new Box(0, 0, 1.8 * pattern.BoundingBox.Length, 96),
"vertical-stripe" => new Box(0, 0, pattern.BoundingBox.Length, 96),
_ => new Box(0, 0, 96, pattern.BoundingBox.Width),
};
var parts = CheckPattern(pattern, area, 0.5, direction);
Assert.Equal(count, parts.Count);
var stripe = vertical ? new Box(0, 0, pattern.BoundingBox.Length, 96)
: new Box(0, 0, 96, pattern.BoundingBox.Width);
var row = new PreStep3FillLinear(stripe, 0.5).Fill(pattern, direction);
if (mode == "full-grid")
{
Assert.Equal(8, row.Count);
Assert.Equal(28, parts.Count - row.Count);
}
if (mode == "partial-only")
{
Assert.Equal(19, row.Count);
Assert.Equal(10, parts.Count - row.Count);
// Fewer appended parts than the row: TilePattern's incomplete-copy path,
// not a full row. The ordered prefix must be unchanged.
AssertLayout(row, parts.Take(row.Count).ToList(), pattern.Parts.Select(p => p.Program).ToArray());
}
output.WriteLine($"{mode}: row={row.Count}; total={parts.Count}; appended={parts.Count - row.Count}");
}
[Theory]
[InlineData(NestDirection.Horizontal, 9)]
[InlineData(NestDirection.Vertical, 4)]
public void PerpendicularOnly_MatchesPreStep3(NestDirection direction, int count)
{
var pattern = MakePattern("concave", "rotated");
var area = direction == NestDirection.Horizontal
? new Box(0, 0, pattern.BoundingBox.Length, 48)
: new Box(0, 0, 48, pattern.BoundingBox.Width);
var seedOnlyArea = new Box(0, 0, pattern.BoundingBox.Length, pattern.BoundingBox.Width);
Assert.Equal(pattern.Parts.Count, new PreStep3FillLinear(seedOnlyArea, 0.5).Fill(pattern, direction).Count);
var parts = CheckPattern(pattern, area, 0.5, direction);
Assert.True(parts.Count > pattern.Parts.Count);
Assert.Equal(count, parts.Count);
output.WriteLine($"PerpOnly {direction}: total={parts.Count}");
}
[Theory]
[InlineData(NestDirection.Horizontal)]
[InlineData(NestDirection.Vertical)]
public void LastCopy_AdjacentDoubleThreshold_MatchesBothPublicFillOverloads(NestDirection direction)
{
var drawing = MakeDrawing("rectangle");
var pattern = new Pattern();
pattern.Parts.Add(Part.CreateAtOrigin(drawing));
pattern.UpdateBounds();
var before = PatternSnapshot(pattern);
var dim = direction == NestDirection.Horizontal ? 10.0 : 8.0;
Box Area(double size) => new(3.1, 5.3, direction == NestDirection.Horizontal ? size : 10,
direction == NestDirection.Vertical ? size : 8);
var rejected = 2 * dim + 0.5;
var accepted = 4 * dim + 1.5;
// Find the actual adjacent-double boundary through the frozen public Fill;
// nominal pitch arithmetic is not an oracle for the accumulated FP endpoint.
for (var i = 0; i < 64; i++)
{
var middle = (rejected + accepted) / 2;
if (new PreStep3FillLinear(Area(middle), 0.5).Fill(pattern, direction).Count < 3)
rejected = middle;
else
accepted = middle;
}
Assert.Equal(Bits(System.Math.BitIncrement(rejected)), Bits(accepted));
foreach (var size in new[] { rejected, accepted })
{
var area = Area(size);
var frozen = new PreStep3FillLinear(area, 0.5);
var filler = new FillLinear(area, 0.5);
var expectedPattern = frozen.Fill(pattern, direction);
var expectedDrawing = frozen.Fill(drawing, 0, direction);
Assert.Equal(size == rejected ? 2 : 3, expectedPattern.Count);
Assert.Equal(expectedPattern.Count, expectedDrawing.Count);
AssertLayout(expectedPattern, filler.Fill(pattern, direction), new[] { pattern.Parts[0].Program });
AssertLayout(expectedDrawing, filler.Fill(drawing, 0, direction), new[] { drawing.Program });
}
Assert.Equal(before, PatternSnapshot(pattern));
output.WriteLine($"{direction}: rejected={rejected:R} accepted={accepted:R}");
}
[Fact]
public void InvalidOverlappingSeeds_PreserveBothFallbacks_EvenWithoutPerpendicularAdditions()
{
// Deliberately invalid: bbox fallback does not repair an overlapping seed.
// Keep separate from assertions that valid fixtures never overlap.
var first = Part.CreateAtOrigin(MakeDrawing("rectangle"));
var pattern = new Pattern();
pattern.Parts.AddRange(new[] { first, first.CloneAtOffset(new Vector(0.25, 0.25)) });
pattern.UpdateBounds();
Assert.True(FillHelpers.HasOverlappingParts(pattern.Parts));
var before = PatternSnapshot(pattern);
var area = new Box(0, 0, 35, 8.25);
var frozen = new PreStep3FillLinear(area, 0.5) { Label = "invalid-frozen" };
var filler = new FillLinear(area, 0.5) { Label = "invalid-production" };
#if DEBUG
using var listener = new FallbackListener();
Trace.Listeners.Add(listener);
try
{
#endif
var expected = frozen.Fill(pattern, NestDirection.Horizontal);
var actual = filler.Fill(pattern, NestDirection.Horizontal);
Assert.Equal(6, expected.Count);
AssertLayout(expected, actual, pattern.Parts.Select(p => p.Program).ToArray());
Assert.True(FillHelpers.HasOverlappingParts(actual));
Assert.Equal(before, PatternSnapshot(pattern));
#if DEBUG
foreach (var label in new[] { "invalid-frozen", "invalid-production" })
{
var records = listener.Records.Where(r => r.Label == label).ToArray();
Assert.Equal(new[] { "Step1-Primary", "Step2-Perp" }, records.Select(r => r.Step));
Assert.All(records, r =>
{
Assert.Equal(6, r.Count);
Assert.Equal("Overlapping pair [0] vs [1]:", r.Pair);
});
output.WriteLine($"{label}: Step1-Primary and Step2-Perp, both total=6 pair=(0,1); zero perpendicular additions");
}
}
finally
{
Trace.Listeners.Remove(listener);
}
#endif
}
[Theory]
[InlineData(NestDirection.Horizontal)]
[InlineData(NestDirection.Vertical)]
public void EmptyNoFitAndMalformedInputs_PreserveResultsAndExceptionTypes(NestDirection direction)
{
var area = new Box(3, 5, 1, 1);
var filler = new FillLinear(area, 0.5);
var frozen = new PreStep3FillLinear(area, 0.5);
var drawing = MakeDrawing("rectangle");
var pattern = MakePattern("concave", "rotated");
var before = PatternSnapshot(pattern);
Assert.Empty(frozen.Fill(new Pattern(), direction));
Assert.Empty(filler.Fill(new Pattern(), direction));
Assert.Empty(frozen.Fill(pattern, direction));
Assert.Empty(filler.Fill(pattern, direction));
Assert.Empty(frozen.Fill(drawing, 0.37, direction));
Assert.Empty(filler.Fill(drawing, 0.37, direction));
Assert.Equal(before, PatternSnapshot(pattern));
Assert.Throws<NullReferenceException>(() => frozen.Fill((Pattern)null!, direction));
Assert.Throws<NullReferenceException>(() => filler.Fill((Pattern)null!, direction));
Assert.Throws<NullReferenceException>(() => frozen.Fill((Drawing)null!, 0, direction));
Assert.Throws<NullReferenceException>(() => filler.Fill((Drawing)null!, 0, direction));
Assert.Throws<NullReferenceException>(() => new PreStep3FillLinear(null!, 0.5));
Assert.Throws<NullReferenceException>(() => new FillLinear(null!, 0.5));
foreach (var rapidOnly in new[] { false, true })
{
var program = new Program();
if (rapidOnly)
program.Codes.Add(new RapidMove(new Vector(0, 0)));
var empty = new Drawing("empty-material", program);
var emptyBefore = DrawingSnapshot(empty);
var emptyPattern = new Pattern();
emptyPattern.Parts.Add(Part.CreateAtOrigin(empty));
emptyPattern.UpdateBounds();
var patternBefore = PatternSnapshot(emptyPattern);
Assert.Throws<ArgumentOutOfRangeException>(() => frozen.Fill(empty, 0, direction));
Assert.Throws<ArgumentOutOfRangeException>(() => filler.Fill(empty, 0, direction));
Assert.Throws<ArgumentOutOfRangeException>(() => frozen.Fill(emptyPattern, direction));
Assert.Throws<ArgumentOutOfRangeException>(() => filler.Fill(emptyPattern, direction));
Assert.Equal(emptyBefore, DrawingSnapshot(empty));
Assert.Equal(patternBefore, PatternSnapshot(emptyPattern));
}
}
[Fact]
public void ConcurrentIndependentCalls_OneFiller_MatchFrozenAndPreserveInputs()
{
var filler = new FillLinear(new Box(3.1, -5.3, 52, 39), 0.5);
var frozen = new PreStep3FillLinear(filler.WorkArea, 0.5);
var patterns = Enumerable.Range(0, 24).Select(i => MakePattern(i % 2 == 0 ? "concave" : "arc",
i % 3 == 0 ? "shared" : i % 3 == 1 ? "rotated" : "single")).ToArray();
var drawings = patterns.Select(p => p.Parts[0].BaseDrawing).ToArray();
var before = patterns.Select(PatternSnapshot).ToArray();
var expectedPatterns = patterns.Select((p, i) => frozen.Fill(p,
i % 2 == 0 ? NestDirection.Horizontal : NestDirection.Vertical)).ToArray();
var expectedDrawings = drawings.Select((d, i) => frozen.Fill(d, 0.37,
i % 2 == 0 ? NestDirection.Horizontal : NestDirection.Vertical)).ToArray();
Parallel.For(0, patterns.Length, new ParallelOptions { MaxDegreeOfParallelism = 4 }, i =>
{
var direction = i % 2 == 0 ? NestDirection.Horizontal : NestDirection.Vertical;
AssertLayout(expectedPatterns[i], filler.Fill(patterns[i], direction), patterns[i].Parts.Select(p => p.Program).ToArray());
AssertLayout(expectedDrawings[i], filler.Fill(drawings[i], 0.37, direction), new[] { drawings[i].Program });
Assert.Equal(before[i], PatternSnapshot(patterns[i]));
});
}
private static Drawing MakeDrawing(string shape)
{
var drawing = shape switch
{
"circle" => new CircleShape { Diameter = 8 }.GetDrawing(),
"ring" => new RingShape { OuterDiameter = 8, InnerDiameter = 3 }.GetDrawing(),
_ => FillExtentsTests.MakeFixture(shape),
};
var profile = new ShapeProfile(ConvertProgram.ToGeometry(drawing.Program)
.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList());
Assert.True(profile.Perimeter.IsClosed());
Assert.All(profile.Cutouts, cutout => Assert.True(cutout.IsClosed()));
Assert.True(profile.Perimeter.Area() - profile.Cutouts.Sum(c => c.Area()) > 0);
Assert.True(drawing.Area > 0);
if (shape == "concave")
Assert.Equal(50.0, drawing.Area);
if (shape is "arc" or "circle" or "ring")
Assert.Contains(drawing.Program.Codes, code => code is ArcMove);
return drawing;
}
private static Pattern MakePattern(string shape, string kind)
{
var drawing = MakeDrawing(shape);
var first = Part.CreateAtOrigin(drawing, 0);
if (kind == "single")
return FillHelpers.BuildRotatedPattern(new List<Part> { first }, 0.37);
var second = kind == "shared" ? first.CloneAtOffset(new Vector(10.5, 0))
: Part.CreateAtOrigin(drawing, System.Math.PI);
if (kind != "shared")
second.Offset(new Vector(10.5, 0));
if (kind == "rotated")
return FillHelpers.BuildRotatedPattern(new List<Part> { first, second }, 0.37);
var pattern = new Pattern();
pattern.Parts.AddRange(new[] { first, second });
pattern.UpdateBounds();
Assert.Same(first.Program, second.Program);
return pattern;
}
private static List<Part> CheckPattern(Pattern pattern, Box area, double spacing, NestDirection direction)
{
var before = PatternSnapshot(pattern);
var areaBefore = BoxBits(area);
var inputs = pattern.Parts.Select(p => p.Program).ToArray();
Assert.False(FillHelpers.HasOverlappingParts(pattern.Parts));
var expected = new PreStep3FillLinear(area, spacing).Fill(pattern, direction);
Assert.Equal(before, PatternSnapshot(pattern));
var filler = new FillLinear(area, spacing);
var actual = filler.Fill(pattern, direction);
AssertLayout(expected, actual, inputs);
Assert.Equal(before, PatternSnapshot(pattern));
AssertLayout(expected, filler.Fill(pattern, direction), inputs);
Assert.Equal(before, PatternSnapshot(pattern));
AssertValid(actual, area);
Assert.Equal(areaBefore, BoxBits(area));
Assert.Equal(areaBefore, BoxBits(filler.WorkArea));
return actual;
}
private static void AssertValid(List<Part> parts, Box area)
{
Assert.NotEmpty(parts);
Assert.All(parts, p =>
{
Assert.True(p.Left >= area.Left - Tolerance.Epsilon && p.Right <= area.Right + Tolerance.Epsilon);
Assert.True(p.Bottom >= area.Bottom - Tolerance.Epsilon && p.Top <= area.Top + Tolerance.Epsilon);
});
Assert.False(FillHelpers.HasOverlappingParts(parts));
}
private static long Bits(double value) => BitConverter.DoubleToInt64Bits(value);
private static long[] BoxBits(Box box) => new[] { Bits(box.X), Bits(box.Y), Bits(box.Length), Bits(box.Width) };
private static object[] ProgramValues(Program program)
{
var values = new List<object> { program.Mode, Bits(program.Rotation), program.Codes.Count };
values.AddRange(BoxBits(program.BoundingBox()).Cast<object>());
foreach (var code in program.Codes)
{
values.Add(code.GetType());
var motion = Assert.IsAssignableFrom<Motion>(code);
values.AddRange(new object[] { code.Type, Bits(motion.EndPoint.X), Bits(motion.EndPoint.Y),
motion.Feedrate, motion.UseExactStop, motion.Suppressed, motion.VariableRefs?.Count ?? -1 });
if (motion.VariableRefs != null)
foreach (var entry in motion.VariableRefs.OrderBy(e => e.Key))
values.AddRange(new object[] { entry.Key, entry.Value });
if (code is LinearMove line)
values.Add(line.Layer);
else if (code is ArcMove arc)
values.AddRange(new object[] { Bits(arc.CenterPoint.X), Bits(arc.CenterPoint.Y), arc.Rotation, arc.Layer });
else
Assert.IsType<RapidMove>(code);
}
return values.ToArray();
}
private static object[] DrawingSnapshot(Drawing drawing) => new object[] { drawing, drawing.Program, Bits(drawing.Area) }
.Concat(drawing.Program.Codes.Cast<object>()).Concat(ProgramValues(drawing.Program)).ToArray();
private static object[] PatternSnapshot(Pattern pattern) => BoxBits(pattern.BoundingBox).Cast<object>()
.Concat(pattern.Parts.SelectMany(p => new object[] { p, p.Program, Bits(p.Location.X), Bits(p.Location.Y), Bits(p.Rotation) }
.Concat(BoxBits(p.BoundingBox).Cast<object>()).Concat(p.Program.Codes.Cast<object>())
.Concat(ProgramValues(p.Program)).Concat(DrawingSnapshot(p.BaseDrawing)))).ToArray();
private static void AssertLayout(List<Part> expected, List<Part> actual, Program[] inputs)
{
Assert.Equal(expected.Count, actual.Count);
for (var i = 0; i < expected.Count; i++)
{
Assert.Same(expected[i].BaseDrawing, actual[i].BaseDrawing);
Assert.Equal(Bits(expected[i].Location.X), Bits(actual[i].Location.X));
Assert.Equal(Bits(expected[i].Location.Y), Bits(actual[i].Location.Y));
Assert.Equal(Bits(expected[i].Rotation), Bits(actual[i].Rotation));
Assert.Equal(BoxBits(expected[i].BoundingBox), BoxBits(actual[i].BoundingBox));
Assert.Equal(ProgramValues(expected[i].Program), ProgramValues(actual[i].Program));
foreach (var input in inputs)
Assert.Equal(ReferenceEquals(expected[i].Program, input), ReferenceEquals(actual[i].Program, input));
for (var j = 0; j < expected.Count; j++)
Assert.Equal(ReferenceEquals(expected[i].Program, expected[j].Program),
ReferenceEquals(actual[i].Program, actual[j].Program));
}
}
#if DEBUG
private sealed class FallbackListener : TraceListener
{
internal sealed class Entry
{
public string Label = "";
public string Step = "";
public int Count;
public string Pair = "";
}
public List<Entry> Records { get; } = new();
private Entry? current;
public override void Write(string? message) { }
public override void WriteLine(string? message)
{
var line = message?.Trim() ?? "";
const string prefix = "[FillLinear] OVERLAP FALLBACK (";
if (line.StartsWith(prefix, StringComparison.Ordinal))
{
current = new Entry { Label = line[prefix.Length..^1] };
Records.Add(current);
}
else if (current != null && line.StartsWith("Step: ", StringComparison.Ordinal))
current.Step = line[6..].Split(',')[0];
else if (current != null && line.StartsWith("Total parts after tiling: ", StringComparison.Ordinal))
current.Count = int.Parse(line[26..], System.Globalization.CultureInfo.InvariantCulture);
else if (current != null && line.StartsWith("Overlapping pair ", StringComparison.Ordinal))
current.Pair = line;
}
}
#endif
}
+540
View File
@@ -0,0 +1,540 @@
// Frozen from OpenNest.Engine/Fill/FillLinear.cs at
// 1b23ad79f25d77fdd745bfea029de9dff2a91f6d (before unchanged-row validation reuse).
// Mechanical edits only: type/constructor renamed to PreStep3FillLinear;
// namespace changed to OpenNest.Engine.Tests.Fill; class made internal;
// Engine/Fill imports added; nullable disabled; import ordering/formatting only.
// Keep independent of future FillLinear edits. LegacyFillLinear is an older oracle.
#nullable disable
using System.Collections.Generic;
using System.Diagnostics;
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.Tests.Fill
{
internal class PreStep3FillLinear
{
// Owned by one public call: FillHelpers can use this filler concurrently.
// Cached local entities never escape; only translated clones reach spatial queries.
private sealed class OffsetPerimeterCache
{
private readonly Dictionary<CNC.Program, List<Entity>> perimeters =
new Dictionary<CNC.Program, List<Entity>>(ReferenceEqualityComparer.Instance);
private readonly double spacing;
public OffsetPerimeterCache(double spacing) => this.spacing = spacing;
public List<Entity> AtLocation(CNC.Program program, Vector location)
{
if (!perimeters.TryGetValue(program, out var local))
{
local = PartGeometry.GetOffsetPerimeterEntities(program, spacing);
perimeters.Add(program, local);
}
var result = new List<Entity>(local.Count);
foreach (var entity in local)
{
var clone = entity.Clone();
clone.Offset(location);
result.Add(clone);
}
return result;
}
}
public PreStep3FillLinear(Box workArea, double partSpacing)
{
PartSpacing = partSpacing;
WorkArea = new Box(workArea.X, workArea.Y, workArea.Length, workArea.Width);
}
public Box WorkArea { get; }
public double PartSpacing { get; }
public double HalfSpacing => PartSpacing / 2;
/// <summary>
/// Diagnostic label set by callers to identify the engine/context in overlap logs.
/// </summary>
public string Label { get; set; }
private static Vector MakeOffset(NestDirection direction, double distance)
{
return direction == NestDirection.Horizontal
? new Vector(distance, 0)
: new Vector(0, distance);
}
private static PushDirection GetPushDirection(NestDirection direction)
{
return direction == NestDirection.Horizontal ? PushDirection.Left : PushDirection.Down;
}
private static double GetDimension(Box box, NestDirection direction)
{
return direction == NestDirection.Horizontal ? box.Length : box.Width;
}
private static double GetStart(Box box, NestDirection direction)
{
return direction == NestDirection.Horizontal ? box.Left : box.Bottom;
}
private double GetLimit(NestDirection direction)
{
return direction == NestDirection.Horizontal ? WorkArea.Right : WorkArea.Top;
}
private static NestDirection PerpendicularAxis(NestDirection direction)
{
return direction == NestDirection.Horizontal
? NestDirection.Vertical
: NestDirection.Horizontal;
}
/// <summary>
/// Finds the geometry-aware copy distance between two identical parts along an axis.
/// Uses native Line/Arc entities (inflated by half-spacing) so curves are handled
/// exactly without polygon sampling error.
/// </summary>
private double FindCopyDistance(Part partA, NestDirection direction, OffsetPerimeterCache cache)
{
var bboxDim = GetDimension(partA.BoundingBox, direction);
var pushDir = GetPushDirection(direction);
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
var offset = MakeOffset(direction, startOffset);
var stationaryEntities = cache.AtLocation(partA.Program, partA.Location);
var movingEntities = cache.AtLocation(partA.Program, partA.Location + offset);
var slideDistance = SpatialQuery.DirectionalDistance(
movingEntities,
stationaryEntities,
pushDir
);
if (slideDistance >= double.MaxValue || slideDistance < 0)
return bboxDim + PartSpacing;
return startOffset - slideDistance;
}
/// <summary>
/// Finds the geometry-aware copy distance between two identical patterns along an axis.
/// Checks every pair of parts across adjacent pattern copies so multi-part patterns
/// (e.g. interlocking pairs) maintain spacing between ALL parts. Uses native entity
/// geometry inflated by half-spacing — same primitive the Compactor uses — so arcs
/// are exact and no bbox clamp is needed.
/// </summary>
private double FindPatternCopyDistance(Pattern patternA, NestDirection direction, OffsetPerimeterCache cache)
{
if (patternA.Parts.Count == 1)
return FindCopyDistance(patternA.Parts[0], direction, cache);
var bboxDim = GetDimension(patternA.BoundingBox, direction);
var pushDir = GetPushDirection(direction);
var opposite = SpatialQuery.OppositeDirection(pushDir);
var dirVec = SpatialQuery.DirectionToOffset(pushDir, 1.0);
// 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);
var parts = patternA.Parts;
var stationaryBoxes = new Box[parts.Count];
var movingBoxes = new Box[parts.Count];
var stationaryEntities = new List<Entity>[parts.Count];
var movingEntities = new List<Entity>[parts.Count];
for (var i = 0; i < parts.Count; i++)
{
stationaryBoxes[i] = parts[i].BoundingBox;
movingBoxes[i] = stationaryBoxes[i].Translate(offset);
}
var maxCopyDistance = 0.0;
for (var j = 0; j < parts.Count; j++)
{
var movingBox = movingBoxes[j];
for (var i = 0; i < parts.Count; i++)
{
var stationaryBox = stationaryBoxes[i];
// Skip if stationary is already ahead of moving in the push direction
// (sliding forward would take them further apart).
if (SpatialQuery.DirectionalGap(movingBox, stationaryBox, opposite) > 0)
continue;
// Skip if bboxes can't overlap along the axis perpendicular to the push.
if (!SpatialQuery.PerpendicularOverlap(movingBox, stationaryBox, dirVec))
continue;
stationaryEntities[i] ??= cache.AtLocation(parts[i].Program, parts[i].Location);
movingEntities[j] ??= cache.AtLocation(parts[j].Program, parts[j].Location + offset);
var slideDistance = SpatialQuery.DirectionalDistance(
movingEntities[j],
stationaryEntities[i],
pushDir
);
if (slideDistance >= double.MaxValue || slideDistance < 0)
continue;
var copyDist = startOffset - slideDistance;
if (copyDist > maxCopyDistance)
maxCopyDistance = copyDist;
}
}
return maxCopyDistance;
}
/// <summary>
/// Tiles a pattern along the given axis, returning the cloned parts
/// (does not include the original pattern's parts). For multi-part
/// patterns, also adds individual parts from the next incomplete copy
/// that still fit within the work area.
/// </summary>
private List<Part> TilePattern(Pattern basePattern, NestDirection direction, OffsetPerimeterCache cache)
{
var copyDistance = FindPatternCopyDistance(basePattern, direction, cache);
if (copyDistance <= 0)
return new List<Part>();
var dim = GetDimension(basePattern.BoundingBox, direction);
var start = GetStart(basePattern.BoundingBox, direction);
var limit = GetLimit(direction);
var estimatedCopies = (int)((limit - start - dim) / copyDistance);
var result = new List<Part>(estimatedCopies * basePattern.Parts.Count);
var count = 1;
while (true)
{
var nextPos = start + copyDistance * count;
if (nextPos + dim > limit + Tolerance.Epsilon)
break;
var offset = MakeOffset(direction, copyDistance * count);
foreach (var part in basePattern.Parts)
result.Add(part.CloneAtOffset(offset));
count++;
}
// For multi-part patterns, try to place individual parts from the
// next copy that didn't fit as a whole. This handles cases where
// e.g. a 2-part pair only partially fits — one part may still be
// within the work area even though the full pattern exceeds it.
if (basePattern.Parts.Count > 1)
{
var offset = MakeOffset(direction, copyDistance * count);
foreach (var basePart in basePattern.Parts)
{
var part = basePart.CloneAtOffset(offset);
if (
part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon
&& part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon
&& part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon
&& part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon
)
{
result.Add(part);
}
}
}
return result;
}
/// <summary>
/// Fallback tiling using bounding-box spacing when geometry-aware tiling
/// produces overlapping parts.
/// </summary>
private List<Part> TilePatternBbox(Pattern basePattern, NestDirection direction)
{
var copyDistance = GetDimension(basePattern.BoundingBox, direction) + PartSpacing;
if (copyDistance <= 0)
return new List<Part>();
var dim = GetDimension(basePattern.BoundingBox, direction);
var start = GetStart(basePattern.BoundingBox, direction);
var limit = GetLimit(direction);
var result = new List<Part>();
var count = 1;
while (true)
{
var nextPos = start + copyDistance * count;
if (nextPos + dim > limit + Tolerance.Epsilon)
break;
var offset = MakeOffset(direction, copyDistance * count);
foreach (var part in basePattern.Parts)
result.Add(part.CloneAtOffset(offset));
count++;
}
return result;
}
private static bool HasOverlappingParts(
List<Part> parts,
out int overlapA,
out int overlapB
)
{
var checker = new PartOverlapChecker();
for (var i = 0; i < parts.Count; i++)
{
var b1 = parts[i].BoundingBox;
for (var j = i + 1; j < parts.Count; j++)
{
var b2 = parts[j].BoundingBox;
var overlapX =
System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left);
var overlapY =
System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
continue;
if (checker.Overlaps(parts[i], parts[j]))
{
overlapA = i;
overlapB = j;
return true;
}
}
}
overlapA = -1;
overlapB = -1;
return false;
}
/// <summary>
/// Creates a seed pattern containing a single part positioned at the work area origin.
/// Returns an empty pattern if the part does not fit.
/// </summary>
private Pattern MakeSeedPattern(Drawing drawing, double rotationAngle)
{
var pattern = new Pattern();
var template = new Part(drawing);
if (!rotationAngle.IsEqualTo(0))
template.Rotate(rotationAngle);
template.Offset(WorkArea.Location - template.BoundingBox.Location);
if (
template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon
|| template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon
)
return pattern;
pattern.Parts.Add(template);
pattern.UpdateBounds();
return pattern;
}
/// <summary>
/// Fills the work area by tiling the pattern along the primary axis to form
/// a row, then tiling that row along the perpendicular axis to form a grid.
/// After the grid is formed, fills the remaining strip with individual parts.
/// </summary>
private List<Part> FillGrid(Pattern pattern, NestDirection direction, OffsetPerimeterCache cache)
{
var perpAxis = PerpendicularAxis(direction);
// Step 1: Tile along primary axis
var row = new List<Part>(pattern.Parts);
row.AddRange(TilePattern(pattern, direction, cache));
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a1, out var b1))
{
LogOverlap("Step1-Primary", direction, pattern, row, a1, b1);
row = new List<Part>(pattern.Parts);
row.AddRange(TilePatternBbox(pattern, direction));
}
// If primary tiling didn't produce copies, just tile along perpendicular
if (row.Count <= pattern.Parts.Count)
{
row.AddRange(TilePattern(pattern, perpAxis, cache));
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a2, out var b2))
{
LogOverlap("Step1-PerpOnly", perpAxis, pattern, row, a2, b2);
row = new List<Part>(pattern.Parts);
row.AddRange(TilePatternBbox(pattern, perpAxis));
}
return row;
}
// Step 2: Build row pattern and tile along perpendicular axis
var rowPattern = new Pattern();
rowPattern.Parts.AddRange(row);
rowPattern.UpdateBounds();
var gridResult = new List<Part>(rowPattern.Parts);
gridResult.AddRange(TilePattern(rowPattern, perpAxis, cache));
if (HasOverlappingParts(gridResult, out var a3, out var b3))
{
LogOverlap("Step2-Perp", perpAxis, rowPattern, gridResult, a3, b3);
gridResult = new List<Part>(rowPattern.Parts);
gridResult.AddRange(TilePatternBbox(rowPattern, perpAxis));
}
return gridResult;
}
private void LogOverlap(
string step,
NestDirection tilingDir,
Pattern pattern,
List<Part> parts,
int idxA,
int idxB
)
{
var pa = parts[idxA];
var pb = parts[idxB];
var ba = pa.BoundingBox;
var bb = pb.BoundingBox;
Debug.WriteLine($"[FillLinear] OVERLAP FALLBACK ({Label ?? "unknown"})");
Debug.WriteLine($" Step: {step}, TilingDir: {tilingDir}");
Debug.WriteLine(
$" WorkArea: ({WorkArea.X:F4},{WorkArea.Y:F4}) {WorkArea.Width:F4}x{WorkArea.Length:F4}, Spacing: {PartSpacing}"
);
Debug.WriteLine(
$" Pattern: {pattern.Parts.Count} parts, bbox {pattern.BoundingBox.Width:F4}x{pattern.BoundingBox.Length:F4}"
);
Debug.WriteLine($" Total parts after tiling: {parts.Count}");
Debug.WriteLine($" Overlapping pair [{idxA}] vs [{idxB}]:");
Debug.WriteLine(
$" [{idxA}]: drawing={pa.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pa.Rotation):F2}° "
+ $"loc=({pa.Location.X:F4},{pa.Location.Y:F4}) bbox=({ba.Left:F4},{ba.Bottom:F4})-({ba.Right:F4},{ba.Top:F4})"
);
Debug.WriteLine(
$" [{idxB}]: drawing={pb.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pb.Rotation):F2}° "
+ $"loc=({pb.Location.X:F4},{pb.Location.Y:F4}) bbox=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4})"
);
// Log all pattern seed parts for reproduction
Debug.WriteLine($" Pattern seed parts:");
for (var i = 0; i < pattern.Parts.Count; i++)
{
var p = pattern.Parts[i];
Debug.WriteLine(
$" [{i}]: drawing={p.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(p.Rotation):F2}° "
+ $"loc=({p.Location.X:F4},{p.Location.Y:F4}) bbox={p.BoundingBox.Width:F4}x{p.BoundingBox.Length:F4}"
);
}
}
/// <summary>
/// Fills a single row of identical parts along one axis using geometry-aware spacing.
/// </summary>
public Pattern FillRow(Drawing drawing, double rotationAngle, NestDirection direction)
{
var cache = new OffsetPerimeterCache(HalfSpacing);
var seed = MakeSeedPattern(drawing, rotationAngle);
if (seed.Parts.Count == 0)
return seed;
var template = seed.Parts[0];
var copyDistance = FindCopyDistance(template, direction, cache);
if (copyDistance <= 0)
return seed;
var dim = GetDimension(template.BoundingBox, direction);
var start = GetStart(template.BoundingBox, direction);
var limit = GetLimit(direction);
var count = 1;
while (true)
{
var nextPos = start + copyDistance * count;
if (nextPos + dim > limit + Tolerance.Epsilon)
break;
var clone = template.CloneAtOffset(MakeOffset(direction, copyDistance * count));
seed.Parts.Add(clone);
count++;
}
seed.UpdateBounds();
return seed;
}
/// <summary>
/// Fills the work area by tiling a pre-built pattern along both axes.
/// </summary>
public List<Part> Fill(Pattern pattern, NestDirection primaryAxis)
{
var cache = new OffsetPerimeterCache(HalfSpacing);
if (pattern.Parts.Count == 0)
return new List<Part>();
var offset = WorkArea.Location - pattern.BoundingBox.Location;
var basePattern = pattern.Clone(offset);
if (
basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon
|| basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon
)
return new List<Part>();
return FillGrid(basePattern, primaryAxis, cache);
}
/// <summary>
/// Fills the work area by creating a seed part, then recursively tiling
/// along the primary axis and then the perpendicular axis.
/// </summary>
public List<Part> Fill(Drawing drawing, double rotationAngle, NestDirection primaryAxis)
{
var cache = new OffsetPerimeterCache(HalfSpacing);
var seed = MakeSeedPattern(drawing, rotationAngle);
if (seed.Parts.Count == 0)
return new List<Part>();
return FillGrid(seed, primaryAxis, cache);
}
}
}
+19
View File
@@ -11,6 +11,25 @@ Only the exact value `1` enables these tests; otherwise they skip; [README](../.
The category covers comparer, group-pattern, rotated-pattern, extents-column, feature-extraction, no-model angle, and FillLinear offset-geometry workloads; individual filters match benchmark method names in `FillPerformanceTests.cs`. Overlap checks are measured separately by `OverlapCheck_ReportsPolygonPairsAndGridChecks` in `OpenNest.Tests/Fill/OverlapCheckPerformanceTests.cs` (same category). Keep harness, inputs, warmups and batches identical before/after; exclude setup/assertions from timing. Comparer/extents allocations are synchronous and current-thread only; parallel group fills omit allocation totals. No timing CI gates or whole-job speedup claims. Preserve evidence in [the measured report](fill-performance.md).
### FillLinear unchanged-row validation baseline
```bash
# Both configurations must characterize unchanged production; unset perf flag skips.
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Debug \
--filter 'FullyQualifiedName~FillLinearValidation' --logger 'trx;LogFileName=linear-validation-debug.trx'
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
--filter 'FullyQualifiedName~FillLinearValidation' --logger 'trx;LogFileName=linear-validation-release.trx'
OPENNEST_RUN_FILL_PERF=1 dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
--filter 'FullyQualifiedName~FillLinearValidation_ReportsStripeAndControls' \
--logger 'console;verbosity=detailed' --logger 'trx;LogFileName=linear-validation-perf.trx'
```
`FillLinearValidationPerformanceTests.cs` is self-contained against base APIs: byte-copy and hash the same file in both trees. It times complete production `Fill` calls (not frozen code or overlap-only helpers): horizontal/vertical stripes, full grid, partial-only additions, and single-seed stripe. The closed concave fixture gives counts **8 / 19 / 36 / 29 / 8**; grid is row 8 + 28 additions, partial-only is row 19 + 10 (no full row). Two 100-call warmups and seven 200-call batches per mode alternate forward/reverse mode order. Retain every raw tick/time/current-thread allocation row, including warmups; setup/assertions/output are excluded, count/last-result consumption is identical. No timing or reduced-work assertions belong in this baseline harness.
`PreStep3FillLinear.cs` freezes production `FillLinear` at **1b23ad79f25d77fdd745bfea029de9dff2a91f6d**, separately from the older `LegacyFillLinear`. Reverse only the documented type/constructor rename, namespace/visibility/import additions, nullable directive and import formatting; retain a zero-diff comparison against `git show <base>:OpenNest.Engine/Fill/FillLinear.cs` and SHA-256 hashes. Do not refactor the oracle or use it for before timings. `FillLinearValidationReuseTests` compares ordered IEEE-754 poses/bounds, CNC values/program sharing, drawing references and immutable inputs through both public `Fill` overloads, including concurrent calls on one filler. Valid fixtures assert closed positive-area material and no overlap. The separate invalid overlapping-seed case intentionally remains invalid; in Debug its listener confirms Step1 and Step2 fallback pair `(0,1)` with six parts at each step (zero perpendicular additions). It makes no claim that bbox fallback repairs the seeds.
Keep future validation-work counter assertions Debug-only in `FillCacheCollection`, reset counters in `finally`, and retain the invalid fallback and single-seed/partial-only controls. This baseline does not assert a future skipped check or introduce production counters. Whole-job exact preservation needs a separately repeated serial oracle (fresh process, `DOTNET_PROCESSOR_COUNT=1`, verified one-worker thread-pool cap, solve on that worker); ordinary parallel layouts can differ on the same tree, and the serial gate does not replace concurrent differential tests.
Debug behavior/skipped-work checks:
```bash