fix(core): check equivalent existing cutoffs against clamp spans

The automatic cutoff planner tested only the proposed X against active
clamp spans, then certified a tolerance-close existing line at its own
X. A manual line just inside a clamp edge could therefore be reported
as a verified tail separator. Test the proposed X and every equivalent
existing definition before regenerating.

Also keep a same-line limited manual cut blocking the whole plan when
it lies on a clamp, as it did before clamp checks were added.

Project Memory: opennest 51597040-cff6-4304-87dc-3c2a13edf45a
This commit is contained in:
aj committed 2026-10-13 18:15:49 -04:00
1 parent d7187f65c7
commit bde332fb10
3 files changed
+79 -18

No files matched your search

+21 -16
View File
@@ -173,24 +173,9 @@ public static class AutomaticCutOffPlanner
void AddCandidate(double x, bool isSeparator)
{
// A full-width cut crosses every active front-edge clamp at this X. Test
// even equivalent existing definitions: a stale manual line cannot certify
// a retained tail. Keep the whole footprint when it hangs off the sheet.
var clearance = plate.EdgeSpacing.Bottom;
var blocked = plate.ClampZones.FirstOrDefault(zone =>
x >= zone.Bounds.Left - clearance && x <= zone.Bounds.Right + clearance);
if (blocked != null)
{
diagnostics.Add(new AutomaticCutOffDiagnostic(
AutomaticCutOffDiagnosticCode.ClampBlockedCut,
$"The proposed cut crosses clamp '{blocked.Id}' (including bottom-edge clearance); it was skipped.",
X: x));
if (isSeparator)
WarnNoSeparator(x);
return;
}
var matches = plate.CutOffs.Where(c => c.Axis == CutOffAxis.Vertical &&
Near(c.Position.X, x)).ToList();
if (matches.Any(c => !FullSpanLimits(c, bounds, settings)))
{
diagnostics.Add(new AutomaticCutOffDiagnostic(
@@ -202,6 +187,26 @@ public static class AutomaticCutOffPlanner
return;
}
// A full-width cut crosses every active front-edge clamp at its X. Test the
// proposed X and every equivalent existing definition, because a tolerance-close
// existing line is regenerated and certified at its own X. Keep the whole
// footprint when it hangs off the sheet. A same-line manual limit conflict above
// still blocks the whole plan, clamp or not.
var clearance = plate.EdgeSpacing.Bottom;
var blocked = plate.ClampZones.FirstOrDefault(zone =>
matches.Select(c => c.Position.X).Prepend(x).Any(position =>
position >= zone.Bounds.Left - clearance && position <= zone.Bounds.Right + clearance));
if (blocked != null)
{
diagnostics.Add(new AutomaticCutOffDiagnostic(
AutomaticCutOffDiagnosticCode.ClampBlockedCut,
$"The proposed cut crosses clamp '{blocked.Id}' (including bottom-edge clearance); it was skipped.",
X: x));
if (isSeparator)
WarnNoSeparator(x);
return;
}
// Even a duplicate must be regenerated detached with CURRENT settings. Its live
// drawing can be stale, and a tolerance-close line can intersect a part at the tail.
var existing = matches.FirstOrDefault();
@@ -244,6 +244,62 @@ public class AutomaticCutOffPlannerTests
Assert.Same(existing, Assert.Single(plate.CutOffs));
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public void NearEquivalentExistingSeparatorInsideClampCannotCertifyTail(int quadrant)
{
// The nominal separator (80.50001) lies just beyond the clamp edge (80.5000075),
// but the equivalent existing line that would be certified (80.500005) is inside it.
var plate = MakePlate(quadrant);
var sign = quadrant is 2 or 3 ? -1 : 1;
var y = quadrant is 3 or 4 ? -0.625 : 0;
plate.ClampZones.Add(new ClampZone("edge", sign > 0
? new Box(79.500005, y, 1.0000025, 0.625)
: new Box(-80.5000075, y, 1.0000025, 0.625)));
var existing = new CutOff(new Vector(sign * 80.500005, 0), CutOffAxis.Vertical);
plate.CutOffs.Add(existing);
existing.Regenerate(plate, new CutOffSettings());
var original = existing.Drawing.Program;
var plan = Plan(plate);
Assert.False(plan.HasSeparatedTail);
Assert.Null(plan.TailSeparatorX);
Assert.Equal(0, plan.TailLength);
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ClampBlockedCut);
Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.NoSafeTailSeparator);
Assert.DoesNotContain(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ExistingCutOff);
Assert.Equal(new[] { sign * 35.0, sign * 70.0 }, Positions(plan));
Assert.Same(original, existing.Drawing.Program);
Assert.Same(existing, Assert.Single(plate.CutOffs));
}
[Theory]
[InlineData(35)]
[InlineData(80.50001)]
public void SameLineLimitedManualCutOnClamp_StillBlocksWholePlan(double x)
{
var plate = MakePlate();
plate.ClampZones.Add(new ClampZone("under-manual", new Box(x - 0.75, 0, 1.5, 0.625)));
var manual = new CutOff(new Vector(x, 0), CutOffAxis.Vertical) { StartLimit = 10, EndLimit = 60 };
plate.CutOffs.Add(manual);
var program = manual.Drawing.Program;
var plan = Plan(plate);
Assert.True(plan.HasBlockingDiagnostics);
Assert.Empty(plan.Definitions);
Assert.Empty(plan.PreviewParts);
Assert.False(plan.HasSeparatedTail);
Assert.Contains(plan.Diagnostics, d => d.IsBlocking &&
d.Code == AutomaticCutOffDiagnosticCode.LimitedCutOffConflict);
Assert.Same(program, manual.Drawing.Program);
Assert.Same(manual, Assert.Single(plate.CutOffs));
}
[Fact]
public void Planning_DoesNotMutateAnyLiveObjectsOrSequence()
{
@@ -603,7 +659,7 @@ public class AutomaticCutOffPlannerTests
}
[Fact]
public void ToleranceCloseExistingSeparator_ThatStillTouchesClearance_DoesNotClaimTail()
public void NearEquivalentExistingSeparator_ThatStillTouchesClearance_DoesNotClaimTail()
{
var plate = MakePlate();
plate.PartSpacing = 0;
+1 -1
View File
@@ -21,7 +21,7 @@ Only real parts contribute to the occupied envelope; existing cutoff parts do no
Cutoff trimming measures clearance from the part contour, excluding tagged lead-in and lead-out moves when building the closed outline. Leads must not turn a recessed outline into a convex hull and leave oversized gaps elsewhere. Genuinely open contours still use the conservative hull/bounds fallback. The conservative curve/offset approximation can leave a small extra gap (about 0.0003 in on a straight edge at the default 0.02-in clearance), not an extra quarter inch.
If no safe separator fits inside the sheet, there is no retained-tail claim and the nominal cuts may cover the full sheet length. A proposed tail is not reported as separated unless its generated program spans the full sheet width. An active clamp blocks any proposed full-width line whose X coordinate falls in the clamp's whole footprint plus bottom-edge clearance, including a nominal grid line or an equivalent existing line. Blocked lines are skipped with a clamp diagnostic; a blocked tail also reports `NoSafeTailSeparator` rather than claiming separation. Removed clamps on a sheet are absent from its active zones and do not block cuts. Obstructed or minimum-length-filtered lines produce diagnostics; they are not proof of a completed partition.
If no safe separator fits inside the sheet, there is no retained-tail claim and the nominal cuts may cover the full sheet length. A proposed tail is not reported as separated unless its generated program spans the full sheet width. An active clamp blocks any proposed full-width line whose X coordinate falls in the clamp's whole footprint plus bottom-edge clearance, including a nominal grid line or an equivalent existing line. A tolerance-close existing line is tested at its own X as well as the proposed X, because that existing line is what would be certified. A same-line manual cut with different limits still blocks the whole plan, even on a clamp. Blocked lines are skipped with a clamp diagnostic; a blocked tail also reports `NoSafeTailSeparator` rather than claiming separation. Removed clamps on a sheet are absent from its active zones and do not block cuts. Obstructed or minimum-length-filtered lines produce diagnostics; they are not proof of a completed partition.
For a 120-by-81-inch sheet whose parts extend through 80 inches, the nominal skeleton lines are at 35 and 70 inches, followed by a separator beyond 80 inches at the required margin. There is no line at 105 inches through the retained tail.