fix(diagnostics): read absolute subprogram holes and refuse unknown codes

PlateOverlapAnalyzer converted clean programs directly with
ConvertProgram.ToGeometry, which adds call offsets only to
incremental moves, so holes in absolute-mode subprograms were read at
their frame origin. A square with two absolute holes reported
"Native material contours cross or touch", and the Plan Cutting
overlap gate blocked it while the incremental twin passed. Convert an
owned incremental-mode copy instead (PreparedContours.CopyForGeometry,
now internal, the same normalization the cutting planner uses).

The analyzer also threw on a program whose Codes list was null and
cast or cloned instructions it does not know. It now refuses a
missing list and anything other than the exact built-in instruction
types as an incomplete check, before any copy or conversion runs.
Pre-post verification and the plate overlap overlay share this path.
This commit is contained in:
aj committed 2026-10-05 21:52:30 -04:00
1 parent faf3c9bcc4
commit 1b422ef99a
5 files changed
+127 -6

No files matched your search

@@ -215,7 +215,12 @@ public sealed class PreparedContours
private static Vector Start(Entity entity) => entity is Line line ? line.StartPoint : ((Arc)entity).StartPoint();
private static Vector End(Entity entity) => entity is Line line ? line.EndPoint : ((Arc)entity).EndPoint();
private static Program CopyForGeometry(Program source, CancellationToken token)
/// <summary>
/// Owned copy of a validated graph with every program in incremental mode, so
/// <see cref="ConvertProgram.ToGeometry"/> keeps absolute subprogram frame offsets. Shared
/// subprograms stay shared. Validate exact instruction types first: this clones every code.
/// </summary>
internal static Program CopyForGeometry(Program source, CancellationToken token)
{
var copies = new Dictionary<Program, Program>(ReferenceEqualityComparer.Instance);
return Copy(source);
@@ -43,7 +43,7 @@ internal sealed class OverlapSource
internal OverlapSource(Program program, List<Entity> entities, string error)
{
codeCount = program.Codes.Count;
codeCount = CodeCount(program);
rotation = BitConverter.DoubleToInt64Bits(program.Rotation);
Entities = entities;
Error = error;
@@ -54,7 +54,10 @@ internal sealed class OverlapSource
internal string Error { get; }
internal bool Matches(Program program) =>
program.Codes.Count == codeCount && BitConverter.DoubleToInt64Bits(program.Rotation) == rotation;
CodeCount(program) == codeCount && BitConverter.DoubleToInt64Bits(program.Rotation) == rotation;
// Program.Codes is a writable field; a missing list is a refused source, not a crash.
private static int CodeCount(Program program) => program.Codes?.Count ?? -1;
/// <summary>
/// Prepares once and shares the result. A geometry failure is cached like a success;
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Converters;
using OpenNest.Geometry;
@@ -72,9 +73,11 @@ public static class PlateOverlapAnalyzer
try
{
ValidateProgram(program, new HashSet<Program>(ReferenceEqualityComparer.Instance));
// Conversion creates fresh geometry, including expanded shared hole calls;
// no cloning/rotation of a live program or subprogram is necessary.
return new OverlapSource(program, ConvertProgram.ToGeometry(program)
// Convert an incremental-mode copy: the converter adds call offsets to incremental moves
// only, so absolute subprogram holes would otherwise land at their frame origin. The
// copy is owned; the live program is neither converted in place nor rotated.
var geometry = PreparedContours.CopyForGeometry(program, CancellationToken.None);
return new OverlapSource(program, ConvertProgram.ToGeometry(geometry)
.Where(entity => SpecialLayers.IsMaterial(entity.Layer)
&& entity.Layer != SpecialLayers.Leadin
&& entity.Layer != SpecialLayers.Leadout).ToList(), null);
@@ -267,14 +270,26 @@ public static class PlateOverlapAnalyzer
internal static bool IsGeometryFailure(Exception exception) => exception is
ArgumentException or InvalidOperationException or NotSupportedException or ArithmeticException;
// Exact built-in instruction types. Anything else, subclasses included, is refused before the
// graph is copied or converted, so no unknown Clone or cast runs.
private static readonly HashSet<Type> SupportedCodeTypes =
[
typeof(RapidMove), typeof(LinearMove), typeof(ArcMove), typeof(SubProgramCall),
typeof(Comment), typeof(Feedrate), typeof(Kerf),
];
private static void ValidateProgram(Program program, HashSet<Program> visiting)
{
if (program == null || !visiting.Add(program) || visiting.Count > 64)
throw new ArgumentException("Missing, recursive, or excessively nested subprogram.");
if (program.Codes == null)
throw new ArgumentException("Program has no instruction list.");
foreach (var code in program.Codes)
{
if (code == null)
throw new ArgumentException("Program contains a missing instruction.");
if (!SupportedCodeTypes.Contains(code.GetType()))
throw new NotSupportedException("Program contains an unsupported instruction.");
if (code is Motion motion && !OverlapMaterial.IsFinite(motion.EndPoint)
|| code is ArcMove arc && !OverlapMaterial.IsFinite(arc.CenterPoint))
throw new ArgumentException("Program coordinates must be finite.");