mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-14 17:11:57 -04:00
fix(rectangles): keep large-spacing clamp blocks off the clamp
When part spacing exceeds a clamp's inflated span the per-axis block interval [zone - clearance + s, zone + clearance] inverts. The old collapse to the zone centerline was not conservative: a part could start exactly on the centerline with material inside the clamp (PM d86d22c9 - the probe rail Box(0,0,5,0.625) at part spacing 4 completed at y=0.3125). Per decision D11, an inverted span now blocks a zero-extent line at the clamp edge facing the sheet interior: zone max + clearance when the zone center is in the bin's lower half on that axis, else zone min - clearance + s, so interior-side parts keep the exact bottom-edge clearance and the unavoidable extra exclusion lands toward the nearer sheet edge. Failing-first: the PM probe fixture in quadrants 1-4 and two X-inverted fixtures (mid-rail and far-rail) were red on ab50900 against an independent material-vs-inflated-footprint oracle; all six now pass. Non-inverted spans stay bit-for-bit identical: four keep-row layout digests captured from the unfixed ab50900 packer are unchanged, the between-clamp rows still pack the gap, and the three zero-clamp C2 baseline hashes are untouched. Full Engine suite 929 passed/2 existing skips.
This commit is contained in:
1 parent
83ae1a48f0
commit
3f0a2703fd
2 files changed
+176
-9
No files matched your search
@@ -147,4 +147,159 @@ public class RectanglesClampZoneTests
|
||||
LayoutAssert.Valid(jobClamped, clamped);
|
||||
Assert.Equal(LayoutHash(plain), LayoutHash(clamped));
|
||||
}
|
||||
|
||||
// ---- large part spacing vs shallow clamp spans (PM d86d22c9, decision D11) --------
|
||||
//
|
||||
// The packer reserves a trailing part-spacing tail behind every item, so the block it
|
||||
// carves per axis is [zone - clearance + s, zone + clearance]. When s exceeds the
|
||||
// inflated span, that interval inverts. D11: block a zero-extent line at the clamp
|
||||
// edge facing the sheet interior — at (zone max + clearance) when the zone center lies
|
||||
// in the lower half of the bin on that axis, else at (zone min - clearance + s). The
|
||||
// old collapse to the zone centerline let a part start inside the clamp.
|
||||
//
|
||||
// The oracle below is independent of the packer and of NestLayoutCheck: material
|
||||
// [x, x+w] x [y, y+h] must not share interior with the clamp footprint predicted
|
||||
// directly from the rail rule (center +/- width/2 from the origin edge, depth into the
|
||||
// sheet, translated exactly like the work area), inflated by the bottom-edge spacing.
|
||||
// Boundary contact is clear (C16).
|
||||
|
||||
private static Box InflatedRail(int quadrant, double length, double width,
|
||||
double center, double clampWidth, double depth, double clearance)
|
||||
{
|
||||
var left = quadrant is 1 or 4 ? 0.0 : -length;
|
||||
var bottom = quadrant is 1 or 2 ? 0.0 : -width;
|
||||
var near = center - clampWidth / 2 + left;
|
||||
var far = center + clampWidth / 2 + left;
|
||||
return new Box(
|
||||
System.Math.Min(near, far) - clearance,
|
||||
bottom - clearance,
|
||||
clampWidth + 2 * clearance,
|
||||
depth + 2 * clearance);
|
||||
}
|
||||
|
||||
private static void AssertMaterialClearOfRail(NestJobResult result, Box inflated,
|
||||
double partW, double partH)
|
||||
{
|
||||
Assert.NotEmpty(result.Plates.SelectMany(p => p.Placements));
|
||||
foreach (var p in result.Plates.SelectMany(pl => pl.Placements))
|
||||
{
|
||||
var overlaps = p.X < inflated.Right - 1e-9 && p.X + partW > inflated.Left + 1e-9
|
||||
&& p.Y < inflated.Top - 1e-9 && p.Y + partH > inflated.Bottom + 1e-9;
|
||||
Assert.False(overlaps,
|
||||
$"material [{p.X},{p.X + partW}] x [{p.Y},{p.Y + partH}] intrudes into the " +
|
||||
$"inflated clamp footprint [{inflated.Left},{inflated.Right}] x " +
|
||||
$"[{inflated.Bottom},{inflated.Top}]");
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// PM d86d22c9 probe fixture: Y degenerate (s=4 > 0.625 + 2x0.25), full-width front
|
||||
// rail. Red on ab50900: Rectangles completed at y=0.3125, material on the clamp.
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[InlineData(4)]
|
||||
public void DegenerateYSpanKeepsExactClearanceInEveryQuadrant(int quadrant)
|
||||
{
|
||||
var stock = Stock("sheet", 4, 5, spacing: 4, edge: new Spacing(0, 0.25, 0, 0),
|
||||
quadrant: quadrant, clampZones: [new ClampZone("rail", new Box(0, 0, 5, 0.625))]);
|
||||
var job = Job(new[] { Rectangle("square", 2, 2, 1) }, new[] { stock });
|
||||
|
||||
var result = Engine().Solve(job);
|
||||
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
AssertMaterialClearOfRail(result,
|
||||
InflatedRail(quadrant, 5, 4, 2.5, 5, 0.625, 0.25), 2, 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DegenerateXSpanMidRailKeepsExactClearance()
|
||||
{
|
||||
// X degenerate (s=3 > 1.5 + 2x0.25); the 10-wide part is forced across the clamp's
|
||||
// X span because neither side can hold it with the tail. Red on ab50900: completed
|
||||
// at x=11.5 (the centerline), material inside the clamp.
|
||||
var stock = Stock("sheet", 4, 23, spacing: 3, edge: new Spacing(0, 0.25, 0, 0),
|
||||
clampZones: [new ClampZone("mid-rail", new Box(10.75, 0, 1.5, 0.625))]);
|
||||
var job = Job(new[] { Rectangle("wide", 10, 2, 1) }, new[] { stock });
|
||||
|
||||
var result = Engine().Solve(job);
|
||||
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
AssertMaterialClearOfRail(result, InflatedRail(1, 23, 4, 11.5, 1.5, 0.625, 0.25), 10, 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DegenerateXSpanFarRailClearsTheInteriorFacingEdge()
|
||||
{
|
||||
// The clamp center sits in the upper half of the bin, so D11 blocks the
|
||||
// origin-facing edge (zone min - clearance + s = 20). Red on ab50900: the
|
||||
// centerline split let the 2x2 land at x=21, material inside the clamp;
|
||||
// after D11 it lands at x=20 — boundary contact, which is clear.
|
||||
var stock = Stock("sheet", 4, 23, spacing: 3, edge: new Spacing(0, 0.25, 0, 0),
|
||||
clampZones: [new ClampZone("far-rail", new Box(20.25, 0, 1.5, 0.625))]);
|
||||
var job = Job(new[] { Rectangle("small", 2, 2, 1) }, new[] { stock });
|
||||
|
||||
var result = Engine().Solve(job);
|
||||
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
AssertMaterialClearOfRail(result, InflatedRail(1, 23, 4, 21, 1.5, 0.625, 0.25), 2, 2);
|
||||
}
|
||||
|
||||
// ---- C15 keep rows: non-inverted spans and between-clamp packing stay bit-exact ----
|
||||
|
||||
private static NestJobResult KeepFixtureLargeSpacingNonInverted() => Engine().Solve(Job(
|
||||
new[] { Rectangle("blk", 6, 6, 6) },
|
||||
new[] { Stock("sheet", 20, 40, spacing: 0.5, clampZones: [new ClampZone("front-center", new Box(19.25, 0, 1.5, 0.625))]) }));
|
||||
|
||||
private static NestJobResult KeepFixtureBetweenClamps() => Engine().Solve(Job(
|
||||
new[] { Rectangle("row", 2, 2, 3) },
|
||||
new[] { Stock("strip", 2, 12, clampZones: new[]
|
||||
{
|
||||
new ClampZone("clamp-a", new Box(3, 0, 2, 1)),
|
||||
new ClampZone("clamp-b", new Box(7, 0, 2, 1)),
|
||||
}) }));
|
||||
|
||||
private static NestJobResult KeepFixtureBetweenClampsWithSpacing() => Engine().Solve(Job(
|
||||
new[] { Rectangle("row", 2, 2, 3) },
|
||||
new[] { Stock("strip", 2, 12, spacing: 0.5, clampZones: new[]
|
||||
{
|
||||
new ClampZone("clamp-a", new Box(3, 0, 2, 1)),
|
||||
new ClampZone("clamp-b", new Box(7, 0, 2, 1)),
|
||||
}) }));
|
||||
|
||||
private static NestJobResult KeepFixtureMidRailSmallSpacing() => Engine().Solve(Job(
|
||||
new[] { Rectangle("wide", 10, 2, 2) },
|
||||
new[] { Stock("sheet", 4, 23, spacing: 0.5, edge: new Spacing(0, 0.25, 0, 0),
|
||||
clampZones: [new ClampZone("mid-rail", new Box(10.75, 0, 1.5, 0.625))]) }));
|
||||
|
||||
[Theory]
|
||||
// Captured from the unchanged packer at ab50900 (see progress task-6.2 evidence):
|
||||
// non-inverted clamp spans must keep today's blocks and layouts bit-for-bit.
|
||||
[InlineData("KEEP_LARGE_SPACING")]
|
||||
[InlineData("KEEP_BETWEEN_CLAMPS")]
|
||||
[InlineData("KEEP_BETWEEN_SPACED")]
|
||||
[InlineData("KEEP_MID_RAIL_SMALL_SPACING")]
|
||||
public void NonInvertedClampLayoutsMatchAb50900Digests(string fixture)
|
||||
{
|
||||
var hash = fixture switch
|
||||
{
|
||||
"KEEP_LARGE_SPACING" => "F7E4691C085FFB2164CEFFFB9B082B5F271E56E8B8B3D2AE23ECF5646FD40450",
|
||||
"KEEP_BETWEEN_CLAMPS" => "F09B878211EB7C49649647003410B20191F635E3CD8DED6E03BEA5BD63B17D61",
|
||||
"KEEP_BETWEEN_SPACED" => "F09B878211EB7C49649647003410B20191F635E3CD8DED6E03BEA5BD63B17D61",
|
||||
"KEEP_MID_RAIL_SMALL_SPACING" => "97868486E834CA0B1DB110FF717635E38537DB96DF5CDFBB2AFE37E4B1366B71",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(fixture)),
|
||||
};
|
||||
var actual = fixture switch
|
||||
{
|
||||
"KEEP_LARGE_SPACING" => LayoutHash(KeepFixtureLargeSpacingNonInverted()),
|
||||
"KEEP_BETWEEN_CLAMPS" => LayoutHash(KeepFixtureBetweenClamps()),
|
||||
"KEEP_BETWEEN_SPACED" => LayoutHash(KeepFixtureBetweenClampsWithSpacing()),
|
||||
"KEEP_MID_RAIL_SMALL_SPACING" => LayoutHash(KeepFixtureMidRailSmallSpacing()),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(fixture)),
|
||||
};
|
||||
Assert.Equal(hash, actual);
|
||||
}
|
||||
}
|
||||
@@ -90,9 +90,14 @@ internal static class SheetPacker
|
||||
/// inflated-box model an item at position p covers material [p, p + size] plus a trailing
|
||||
/// part-spacing tail; blocking bin-local [zone - clearance + s, zone + clearance] per axis
|
||||
/// therefore means exactly: item material keeps at least the bottom-edge clearance from
|
||||
/// every clamp side, without paying any extra part-spacing tax there. A span that
|
||||
/// degenerates because the spacing exceeds the inflated zone collapses to the zone
|
||||
/// centerline - conservative (it forbids crossing the clamp), never permissive.
|
||||
/// every clamp side, without paying any extra part-spacing tax there. When s exceeds the
|
||||
/// inflated span the interval inverts (decision D11): block a zero-extent line at the
|
||||
/// clamp edge facing the sheet interior — at (zone max + clearance) when the zone center
|
||||
/// lies in the lower half of the bin on that axis, else at (zone min - clearance + s).
|
||||
/// Parts on the interior side then keep the exact clearance; the unavoidable extra
|
||||
/// exclusion (s - extent - 2*clearance) lands on the side toward the nearer sheet edge.
|
||||
/// The old collapse to the zone centerline was NOT conservative: an item could start
|
||||
/// exactly on the centerline with material inside the clamp.
|
||||
/// Empty zone lists change nothing: the free list evolves bit-identically (C2).
|
||||
/// </summary>
|
||||
private static void BlockClamps(MaxRectsSheet sheet, NestPlateStock stock, Box work, double s)
|
||||
@@ -110,17 +115,24 @@ internal static class SheetPacker
|
||||
var right = x0(b.Right) + clearance;
|
||||
if (right <= left)
|
||||
{
|
||||
var centerX = x0((b.Left + b.Right) * 0.5);
|
||||
left = centerX;
|
||||
right = centerX;
|
||||
var center = x0((b.Left + b.Right) * 0.5);
|
||||
// D11: the interior-facing clamp edge (toward the farther sheet edge).
|
||||
var line = center < sheet.Width * 0.5
|
||||
? x0(b.Right) + clearance
|
||||
: x0(b.Left) - clearance + s;
|
||||
left = line;
|
||||
right = line;
|
||||
}
|
||||
var bottom = y0(b.Bottom) - clearance + s;
|
||||
var top = y0(b.Top) + clearance;
|
||||
if (top <= bottom)
|
||||
{
|
||||
var centerY = y0((b.Bottom + b.Top) * 0.5);
|
||||
bottom = centerY;
|
||||
top = centerY;
|
||||
var center = y0((b.Bottom + b.Top) * 0.5);
|
||||
var line = center < sheet.Height * 0.5
|
||||
? y0(b.Top) + clearance
|
||||
: y0(b.Bottom) - clearance + s;
|
||||
bottom = line;
|
||||
top = line;
|
||||
}
|
||||
sheet.Block(new Rect(left, bottom, right - left, top - bottom));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user