mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-07 10:12: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 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 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);
|
var copies = new Dictionary<Program, Program>(ReferenceEqualityComparer.Instance);
|
||||||
return Copy(source);
|
return Copy(source);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ internal sealed class OverlapSource
|
|||||||
|
|
||||||
internal OverlapSource(Program program, List<Entity> entities, string error)
|
internal OverlapSource(Program program, List<Entity> entities, string error)
|
||||||
{
|
{
|
||||||
codeCount = program.Codes.Count;
|
codeCount = CodeCount(program);
|
||||||
rotation = BitConverter.DoubleToInt64Bits(program.Rotation);
|
rotation = BitConverter.DoubleToInt64Bits(program.Rotation);
|
||||||
Entities = entities;
|
Entities = entities;
|
||||||
Error = error;
|
Error = error;
|
||||||
@@ -54,7 +54,10 @@ internal sealed class OverlapSource
|
|||||||
internal string Error { get; }
|
internal string Error { get; }
|
||||||
|
|
||||||
internal bool Matches(Program program) =>
|
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>
|
/// <summary>
|
||||||
/// Prepares once and shares the result. A geometry failure is cached like a success;
|
/// 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.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.CNC.CuttingPlanning;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
@@ -72,9 +73,11 @@ public static class PlateOverlapAnalyzer
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
ValidateProgram(program, new HashSet<Program>(ReferenceEqualityComparer.Instance));
|
ValidateProgram(program, new HashSet<Program>(ReferenceEqualityComparer.Instance));
|
||||||
// Conversion creates fresh geometry, including expanded shared hole calls;
|
// Convert an incremental-mode copy: the converter adds call offsets to incremental moves
|
||||||
// no cloning/rotation of a live program or subprogram is necessary.
|
// only, so absolute subprogram holes would otherwise land at their frame origin. The
|
||||||
return new OverlapSource(program, ConvertProgram.ToGeometry(program)
|
// 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)
|
.Where(entity => SpecialLayers.IsMaterial(entity.Layer)
|
||||||
&& entity.Layer != SpecialLayers.Leadin
|
&& entity.Layer != SpecialLayers.Leadin
|
||||||
&& entity.Layer != SpecialLayers.Leadout).ToList(), null);
|
&& entity.Layer != SpecialLayers.Leadout).ToList(), null);
|
||||||
@@ -267,14 +270,26 @@ public static class PlateOverlapAnalyzer
|
|||||||
internal static bool IsGeometryFailure(Exception exception) => exception is
|
internal static bool IsGeometryFailure(Exception exception) => exception is
|
||||||
ArgumentException or InvalidOperationException or NotSupportedException or ArithmeticException;
|
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)
|
private static void ValidateProgram(Program program, HashSet<Program> visiting)
|
||||||
{
|
{
|
||||||
if (program == null || !visiting.Add(program) || visiting.Count > 64)
|
if (program == null || !visiting.Add(program) || visiting.Count > 64)
|
||||||
throw new ArgumentException("Missing, recursive, or excessively nested subprogram.");
|
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)
|
foreach (var code in program.Codes)
|
||||||
{
|
{
|
||||||
if (code == null)
|
if (code == null)
|
||||||
throw new ArgumentException("Program contains a missing instruction.");
|
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)
|
if (code is Motion motion && !OverlapMaterial.IsFinite(motion.EndPoint)
|
||||||
|| code is ArcMove arc && !OverlapMaterial.IsFinite(arc.CenterPoint))
|
|| code is ArcMove arc && !OverlapMaterial.IsFinite(arc.CenterPoint))
|
||||||
throw new ArgumentException("Program coordinates must be finite.");
|
throw new ArgumentException("Program coordinates must be finite.");
|
||||||
|
|||||||
@@ -95,6 +95,46 @@ public class CuttingPlanBatchTests
|
|||||||
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns));
|
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]
|
[Fact]
|
||||||
public void Plan_FreeSearchOutOfBudget_KeepsTheCurrentOrderAndSaysSo()
|
public void Plan_FreeSearchOutOfBudget_KeepsTheCurrentOrderAndSaysSo()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -501,6 +501,64 @@ public class PlateOverlapAnalyzerTests
|
|||||||
Assert.Equal(y, pair.Centroid.Y, 7);
|
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) =>
|
private static Part Rectangle(double x, double y, double width, double height) =>
|
||||||
new(WithContours(new[] { new Vector(0, 0), new Vector(width, 0),
|
new(WithContours(new[] { new Vector(0, 0), new Vector(width, 0),
|
||||||
new Vector(width, height), new Vector(0, height) }).BaseDrawing, new Vector(x, y));
|
new Vector(width, height), new Vector(0, height) }).BaseDrawing, new Vector(x, y));
|
||||||
|
|||||||
Reference in new issue
Block a user