mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-06 15:12:20 -04:00
214 lines
10 KiB
C#
214 lines
10 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.Engine.CuttingPlanning;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests.CuttingPlanning;
|
|
|
|
public class OwnedProgramCopyTests
|
|
{
|
|
[Theory]
|
|
[InlineData(false, false)]
|
|
[InlineData(false, true)]
|
|
[InlineData(true, false)]
|
|
[InlineData(true, true)]
|
|
public void FixedPayload_PreservesEveryAuthoredFieldAndGraphAlias(bool confirmed, bool locked)
|
|
{
|
|
var parameters = ExplicitContourTests.Parameters();
|
|
var clean = LeadPathValidationTests.Rectangle(0, 0, 10, 10);
|
|
var prepared = PreparedContours.Capture(clean, parameters);
|
|
var emitted = prepared.Emit([prepared.ClosestEntry(0, new Vector(-2, 5))]);
|
|
var part = new Part(new Drawing("metadata", clean));
|
|
Assert.True(part.RestoreLeadInProgram(emitted, locked));
|
|
part.Rotate(0.2);
|
|
AddMetadata(part.Program);
|
|
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], new Vector(-2, 5),
|
|
confirmedParameters: confirmed ? parameters : null, eligibleParts: confirmed ? [] : null));
|
|
var captured = Assert.Single(snapshot.Placements);
|
|
AssertGraph(part.Program, captured.CopyProgram());
|
|
var result = CuttingPlanService.Plan(snapshot);
|
|
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
|
var proposal = Assert.Single(result.ProposedOrder);
|
|
AssertGraph(part.Program, proposal.CopyProgram());
|
|
var detached = proposal.CopyProgram();
|
|
var expectedFeed = ((Motion)part.Program.Codes.First(c => c is Motion)).Feedrate;
|
|
((Motion)detached.Codes.First(c => c is Motion)).Feedrate = 999;
|
|
detached.SubPrograms[-40].Codes.Clear();
|
|
detached.Variables.Clear();
|
|
AssertGraph(part.Program, proposal.CopyProgram());
|
|
part.Program.Codes.Clear();
|
|
part.Program.SubPrograms[-40].Codes.Clear();
|
|
part.Program.Variables.Clear();
|
|
var retained = proposal.CopyProgram();
|
|
Assert.Equal(expectedFeed, ((Motion)retained.Codes.First(c => c is Motion)).Feedrate);
|
|
Assert.NotEmpty(retained.SubPrograms[-40].Codes);
|
|
Assert.NotEmpty(retained.Variables);
|
|
}
|
|
|
|
[Fact]
|
|
public void Propose_OwnsLosslessStorageRatherThanRetainingCallerProgram()
|
|
{
|
|
var part = new Part(new Drawing("proposal", LeadPathValidationTests.Rectangle(0, 0, 10, 10)));
|
|
var captured = Assert.Single(CuttingPlanService.Capture(new CuttingPlanRequest([part])).Placements);
|
|
var authored = captured.CopyProgram();
|
|
AddMetadata(authored);
|
|
var proposed = captured.Propose(authored, ExecutionMotionReader.ReadSupported(authored, Vector.Zero, null), []);
|
|
AssertGraph(authored, proposed.CopyProgram());
|
|
authored.Codes.Clear(); authored.SubPrograms[-40].Codes.Clear(); authored.Variables.Clear();
|
|
var retained = proposed.CopyProgram();
|
|
Assert.NotEmpty(retained.Codes);
|
|
Assert.NotEmpty(retained.SubPrograms[-40].Codes);
|
|
Assert.NotEmpty(retained.Variables);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("cycle")]
|
|
[InlineData("depth")]
|
|
[InlineData("cached-depth")]
|
|
[InlineData("expansion")]
|
|
public void OwnedCopy_RefusesUnsafeCloneTraversalBeforeInvokingClone(string kind)
|
|
{
|
|
var root = new Program();
|
|
if (kind == "cycle") root.SubPrograms[-1] = root;
|
|
if (kind is "depth" or "cached-depth")
|
|
{
|
|
var child = new Program();
|
|
if (kind == "cached-depth") root.SubPrograms[-2] = child;
|
|
var parent = child;
|
|
for (var i = 0; i < 64; i++)
|
|
{
|
|
var next = new Program(); next.SubPrograms[-1] = parent; parent = next;
|
|
}
|
|
root.SubPrograms[-1] = parent;
|
|
}
|
|
if (kind == "expansion")
|
|
{
|
|
var child = new Program();
|
|
for (var i = 0; i < 20; i++)
|
|
{
|
|
var left = new Program(); left.SubPrograms[-1] = child;
|
|
var right = new Program(); right.SubPrograms[-1] = child;
|
|
var parent = new Program(); parent.SubPrograms[-1] = left; parent.SubPrograms[-2] = right;
|
|
child = parent;
|
|
}
|
|
root.SubPrograms[-1] = child;
|
|
}
|
|
Assert.Throws<ArgumentException>(() => OwnedProgramCopy.Copy(root));
|
|
}
|
|
|
|
[Fact]
|
|
public void OwnedCopy_HonorsCancellationBeforeClone()
|
|
{
|
|
Assert.Throws<OperationCanceledException>(() => OwnedProgramCopy.Copy(new Program(), new CancellationToken(true)));
|
|
}
|
|
|
|
private static void AddMetadata(Program root)
|
|
{
|
|
var leaf = new Program();
|
|
leaf.MoveTo(-1, 0);
|
|
leaf.Codes.Add(new LinearMove(-2, 0) { Layer = LayerType.Scribe });
|
|
leaf.Codes.Add(new ArcMove(-1, 1, -1, 0, RotationType.CW) { Layer = LayerType.Scribe });
|
|
leaf.Rotate(0.37);
|
|
leaf.Mode = Mode.Incremental;
|
|
var degrees = 0.37 * 180 / System.Math.PI;
|
|
var middle = new Program();
|
|
middle.Codes.Add(new SubProgramCall(leaf, degrees) { Id = -7, Offset = new Vector(-1, 0) });
|
|
middle.SubPrograms[-7] = leaf;
|
|
root.Codes.Add(new SubProgramCall(middle, 0) { Id = -9, Offset = new Vector(-5, -5) });
|
|
root.Codes.Add(new SubProgramCall(leaf, degrees) { Id = -7, Offset = new Vector(-10, -10) });
|
|
root.SubPrograms[-9] = middle;
|
|
root.SubPrograms[-7] = leaf;
|
|
var inactive = new Program(Mode.Incremental);
|
|
inactive.Codes.Add(new LinearMove(4, 5) { Suppressed = true, Layer = LayerType.Display });
|
|
inactive.SubPrograms[-7] = leaf;
|
|
root.SubPrograms[-40] = inactive;
|
|
root.SubPrograms[-41] = new Program(); // Accepted motionless inactive registration.
|
|
foreach (var p in new[] { root, middle, leaf, inactive })
|
|
{
|
|
p.Codes.Insert(0, new Comment("literal, : #value"));
|
|
p.Codes.Insert(1, new Feedrate(123.5) { VariableRef = "speed" });
|
|
p.Codes.Insert(2, new Kerf(KerfType.Right));
|
|
p.Variables["value"] = new VariableDefinition("value", "2+3", 5, inline: true, global: true);
|
|
foreach (var motion in p.Codes.OfType<Motion>())
|
|
{
|
|
motion.UseExactStop = true;
|
|
motion.Feedrate = 123;
|
|
motion.VariableRefs = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["X"] = "value" };
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AssertGraph(Program source, Program copy)
|
|
{
|
|
var mapped = new Dictionary<Program, Program>(ReferenceEqualityComparer.Instance);
|
|
var reverse = new HashSet<Program>(ReferenceEqualityComparer.Instance);
|
|
Visit(source, copy);
|
|
void Visit(Program original, Program owned)
|
|
{
|
|
Assert.NotSame(original, owned);
|
|
if (mapped.TryGetValue(original, out var previous))
|
|
{
|
|
Assert.Same(previous, owned);
|
|
return;
|
|
}
|
|
Assert.True(reverse.Add(owned));
|
|
mapped.Add(original, owned);
|
|
Assert.Equal(original.Mode, owned.Mode);
|
|
Bits(original.Rotation, owned.Rotation);
|
|
Assert.NotSame(original.Codes, owned.Codes);
|
|
Assert.NotSame(original.Variables, owned.Variables);
|
|
Assert.NotSame(original.SubPrograms, owned.SubPrograms);
|
|
Assert.Equal(original.Variables.Keys, owned.Variables.Keys);
|
|
foreach (var (key, value) in original.Variables)
|
|
{
|
|
var actual = owned.Variables[key];
|
|
Assert.Equal(value.Name, actual.Name); Assert.Equal(value.Expression, actual.Expression);
|
|
Bits(value.Value, actual.Value); Assert.Equal(value.Inline, actual.Inline); Assert.Equal(value.Global, actual.Global);
|
|
}
|
|
Assert.Equal(original.Codes.Count, owned.Codes.Count);
|
|
for (var i = 0; i < original.Codes.Count; i++)
|
|
{
|
|
var code = original.Codes[i]; var actual = owned.Codes[i];
|
|
Assert.NotSame(code, actual); Assert.Equal(code.GetType(), actual.GetType());
|
|
if (code is Motion motion)
|
|
{
|
|
var m = Assert.IsAssignableFrom<Motion>(actual);
|
|
Point(motion.EndPoint, m.EndPoint);
|
|
Assert.Equal(motion.UseExactStop, m.UseExactStop); Assert.Equal(motion.Feedrate, m.Feedrate);
|
|
Assert.Equal(motion.Suppressed, m.Suppressed);
|
|
if (motion.VariableRefs == null) Assert.Null(m.VariableRefs);
|
|
else
|
|
{
|
|
Assert.NotSame(motion.VariableRefs, m.VariableRefs);
|
|
Assert.Equal(motion.VariableRefs, m.VariableRefs);
|
|
Assert.Equal(motion.VariableRefs.ContainsKey("x"), m.VariableRefs!.ContainsKey("x"));
|
|
}
|
|
if (motion is LinearMove line) Assert.Equal(line.Layer, ((LinearMove)m).Layer);
|
|
if (motion is ArcMove arc)
|
|
{
|
|
var a = Assert.IsType<ArcMove>(m);
|
|
Point(arc.CenterPoint, a.CenterPoint); Assert.Equal(arc.Rotation, a.Rotation); Assert.Equal(arc.Layer, a.Layer);
|
|
}
|
|
}
|
|
if (code is Comment comment) Assert.Equal(comment.Value, Assert.IsType<Comment>(actual).Value);
|
|
if (code is Feedrate feed)
|
|
{
|
|
var f = Assert.IsType<Feedrate>(actual); Bits(feed.Value, f.Value); Assert.Equal(feed.VariableRef, f.VariableRef);
|
|
}
|
|
if (code is Kerf kerf) Assert.Equal(kerf.Value, Assert.IsType<Kerf>(actual).Value);
|
|
if (code is SubProgramCall call)
|
|
{
|
|
var c = Assert.IsType<SubProgramCall>(actual);
|
|
Assert.Equal(call.Id, c.Id); Point(call.Offset, c.Offset); Bits(call.Rotation, c.Rotation);
|
|
Visit(call.Program, c.Program);
|
|
}
|
|
}
|
|
Assert.Equal(original.SubPrograms.Keys, owned.SubPrograms.Keys);
|
|
foreach (var (key, child) in original.SubPrograms) Visit(child, owned.SubPrograms[key]);
|
|
}
|
|
}
|
|
|
|
private static void Point(Vector expected, Vector actual) { Bits(expected.X, actual.X); Bits(expected.Y, actual.Y); }
|
|
private static void Bits(double expected, double actual) => Assert.Equal(BitConverter.DoubleToInt64Bits(expected), BitConverter.DoubleToInt64Bits(actual));
|
|
}
|