fix: leave etch/scribe marks out of nesting geometry

Every nesting-geometry consumer filtered only rapids, so scribe/etch
moves counted as part material. An etch tick that ends a hair outside
the outline (PEP bend ticks start on the notch edge) made the part
"open geometry leaving the material region": the job validator threw
and every built-in engine plus Gpt6Astra crashed on real PEP jobs
(PT75, drawing 4980 A01 PT77). Marks are only on the surface, so they
should never affect placement, collision, area, or validation.

- SpecialLayers.IsMaterial excludes Rapid and Scribe; used by drawing
  area, canonical angle, part collision, PartGeometry, plate perimeter,
  best-fit/pair evaluation, rotation analysis, GPU evaluators, and both
  validators. Timing, display, splitting and posts still see marks.
- ConvertGeometry also maps the saved SCRIBE layer name to Scribe, so
  programs rebuilt from stored entities keep their marks.
- NestReader repairs older files (e.g. PepNestExport output) whose
  programs saved etch as cut moves while source entities kept SCRIBE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-25 06:44:29 -04:00
co-authored by Claude Opus 5.5
parent e3d10e95ae
commit 1b5e1b14a6
24 changed files with 273 additions and 30 deletions
@@ -179,6 +179,41 @@ public class NestJobValidationTests
);
}
[Fact]
public void EtchMarksAreLeftOutOfNestingGeometry()
{
// The etch tick sticks 0.5 past the right edge. As material it would be open geometry
// leaving the part and would overlap the neighbour placed 0.2 away; as a mark it is ignored.
var job = new NestJob(
new[] { new NestJobPart("part", PartGeometrySnapshot.FromProgram(RectangleWithEtch(LayerType.Scribe)), 2) },
new[] { new NestPlateStock("stock", new Size(30, 30), 1) }
);
var result = Solve(job, new NestJobPlacement("part", 0, 0, 0, 0), new NestJobPlacement("part", 1, 10.2, 0, 0));
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(new PartFulfillment("part", 2, 2, 0), Assert.Single(result.Fulfillment));
}
[Fact]
public void OpenCutGeometryLeavingThePartIsStillRejected()
{
var job = new NestJob(
new[] { new NestJobPart("part", PartGeometrySnapshot.FromProgram(RectangleWithEtch(LayerType.Cut)), 1) },
new[] { new NestPlateStock("stock", new Size(30, 30), 1) }
);
Assert.Throws<ArgumentException>(() => Solve(job, new NestJobPlacement("part", 0, 0, 0, 0)));
}
private static Program RectangleWithEtch(LayerType etchLayer)
{
var program = TestDrawingFactory.Rectangle(10, 10);
program.MoveTo(9.5, 5);
program.Codes.Add(new LinearMove(10.5, 5) { Layer = etchLayer });
return program;
}
private static NestJobResult Solve(NestJob job, params NestJobPlacement[] placements) =>
new NestJobRunner(_ => new CandidateNester(placements)).Solve(job);