Add Gravograph IS post processor
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Posts.GravographIS;
|
||||
|
||||
namespace OpenNest.Tests.GravographIS;
|
||||
|
||||
public class EnvelopeGuardTests
|
||||
{
|
||||
// 0.610 m / 0.0125 mm/step = 48 800 steps = 24.0157 inches
|
||||
// 1.220 m / 0.0125 mm/step = 97 600 steps = 48.0315 inches
|
||||
|
||||
[Fact]
|
||||
public void NegativeX_FromOrigin_Throws()
|
||||
{
|
||||
// Operator origin is upper-left; quadrant 4 walks right/down. A cut that walks
|
||||
// left of origin in -X must be refused.
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(-1, 0) },
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<System.InvalidOperationException>(() =>
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter().Write(polylines, ms);
|
||||
});
|
||||
|
||||
Assert.Contains("Polyline 1", ex.Message);
|
||||
Assert.Contains("cut segment", ex.Message);
|
||||
Assert.Contains("segment 1", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositiveY_FromOrigin_Throws()
|
||||
{
|
||||
// Positive input-Y is above the upper-left origin in quadrant 4.
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(0, 1) },
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<System.InvalidOperationException>(() =>
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter().Write(polylines, ms);
|
||||
});
|
||||
|
||||
Assert.Contains("Polyline 1", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void XExceedsEnvelope_Throws_AndNamesSegment()
|
||||
{
|
||||
// 25" in X is past the 0.610 m (~24.02") envelope.
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(10, 0), new Vector(25, 0) },
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<System.InvalidOperationException>(() =>
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter().Write(polylines, ms);
|
||||
});
|
||||
|
||||
Assert.Contains("Polyline 1", ex.Message);
|
||||
Assert.Contains("segment 2", ex.Message); // 0→10 ok; 10→25 trips
|
||||
Assert.Contains("25.000\"", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void YExceedsEnvelope_Throws()
|
||||
{
|
||||
// -49" in Y is past the 1.220 m (~48.03") envelope.
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(0, -49) },
|
||||
};
|
||||
|
||||
Assert.Throws<System.InvalidOperationException>(() =>
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter().Write(polylines, ms);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PenUpTravel_OutsideEnvelope_AlsoThrows_AndIsLabeledTravel()
|
||||
{
|
||||
// Polyline 1 ends in-envelope; the PU travel to polyline 2 leaves it.
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(1, 0) },
|
||||
new[] { new Vector(30, 0), new Vector(30, -1) },
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<System.InvalidOperationException>(() =>
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter().Write(polylines, ms);
|
||||
});
|
||||
|
||||
Assert.Contains("Polyline 2", ex.Message);
|
||||
Assert.Contains("pen-up travel", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RightAtEnvelopeCorner_IsAllowed()
|
||||
{
|
||||
// Walk to (24", -48") in int16-sized hops (each delta < 16.1"). The
|
||||
// catalog envelope is 24.02" × 48.03", so this lands just inside.
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new Vector(0, 0),
|
||||
new Vector(8, -16),
|
||||
new Vector(16, -32),
|
||||
new Vector(24, -48),
|
||||
},
|
||||
};
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter().Write(polylines, ms); // no throw
|
||||
Assert.True(ms.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvelopeGuard_CanBeDisabled_ForOffMachineEncoding()
|
||||
{
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(-5, 0) },
|
||||
};
|
||||
|
||||
var opts = new GravographISWriterOptions { EnvelopeGuardEnabled = false };
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter(opts).Write(polylines, ms); // no throw
|
||||
Assert.True(ms.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CustomEnvelope_TightensTheCheck()
|
||||
{
|
||||
// Restrict to 1" × 1" — a 2" line in -Y now overshoots.
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(0, -2) },
|
||||
};
|
||||
|
||||
var opts = new GravographISWriterOptions
|
||||
{
|
||||
WorkEnvelopeXMm = 25.4,
|
||||
WorkEnvelopeYMm = 25.4,
|
||||
};
|
||||
|
||||
Assert.Throws<System.InvalidOperationException>(() =>
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter(opts).Write(polylines, ms);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Posts.GravographIS;
|
||||
|
||||
namespace OpenNest.Tests.GravographIS;
|
||||
|
||||
public class GravographISWriterTests
|
||||
{
|
||||
// 93-byte preamble captured from GravoStyle'98 (VS/VZ=35, DZ=508 → matches defaults).
|
||||
// The original capture ended with a DR command (FF FD 44 52) followed by three
|
||||
// 8-byte int16 records carrying a chunked job-specific travel (~1" X, ~47" Y).
|
||||
// Stripped from the writer (see GravographISWriter.PreambleTemplate) because
|
||||
// those frozen deltas send the head to a fixed point regardless of the job. The
|
||||
// writer now emits a job-specific leading DR travel from operator zero instead.
|
||||
private const string PreambleHex =
|
||||
"21 41 53 20 33 38 3b 01 90 01 f4 01 90 01 f4 01 90 01 f4 00 00 00 00 00 00 00 00 00 00 " +
|
||||
"00 00 00 09 00 00 03 e8 05 06 00 00 00 00 00 00 ff fd 32 44 00 00 ff fd 4d 43 00 01 ff fd " +
|
||||
"4f 55 ff fb ff fd 4f 55 ff fa ff fd 50 5a 00 00 ff fd 56 53 00 23 ff fd 56 5a 00 23 ff fd " +
|
||||
"44 5a 01 fc";
|
||||
|
||||
// Legacy 36-byte tail with lift, aux off, motor off, operator beep, job finish.
|
||||
// Byte-exact capture tests disable dynamic return-to-origin to preserve this form.
|
||||
private const string PostambleHex =
|
||||
"ff fd 50 55 00 01 ff fd 4f 55 ff fa ff fd 4f 55 ff fb ff fd 4d 43 00 00 " +
|
||||
"ff fd 4f 50 00 00 ff fd 4a 46 00 00";
|
||||
|
||||
[Fact]
|
||||
public void TestA_SingleTwoInchVerticalLine_IsByteExact()
|
||||
{
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(1, 1), new Vector(1, 3) },
|
||||
};
|
||||
|
||||
var writer = new GravographISWriter(new GravographISWriterOptions
|
||||
{
|
||||
DepthInches = 0.25,
|
||||
FeedMmPerSec = 35,
|
||||
EnvelopeGuardEnabled = false,
|
||||
ReturnToOriginAtEnd = false,
|
||||
});
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
writer.Write(polylines, ms);
|
||||
|
||||
const string GeomHex =
|
||||
"ff fd 44 52 00 00 2d 41 00 80 07 f0 f8 10 " +
|
||||
"ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20";
|
||||
var expected = HexToBytes(PreambleHex + " " + GeomHex + " " + PostambleHex);
|
||||
|
||||
Assert.Equal(expected, ms.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestB_FourLines_IsByteExact()
|
||||
{
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(1, 1), new Vector(1, 3) },
|
||||
new[] { new Vector(4, 1), new Vector(4, 3) },
|
||||
new[] { new Vector(4, 5), new Vector(4, 7) },
|
||||
new[] { new Vector(1, 5), new Vector(1, 7) },
|
||||
};
|
||||
|
||||
var writer = new GravographISWriter(new GravographISWriterOptions
|
||||
{
|
||||
DepthInches = 0.25,
|
||||
FeedMmPerSec = 35,
|
||||
EnvelopeGuardEnabled = false,
|
||||
ReturnToOriginAtEnd = false,
|
||||
});
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
writer.Write(polylines, ms);
|
||||
|
||||
const string GeomHex =
|
||||
"ff fd 44 52 00 00 2d 41 00 80 07 f0 f8 10 " +
|
||||
"ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " +
|
||||
"ff fd 50 55 00 00 35 40 00 b4 17 d0 0f e0 " +
|
||||
"ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " +
|
||||
"ff fd 50 55 00 00 40 00 00 b4 00 00 f0 20 " +
|
||||
"ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " +
|
||||
"ff fd 50 55 00 00 35 40 00 b4 e8 30 0f e0 " +
|
||||
"ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20";
|
||||
var expected = HexToBytes(PreambleHex + " " + GeomHex + " " + PostambleHex);
|
||||
|
||||
Assert.Equal(expected, ms.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeadingDR_TravelsToFirstPolylineStartBeforePD()
|
||||
{
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(2, 2), new Vector(3, 2) },
|
||||
};
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter(new GravographISWriterOptions { EnvelopeGuardEnabled = false }).Write(polylines, ms);
|
||||
|
||||
var bytes = ms.ToArray();
|
||||
// First command after the 93-byte preamble must be DR to the first point,
|
||||
// followed by PD for the first cut.
|
||||
Assert.Equal(0xFF, bytes[93]);
|
||||
Assert.Equal(0xFD, bytes[94]);
|
||||
Assert.Equal((byte)'D', bytes[95]);
|
||||
Assert.Equal((byte)'R', bytes[96]);
|
||||
|
||||
Assert.Equal(0xFF, bytes[107]);
|
||||
Assert.Equal(0xFD, bytes[108]);
|
||||
Assert.Equal((byte)'P', bytes[109]);
|
||||
Assert.Equal((byte)'D', bytes[110]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeadingDR_LongTravel_IsChunked()
|
||||
{
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(1, 47), new Vector(2, 47) },
|
||||
};
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter(new GravographISWriterOptions { EnvelopeGuardEnabled = false }).Write(polylines, ms);
|
||||
|
||||
var bytes = ms.ToArray();
|
||||
Assert.Equal((byte)'D', bytes[95]);
|
||||
Assert.Equal((byte)'R', bytes[96]);
|
||||
Assert.Equal((byte)'P', bytes[125]);
|
||||
Assert.Equal((byte)'D', bytes[126]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionsPatchVsVzDz()
|
||||
{
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(0.5, 0) },
|
||||
};
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter(new GravographISWriterOptions
|
||||
{
|
||||
DepthInches = 0.125, // 254 steps = 0x00FE
|
||||
FeedMmPerSec = 50, // 0x0032
|
||||
}).Write(polylines, ms);
|
||||
|
||||
var bytes = ms.ToArray();
|
||||
AssertOperand(bytes, (byte)'V', (byte)'S', 0x00, 0x32);
|
||||
AssertOperand(bytes, (byte)'V', (byte)'Z', 0x00, 0x32);
|
||||
AssertOperand(bytes, (byte)'D', (byte)'Z', 0x00, 0xFE);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsToOriginAfterFinalLift_ByDefault()
|
||||
{
|
||||
var polylines = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(1, 0), new Vector(1, -1) },
|
||||
};
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
new GravographISWriter().Write(polylines, ms);
|
||||
|
||||
var bytes = ms.ToArray();
|
||||
var liftIndex = LastIndexOfCommand(bytes, (byte)'P', (byte)'U', 0x00, 0x01);
|
||||
Assert.True(liftIndex >= 0);
|
||||
|
||||
Assert.Equal(0xFF, bytes[liftIndex + 6]);
|
||||
Assert.Equal(0xFD, bytes[liftIndex + 7]);
|
||||
Assert.Equal((byte)'P', bytes[liftIndex + 8]);
|
||||
Assert.Equal((byte)'U', bytes[liftIndex + 9]);
|
||||
|
||||
var dx = ReadInt16(bytes, liftIndex + 16);
|
||||
var dy = ReadInt16(bytes, liftIndex + 18);
|
||||
Assert.Equal(-GravographISWriter.StepsPerInch, dx);
|
||||
Assert.Equal(-GravographISWriter.StepsPerInch, dy);
|
||||
}
|
||||
|
||||
private static void AssertOperand(byte[] bytes, byte c0, byte c1, byte hi, byte lo)
|
||||
{
|
||||
for (var i = 0; i < bytes.Length - 5; i++)
|
||||
{
|
||||
if (bytes[i] == 0xFF && bytes[i + 1] == 0xFD && bytes[i + 2] == c0 && bytes[i + 3] == c1)
|
||||
{
|
||||
Assert.Equal(hi, bytes[i + 4]);
|
||||
Assert.Equal(lo, bytes[i + 5]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Assert.Fail($"Command {(char)c0}{(char)c1} not found in stream.");
|
||||
}
|
||||
|
||||
private static int LastIndexOfCommand(byte[] bytes, byte c0, byte c1, byte hi, byte lo)
|
||||
{
|
||||
for (var i = bytes.Length - 6; i >= 0; i--)
|
||||
{
|
||||
if (bytes[i] == 0xFF && bytes[i + 1] == 0xFD &&
|
||||
bytes[i + 2] == c0 && bytes[i + 3] == c1 &&
|
||||
bytes[i + 4] == hi && bytes[i + 5] == lo)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static short ReadInt16(byte[] bytes, int offset)
|
||||
{
|
||||
return unchecked((short)((bytes[offset] << 8) | bytes[offset + 1]));
|
||||
}
|
||||
|
||||
internal static byte[] HexToBytes(string hex)
|
||||
{
|
||||
var clean = hex.Replace(" ", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty);
|
||||
var bytes = new byte[clean.Length / 2];
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
bytes[i] = System.Convert.ToByte(clean.Substring(i * 2, 2), 16);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using OpenNest;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Posts.GravographIS;
|
||||
|
||||
namespace OpenNest.Tests.GravographIS;
|
||||
|
||||
public class NestPolylineExtractorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExtractPart_IncrementalProgram_ProducesAbsoluteCoordinates()
|
||||
{
|
||||
// 1x1 square in G91 (incremental) mode — the form OpenNest's UI writes
|
||||
// to .nest files. Without absolute-mode handling the extractor plotted
|
||||
// each EndPoint as if it were absolute, producing a 2x2 diamond.
|
||||
var program = new Program(Mode.Incremental);
|
||||
program.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
program.Codes.Add(new LinearMove(1, 0));
|
||||
program.Codes.Add(new LinearMove(0, 1));
|
||||
program.Codes.Add(new LinearMove(-1, 0));
|
||||
program.Codes.Add(new LinearMove(0, -1));
|
||||
|
||||
var drawing = new Drawing("Square 1x1", program);
|
||||
var part = new Part(drawing, new Vector(0.25, 46.75));
|
||||
|
||||
var polylines = new NestPolylineExtractor().ExtractPart(part);
|
||||
|
||||
Assert.Single(polylines);
|
||||
var poly = polylines[0];
|
||||
Assert.Equal(5, poly.Count);
|
||||
Assert.Equal(new Vector(0.25, 46.75), poly[0]);
|
||||
Assert.Equal(new Vector(1.25, 46.75), poly[1]);
|
||||
Assert.Equal(new Vector(1.25, 47.75), poly[2]);
|
||||
Assert.Equal(new Vector(0.25, 47.75), poly[3]);
|
||||
Assert.Equal(new Vector(0.25, 46.75), poly[4]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Posts.GravographIS;
|
||||
|
||||
namespace OpenNest.Tests.GravographIS;
|
||||
|
||||
public class PolylinePrePassTests
|
||||
{
|
||||
[Fact]
|
||||
public void Stitch_TwoConnectedSegments_BecomeOnePolyline()
|
||||
{
|
||||
var inputs = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(1, 0) },
|
||||
new[] { new Vector(1, 0), new Vector(1, 1) },
|
||||
};
|
||||
|
||||
var stitched = PolylinePrePass.Stitch(inputs);
|
||||
|
||||
Assert.Single(stitched);
|
||||
Assert.Equal(3, stitched[0].Count);
|
||||
Assert.Equal(new Vector(0, 0), stitched[0][0]);
|
||||
Assert.Equal(new Vector(1, 0), stitched[0][1]);
|
||||
Assert.Equal(new Vector(1, 1), stitched[0][2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stitch_FourSegmentsFormingClosedSquare_BecomeOnePolyline()
|
||||
{
|
||||
var inputs = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(1, 0) },
|
||||
new[] { new Vector(1, 0), new Vector(1, 1) },
|
||||
new[] { new Vector(1, 1), new Vector(0, 1) },
|
||||
new[] { new Vector(0, 1), new Vector(0, 0) },
|
||||
};
|
||||
|
||||
var stitched = PolylinePrePass.Stitch(inputs);
|
||||
|
||||
Assert.Single(stitched);
|
||||
// Four edges + closing return-to-start = five vertices.
|
||||
Assert.Equal(5, stitched[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stitch_ReversesOneSegmentToMakeAJoin()
|
||||
{
|
||||
// Second segment is given backward; stitcher should reverse it.
|
||||
var inputs = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(1, 0) },
|
||||
new[] { new Vector(2, 0), new Vector(1, 0) },
|
||||
};
|
||||
|
||||
var stitched = PolylinePrePass.Stitch(inputs);
|
||||
|
||||
Assert.Single(stitched);
|
||||
Assert.Equal(3, stitched[0].Count);
|
||||
Assert.Equal(new Vector(0, 0), stitched[0][0]);
|
||||
Assert.Equal(new Vector(2, 0), stitched[0][stitched[0].Count - 1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stitch_DisjointSegments_StayDistinct()
|
||||
{
|
||||
var inputs = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(1, 0) },
|
||||
new[] { new Vector(5, 5), new Vector(6, 5) },
|
||||
};
|
||||
|
||||
var stitched = PolylinePrePass.Stitch(inputs);
|
||||
|
||||
Assert.Equal(2, stitched.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stitch_DropsZeroAndSinglePointPolylines()
|
||||
{
|
||||
var inputs = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new Vector[] { },
|
||||
new[] { new Vector(0, 0) },
|
||||
new[] { new Vector(0, 0), new Vector(1, 0) },
|
||||
};
|
||||
|
||||
var stitched = PolylinePrePass.Stitch(inputs);
|
||||
|
||||
Assert.Single(stitched);
|
||||
Assert.Equal(2, stitched[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reorder_ReducesTotalPenUpTravelVsWorstCase()
|
||||
{
|
||||
// Three short polylines at (0,0), (10,0), (5,0). The greedy NN starting
|
||||
// from origin should pick (0,0)→(5,0)→(10,0) (travels of 4 + 4 ≈ 8) over
|
||||
// the worst-case input order (0,0)→(10,0)→(5,0) (travels 9 + 4 ≈ 13).
|
||||
var inputs = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(0, 0), new Vector(1, 0) },
|
||||
new[] { new Vector(10, 0), new Vector(11, 0) },
|
||||
new[] { new Vector(5, 0), new Vector(6, 0) },
|
||||
};
|
||||
|
||||
var reordered = PolylinePrePass.Reorder(inputs);
|
||||
|
||||
Assert.Equal(3, reordered.Count);
|
||||
var travelBefore = TotalPenUpTravel(inputs);
|
||||
var travelAfter = TotalPenUpTravel(reordered);
|
||||
Assert.True(travelAfter < travelBefore,
|
||||
$"Expected reorder to reduce pen-up travel; before={travelBefore}, after={travelAfter}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reorder_ReversesPolylineIfTailIsCloser()
|
||||
{
|
||||
// Origin (0,0); a single polyline whose tail is much closer to origin
|
||||
// than its head. Reorder should flip it.
|
||||
var inputs = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(10, 0), new Vector(0.5, 0) },
|
||||
};
|
||||
|
||||
var reordered = PolylinePrePass.Reorder(inputs, allowReverse: true);
|
||||
|
||||
Assert.Single(reordered);
|
||||
Assert.Equal(new Vector(0.5, 0), reordered[0][0]);
|
||||
Assert.Equal(new Vector(10, 0), reordered[0][1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reorder_ReverseDisabled_KeepsDirection()
|
||||
{
|
||||
var inputs = new List<IReadOnlyList<Vector>>
|
||||
{
|
||||
new[] { new Vector(10, 0), new Vector(0.5, 0) },
|
||||
};
|
||||
|
||||
var reordered = PolylinePrePass.Reorder(inputs, allowReverse: false);
|
||||
|
||||
Assert.Single(reordered);
|
||||
Assert.Equal(new Vector(10, 0), reordered[0][0]);
|
||||
Assert.Equal(new Vector(0.5, 0), reordered[0][1]);
|
||||
}
|
||||
|
||||
private static double TotalPenUpTravel(IEnumerable<IReadOnlyList<Vector>> polylines)
|
||||
{
|
||||
var total = 0.0;
|
||||
Vector? last = null;
|
||||
foreach (var p in polylines)
|
||||
{
|
||||
if (p == null || p.Count < 2) continue;
|
||||
if (last.HasValue)
|
||||
{
|
||||
var dx = p[0].X - last.Value.X;
|
||||
var dy = p[0].Y - last.Value.Y;
|
||||
total += System.Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
last = p[p.Count - 1];
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Posts.GravographIS\OpenNest.Posts.GravographIS.csproj" />
|
||||
<ProjectReference Include="..\OpenNest\OpenNest.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user