mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-06 06:02:11 -04:00
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:
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.");
|
||||
|
||||
@@ -95,6 +95,46 @@ public class CuttingPlanBatchTests
|
||||
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Mode.Absolute)]
|
||||
[InlineData(Mode.Incremental)]
|
||||
public void Plan_SubprogramHolesInEitherMode_AreNotFalseOverlaps(Mode mode)
|
||||
{
|
||||
var hole = new Program();
|
||||
hole.MoveTo(1, 0);
|
||||
hole.Codes.Add(new ArcMove(1, 0, 0, 0) { Rotation = RotationType.CCW });
|
||||
hole.Mode = mode;
|
||||
var clean = ExplicitContourTests.Square(false);
|
||||
clean.SubPrograms[-1] = hole;
|
||||
clean.Codes.Add(new SubProgramCall { Program = hole, Offset = new Vector(3, 3), Id = -1 });
|
||||
clean.Codes.Add(new SubProgramCall { Program = hole, Offset = new Vector(7, 3), Id = -1 });
|
||||
clean.Mode = mode;
|
||||
var nest = new Nest();
|
||||
var plate = Plate(nest, new Part(new Drawing("holes", clean), new Vector(1, 1)));
|
||||
|
||||
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
||||
|
||||
var planned = Assert.Single(proposal.Plates);
|
||||
Assert.True(planned.IsOverlapClear, string.Join("; ", planned.Overlap.Issues.Select(i => i.Message)));
|
||||
Assert.True(planned.IsReady);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_RefusedGraphs_BlockThePlateInsteadOfThrowing()
|
||||
{
|
||||
var nest = new Nest();
|
||||
var missing = Clean("missing", 1, 1);
|
||||
missing.BaseDrawing.Program.Codes = null;
|
||||
var plate = Plate(nest, missing);
|
||||
|
||||
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
||||
|
||||
var planned = Assert.Single(proposal.Plates);
|
||||
Assert.False(planned.IsReady);
|
||||
Assert.False(proposal.CanApply);
|
||||
Assert.Null(proposal.BuildPreview(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_FreeSearchOutOfBudget_KeepsTheCurrentOrderAndSaysSo()
|
||||
{
|
||||
|
||||
@@ -501,6 +501,64 @@ public class PlateOverlapAnalyzerTests
|
||||
Assert.Equal(y, pair.Centroid.Y, 7);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Mode.Absolute)]
|
||||
[InlineData(Mode.Incremental)]
|
||||
public void Analyze_SubprogramHolesKeepTheirCallOffsetsInEitherMode(Mode mode)
|
||||
{
|
||||
var hole = new Program();
|
||||
hole.MoveTo(1, 0);
|
||||
hole.Codes.Add(new ArcMove(1, 0, 0, 0) { Rotation = RotationType.CCW });
|
||||
hole.Mode = mode;
|
||||
var clean = new Program();
|
||||
clean.MoveTo(0, 0);
|
||||
clean.LineTo(0, 10);
|
||||
clean.LineTo(10, 10);
|
||||
clean.LineTo(10, 0);
|
||||
clean.LineTo(0, 0);
|
||||
clean.SubPrograms[-1] = hole;
|
||||
clean.Codes.Add(new SubProgramCall { Program = hole, Offset = new Vector(3, 3), Id = -1 });
|
||||
clean.Codes.Add(new SubProgramCall { Program = hole, Offset = new Vector(7, 3), Id = -1 });
|
||||
clean.Mode = mode;
|
||||
var host = new Part(new Drawing("host", clean));
|
||||
var insert = Rectangle(2.75, 2.75, 0.5, 0.5); // Inside the hole called at (3, 3).
|
||||
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { host, insert });
|
||||
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues.Select(issue => issue.Message)));
|
||||
Assert.Empty(report.Pairs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_NullOrUnsupportedInstructionsAreIncompleteWithoutRunningThem()
|
||||
{
|
||||
var missing = Rectangle(0, 0, 1, 1);
|
||||
missing.BaseDrawing.Program.Codes = null;
|
||||
var unsupported = Rectangle(5, 0, 1, 1);
|
||||
var code = new CountingCode();
|
||||
unsupported.BaseDrawing.Program.Codes.Add(code);
|
||||
var clear = Rectangle(10, 0, 1, 1);
|
||||
|
||||
var report = PlateOverlapAnalyzer.Analyze(new[] { missing, unsupported, clear });
|
||||
|
||||
Assert.Equal(new[] { 0, 1 }, report.Issues.Select(issue => issue.PartAId).OrderBy(id => id));
|
||||
Assert.Empty(report.Pairs);
|
||||
Assert.Equal(0, code.Clones);
|
||||
}
|
||||
|
||||
// Reports a built-in type but is not one: conversion must refuse it rather than cast or clone it.
|
||||
private sealed class CountingCode : ICode
|
||||
{
|
||||
public int Clones { get; private set; }
|
||||
public CodeType Type => CodeType.LinearMove;
|
||||
|
||||
public ICode Clone()
|
||||
{
|
||||
Clones++;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static Part Rectangle(double x, double y, double width, double height) =>
|
||||
new(WithContours(new[] { new Vector(0, 0), new Vector(width, 0),
|
||||
new Vector(width, height), new Vector(0, height) }).BaseDrawing, new Vector(x, y));
|
||||
|
||||
Reference in new issue
Block a user