Merge branch 'refs/heads/claude/objective-yalow-6f6bde' into claude/help-docs-update-list-760b9c

This commit is contained in:
aj committed 2026-11-02 17:26:44 -05:00
commit 5da1e35e88
9 files changed
+392 -26

No files matched your search

@@ -19,12 +19,15 @@ public static class PostVerificationAnalyzer
CancellationToken cancellationToken = default)
{
var report = Analyze(nest, cancellationToken);
if (postProcessor is IPostVerificationSupport { PreservesPlacedProgramOrder: true })
var change = postProcessor is IPostVerificationSupport support
? support.PlacedProgramOrderChange(nest)
: "does not declare that it preserves placed part/contour order and pierce positions";
if (change == null)
return report;
var findings = report.Findings.ToList();
findings.Add(new(PostVerificationKind.Incomplete, 0, null, null,
$"Post '{postProcessor?.Name ?? "unknown"}' does not declare that it preserves placed part/contour order " +
"and pierce positions. Nest-level checks ran, but the final rapid sequence requires manual review."));
$"Post '{postProcessor?.Name ?? "unknown"}' {change}. " +
"Nest-level checks ran, but the final rapid sequence requires manual review."));
return new PostVerificationReport(findings);
}
+10
View File
@@ -9,5 +9,15 @@ namespace OpenNest;
/// </summary>
public interface IPostVerificationSupport
{
/// <summary>True when the post preserves the placed order for every nest.</summary>
bool PreservesPlacedProgramOrder { get; }
/// <summary>
/// Why posting <paramref name="nest"/> with the current settings would not move in the
/// placed order, or null when it would. A post whose order depends on the nest or its
/// settings overrides this; it must return null only when the posted motion sequence
/// provably equals the placed one.
/// </summary>
string PlacedProgramOrderChange(Nest nest) =>
PreservesPlacedProgramOrder ? null : "does not declare that it preserves placed part/contour order and pierce positions";
}
@@ -0,0 +1,186 @@
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
using OpenNest.Posts.Cincinnati;
namespace OpenNest.Tests.Cincinnati;
/// <summary>
/// The CL post declares that it preserves the placed order only for a nest whose posted
/// pierces and cuts replay exactly as the pre-post check read them, and withholds it
/// whenever the post would change them.
/// </summary>
public class CincinnatiPlacedOrderTests
{
[Fact]
public void ScribesFirst_PostsTheCheckedSequence_AndDeclaresIt()
{
var nest = Nest(scribeFirst: true);
var post = new CincinnatiPostProcessor(CincinnatiReplay.Config(partSubprograms: false));
Assert.Null(post.PlacedProgramOrderChange(nest));
Assert.Equal(
PostVerificationAnalyzer.Analyze(nest).Findings.Select(f => f.Message),
PostVerificationAnalyzer.AnalyzeForPost(nest, post).Findings.Select(f => f.Message));
var (pierces, cuts) = Checked(nest);
Assert.Equal(4, pierces.Count); // scribe, hole, perimeter, square
var run = CincinnatiReplay.Run(CincinnatiReplay.Post(nest, partSubprograms: false));
AssertSame(pierces, run.PierceLog.Select(p => p.Point).ToList());
AssertSame(cuts, run.Cuts);
}
[Fact]
public void ScribeAfterACut_WithholdsTheDeclaration_BecauseThePostCutsItFirst()
{
var nest = Nest(scribeFirst: false);
var post = new CincinnatiPostProcessor(CincinnatiReplay.Config(partSubprograms: false));
var change = post.PlacedProgramOrderChange(nest);
Assert.Equal("writes the etch features of part 1 on plate 1 before the cuts placed ahead of them", change);
var report = PostVerificationAnalyzer.AnalyzeForPost(nest, post);
Assert.Contains(report.Findings, f => f.Kind == PostVerificationKind.Incomplete && f.Message.Contains(change));
Assert.False(report.CanPost(false));
// The withheld declaration is real: the posted pierces differ from the checked ones.
var (pierces, _) = Checked(nest);
var run = CincinnatiReplay.Run(CincinnatiReplay.Post(nest, partSubprograms: false));
Assert.Equal(pierces.Count, run.PierceLog.Count);
Assert.True(CincinnatiReplay.Near(new Vector(3, 3), run.PierceLog[0].Point));
Assert.False(CincinnatiReplay.Near(pierces[0], run.PierceLog[0].Point));
}
[Fact]
public void PartSubprograms_WithholdTheDeclaration()
{
var post = new CincinnatiPostProcessor(CincinnatiReplay.Config(partSubprograms: true));
Assert.Contains("part sub-programs", post.PlacedProgramOrderChange(Nest(scribeFirst: true)));
}
[Fact]
public void SmartRapids_WithholdTheDeclaration()
{
var config = CincinnatiReplay.Config(partSubprograms: false);
config.UseSmartRapids = true;
Assert.Contains("smart rapids", new CincinnatiPostProcessor(config).PlacedProgramOrderChange(Nest(scribeFirst: true)));
}
[Fact]
public void AContourWithoutALeadingRapid_WithholdsTheDeclaration()
{
// The post pierces it at its return point instead of wherever the tool already is.
var program = new Program();
program.LineTo(4, 0);
program.LineTo(4, 4);
program.LineTo(0, 4);
program.LineTo(0, 0);
var nest = new Nest();
nest.CreatePlate().Parts.Add(new Part(new Drawing("square", program), new Vector(2, 2)));
var post = new CincinnatiPostProcessor(CincinnatiReplay.Config(partSubprograms: false));
Assert.Equal("adds or drops rapids in part 1 on plate 1", post.PlacedProgramOrderChange(nest));
}
// A 20x20 plate part at (1, 1) with a scribe, a hole cut through a hole call and its
// perimeter, then a 4x4 square at (30, 1). The scribe comes first or after the hole.
private static Nest Nest(bool scribeFirst)
{
var clean = new Program();
clean.MoveTo(2, 2);
clean.Codes.Add(new LinearMove(6, 2) { Layer = LayerType.Scribe });
clean.MoveTo(13, 10);
clean.ArcTo(13, 10, 10, 10, RotationType.CCW);
clean.MoveTo(20, 10);
Outline(clean, 20);
var hole = new Program();
hole.MoveTo(0, 0);
hole.Codes.Add(new LinearMove(3, 0) { Layer = LayerType.Leadin });
hole.Codes.Add(new ArcMove(new Vector(3, 0), Vector.Zero, RotationType.CCW));
var placed = new Program();
if (scribeFirst)
Scribe(placed);
placed.Codes.Add(new SubProgramCall { Id = 1, Program = hole, Offset = new Vector(10, 10) });
if (!scribeFirst)
Scribe(placed);
placed.MoveTo(20.5, 10);
placed.Codes.Add(new LinearMove(20, 10) { Layer = LayerType.Leadin });
Outline(placed, 20);
var plate = new Part(new Drawing("plate", clean), new Vector(1, 1));
Assert.True(plate.RestoreLeadInProgram(placed, false));
var square = new Program();
square.MoveTo(4, 2);
Outline(square, 4);
var leaded = new Program();
leaded.MoveTo(4.5, 2);
leaded.Codes.Add(new LinearMove(4, 2) { Layer = LayerType.Leadin });
Outline(leaded, 4);
var second = new Part(new Drawing("square", square), new Vector(30, 1));
Assert.True(second.RestoreLeadInProgram(leaded, false));
var nest = new Nest();
var sheet = nest.CreatePlate();
sheet.Size = new Size(60, 120);
sheet.Parts.Add(plate);
sheet.Parts.Add(second);
return nest;
static void Scribe(Program program)
{
program.MoveTo(2, 2);
program.Codes.Add(new LinearMove(6, 2) { Layer = LayerType.Scribe });
}
// A size x size square cut counterclockwise from its right-edge midpoint.
static void Outline(Program program, double size)
{
program.LineTo(size, size);
program.LineTo(0, size);
program.LineTo(0, 0);
program.LineTo(size, 0);
program.LineTo(size, size / 2);
}
}
// The pierces and cuts the pre-post check reads, in plate order and plate coordinates.
private static (List<Vector> Pierces, List<(Vector From, Vector To)> Cuts) Checked(Nest nest)
{
var pierces = new List<Vector>();
var cuts = new List<(Vector, Vector)>();
Vector? position = Vector.Zero;
foreach (var part in nest.Plates[0].Parts)
{
var moves = ExecutionMotionReader.Read(part.Program, part.Location, position, default).Motions;
for (var i = 0; i < moves.Count; i++)
{
if (!moves[i].Rapid)
cuts.Add((moves[i].Start!.Value, moves[i].End));
else if (i + 1 < moves.Count && !moves[i + 1].Rapid)
pierces.Add(moves[i].End);
}
position = moves[^1].End;
}
return (pierces, cuts);
}
private static void AssertSame(List<Vector> expected, List<Vector> actual)
{
Assert.Equal(expected.Count, actual.Count);
for (var i = 0; i < expected.Count; i++)
Assert.True(CincinnatiReplay.Near(expected[i], actual[i]), $"Pierce {i}: {expected[i]} vs {actual[i]}");
}
private static void AssertSame(List<(Vector From, Vector To)> expected, List<(Vector From, Vector To)> actual)
{
Assert.Equal(expected.Count, actual.Count);
for (var i = 0; i < expected.Count; i++)
Assert.True(
CincinnatiReplay.Near(expected[i].From, actual[i].From) && CincinnatiReplay.Near(expected[i].To, actual[i].To),
$"Cut {i}: {expected[i]} vs {actual[i]}");
}
}
@@ -10,23 +10,30 @@ namespace OpenNest.Tests.Diagnostics;
public class PostVerificationPostContractTests
{
[Fact]
public void ReorderingPostsRequireAcknowledgmentEvenWhenNestChecksAreClear()
public void ReorderingPostRequiresAcknowledgmentEvenWhenNestChecksAreClear()
{
var nest = ClearNest();
Assert.False(PostVerificationAnalyzer.Analyze(nest).HasWarnings);
IPostProcessor[] posts =
[
new CincinnatiPostProcessor(new CincinnatiPostConfig()),
new GravographISPostProcessor(new GravographISPostConfig())
];
foreach (var post in posts)
{
var report = PostVerificationAnalyzer.AnalyzeForPost(nest, post);
Assert.Contains(report.Findings, finding => finding.Kind == PostVerificationKind.Incomplete);
Assert.False(report.CanPost(false));
Assert.True(report.CanPost(true));
Assert.Contains(post.Name, report.ToDisplayText());
}
var post = new GravographISPostProcessor(new GravographISPostConfig());
var report = PostVerificationAnalyzer.AnalyzeForPost(nest, post);
Assert.Contains(report.Findings, finding => finding.Kind == PostVerificationKind.Incomplete);
Assert.False(report.CanPost(false));
Assert.True(report.CanPost(true));
Assert.Contains(post.Name, report.ToDisplayText());
}
[Fact]
public void CincinnatiClDeclaresPlacedOrderOnlyWhenThisNestPostsInIt()
{
var report = PostVerificationAnalyzer.AnalyzeForPost(ClearNest(),
new CincinnatiPostProcessor(new CincinnatiPostConfig()));
Assert.Empty(report.Findings);
Assert.True(report.CanPost(false));
var subprograms = PostVerificationAnalyzer.AnalyzeForPost(ClearNest(),
new CincinnatiPostProcessor(new CincinnatiPostConfig { UsePartSubprograms = true }));
Assert.Contains(subprograms.Findings, finding => finding.Kind == PostVerificationKind.Incomplete);
Assert.False(subprograms.CanPost(false));
}
[Fact]
@@ -163,6 +163,17 @@ public sealed class CincinnatiPartSubprogramWriter
return (mapping, entries);
}
/// <summary>
/// The absolute copy a hole sub-program is written from: suppressed moves left uncut and
/// a leading rapid ensured. <see cref="Write"/> then puts its etch features first.
/// </summary>
internal static Program PostedHole(Program hole)
{
var absolute = PostedCopy(hole);
EnsureLeadingRapid(absolute);
return absolute;
}
// An absolute copy with suppressed moves left uncut, as the post writes it.
private static Program PostedCopy(Program program)
{
@@ -203,8 +214,7 @@ public sealed class CincinnatiPartSubprogramWriter
if (mapping.ContainsKey(call.Program) || !FeatureUtils.HasCut(call.Program))
continue;
var absolute = PostedCopy(call.Program);
EnsureLeadingRapid(absolute);
var absolute = PostedHole(call.Program);
var canonical = ProgramToCanonical(absolute);
if (!contentIndex.TryGetValue(canonical, out var subNum))
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Posts.Cincinnati;
/// <summary>
/// Decides whether the post moves through a nest in the placed order the pre-post check
/// verified (see docs/post-verification.md). The writer keeps plate order, but writes each
/// part's (and hole's) etch features before its cut features, adds a rapid ahead of a contour
/// that has none, drops rapids with nothing to cut and, with part sub-programs, rapids to and
/// from each part's corner. The order is declared preserved only when, for this nest and
/// these settings, none of that changes a single motion.
/// </summary>
internal static class CincinnatiPlacedOrder
{
/// <summary>Why the posted motion sequence differs from the placed one, or null when it equals it.</summary>
internal static string Change(Nest nest, CincinnatiPostConfig config)
{
if (config.UsePartSubprograms)
return "calls part sub-programs, which add rapids to and from each part's corner";
if (config.UseSmartRapids)
return "uses smart rapids (G121), whose blended rapids leave the straight line";
if (nest?.Plates == null)
return null;
for (var plateIndex = 0; plateIndex < nest.Plates.Count; plateIndex++)
{
var parts = nest.Plates[plateIndex]?.Parts;
if (parts == null)
continue;
for (var partIndex = 0; partIndex < parts.Count; partIndex++)
{
var part = parts[partIndex];
if (part?.Program == null)
continue;
var where = $"part {partIndex + 1} on plate {plateIndex + 1}";
List<ICode> placed;
List<(List<ICode> codes, bool isEtch)> posted;
try
{
placed = FeatureUtils.AbsoluteCodes(FeatureUtils.PostedProgram(part));
posted = FeatureUtils.SplitAndClassify(part);
}
catch (InvalidOperationException)
{
return $"cannot post {where} as placed";
}
var change = ChangeTo(placed, posted, where);
if (change != null)
return change;
foreach (var call in placed.OfType<SubProgramCall>())
{
if (call.Program == null || !FeatureUtils.HasCut(call.Program))
continue;
var hole = CincinnatiPartSubprogramWriter.PostedHole(call.Program);
change = ChangeTo(
FeatureUtils.AbsoluteCodes(call.Program),
FeatureUtils.ClassifyAndOrder(FeatureUtils.SplitByRapids(hole.Codes)),
$"a hole of {where}"
);
if (change != null)
return change;
}
}
}
return null;
}
private static string ChangeTo(List<ICode> placed, List<(List<ICode> codes, bool isEtch)> posted, string where)
{
if (SameMotions(placed, posted.SelectMany(feature => feature.codes)))
return null;
var cutSeen = false;
foreach (var feature in FeatureUtils.SplitByRapids(placed))
{
var isEtch = FeatureUtils.IsEtch(feature);
if (isEtch && cutSeen)
return $"writes the etch features of {where} before the cuts placed ahead of them";
cutSeen |= !isEtch;
}
return $"adds or drops rapids in {where}";
}
// Motions and hole calls in the same order with exactly the same coordinates.
private static bool SameMotions(IEnumerable<ICode> placed, IEnumerable<ICode> posted)
{
var a = placed.Where(code => code is Motion or SubProgramCall).ToList();
var b = posted.Where(code => code is Motion or SubProgramCall).ToList();
if (a.Count != b.Count)
return false;
for (var i = 0; i < a.Count; i++)
{
if (!Same(a[i], b[i]))
return false;
}
return true;
}
private static bool Same(ICode a, ICode b) =>
(a, b) switch
{
(SubProgramCall x, SubProgramCall y) => ReferenceEquals(x.Program, y.Program) && Exact(x.Offset, y.Offset),
(ArcMove x, ArcMove y) => Exact(x.EndPoint, y.EndPoint)
&& Exact(x.CenterPoint, y.CenterPoint)
&& x.Rotation == y.Rotation,
(Motion x, Motion y) => x.GetType() == y.GetType() && Exact(x.EndPoint, y.EndPoint),
_ => false,
};
private static bool Exact(Vector a, Vector b) => a.X == b.X && a.Y == b.Y;
}
@@ -13,7 +13,8 @@ namespace OpenNest.Posts.Cincinnati
public sealed class CincinnatiPostProcessor
: IJsonConfigPostProcessor,
IPostProcessorNestAware,
IMaterialProvidingPostProcessor
IMaterialProvidingPostProcessor,
IPostVerificationSupport
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
@@ -25,6 +26,15 @@ namespace OpenNest.Posts.Cincinnati
public string Author => "OpenNest";
public string Description => "Cincinnati CL-707/CL-800/CL-900/CL-940/CLX family";
/// <summary>Etch-first writing and part sub-programs can change it; see <see cref="PlacedProgramOrderChange"/>.</summary>
public bool PreservesPlacedProgramOrder => false;
/// <summary>
/// Null when this nest posts with the current settings in exactly its placed motion order,
/// which Plan Cutting's scribes-first programs do; otherwise what the post changes.
/// </summary>
public string PlacedProgramOrderChange(Nest nest) => CincinnatiPlacedOrder.Change(nest, Config);
public CincinnatiPostConfig Config { get; private set; }
object IConfigurablePostProcessor.Config => Config;
+15
View File
@@ -60,6 +60,21 @@ Sub-program bodies start with a `:<subNum>` label and end with `M99`.
The post reads part programs through absolute-mode copies and never changes the
nest: an incremental part program stays incremental after posting.
### Order and pre-post verification
Within a part (and within each hole sub-program) the post writes etch features
before cut features, keeping each group in program order (`FeatureUtils.ClassifyAndOrder`).
It also inserts a rapid to a contour's return point when the contour opens without
one, and drops rapids with nothing to cut. Part sub-programs add a rapid to each
part's toolpath corner and back, and smart rapids blend rapids off the straight line.
The post declares that it preserves the placed order to
[pre-post verification](post-verification.md#selected-post-and-cut-order)
(`CincinnatiPlacedOrder`) only when, for the nest and settings being posted, none of
these changes a motion: part sub-programs and smart rapids off, and each part's and
hole's posted motions exactly equal to its placed ones. Plan Cutting already cuts a
part's scribes first, so planned nests qualify; otherwise posting warns that the
final rapid sequence is unverified. Output is the same either way.
### Sheet header
Each sheet sub-program re-anchors the work frame with `G92 X#5021 Y#5022`
+15 -6
View File
@@ -198,11 +198,17 @@ the gate is installed at OpenNest's desktop and console posting entry points.
Cincinnati CI Fiber preserves the placed part/contour order and pierce positions
used by this check. Cincinnati CL keeps placed part order, including cut-offs, but
puts etch features before cut features and can add subprogram routing moves;
Gravograph builds and optimizes separate tool passes. Those two posts, and unknown
plugins, therefore show an additional **incomplete final rapid sequence** warning
requiring acknowledgment even if the nest-level checks have no findings. This
change does not silently alter their existing CNC output order.
writes each part's (and hole's) etch features before its cut features, adds a rapid
ahead of a contour that opens without one, drops rapids with nothing to cut, and with
**Use part sub-programs** rapids to and from each part's corner. It declares the
placed order for a nest only when none of that changes a single motion with the
settings being posted: part sub-programs and smart rapids off, and every part's and
hole's motions, compared exactly, unchanged by its writer. Plan Cutting's programs
cut a part's scribes first, so a planned nest qualifies. Otherwise the warning names
the first part it changes, and the CNC output order is left as it was.
Gravograph builds and optimizes separate tool passes. It, a CL nest that does not
qualify, and unknown plugins show an additional **incomplete final rapid sequence**
warning requiring acknowledgment even if the nest-level checks have no findings.
## Limits and maintainer contract
@@ -218,7 +224,10 @@ its report exposes findings, display text, and `CanPost(risksAcknowledged)`. Use
`AnalyzeForPost` at posting entry points so unknown post ordering is not silently
certified. `IPostVerificationSupport.PreservesPlacedProgramOrder` is an opt-in
contract, not a machine-safety claim; only declare it after verifying the writer's
part order, contour order, and pierce positions. The WinForms dialog renders the
part order, contour order, and pierce positions. A post whose order depends on the
nest or its settings overrides `PlacedProgramOrderChange(nest)` instead, returning
null only when the posted motion sequence provably equals the placed one and
otherwise what it changes; the analyzer shows that reason in the warning. The WinForms dialog renders the
report and enforces per-attempt consent. New callers
must keep inputs stable until analysis completes, honor cancellation, and never
treat incomplete analysis as a clear result. Keep analysis out of paint handlers.