Compare commits
52 Commits
a3ae61d993
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e493d83899 | |||
| 987a5e25bc | |||
| 86582d28c3 | |||
| f064368008 | |||
| 9148797897 | |||
| da77cc9270 | |||
| 27f0685058 | |||
| 53988acefc | |||
| a8d90be2ea | |||
| c25b6bc23a | |||
| 1c994718fb | |||
| 9d58e6fba8 | |||
| 2bae5340f0 | |||
| 0b322817d7 | |||
| e41f335c63 | |||
| 0ab33af5d3 | |||
| e04c9381f3 | |||
| ceb9cc0b44 | |||
| 4cecaba83a | |||
| 4053f1f989 | |||
| ca67b1bd29 | |||
| 199095ee43 | |||
| eb493d501a | |||
| 6c98732117 | |||
| a2e9fd4d14 | |||
| d228b6b812 | |||
| c634aecd4b | |||
| 14b7c1cf32 | |||
| 402af91af5 | |||
| 9a6b656e3c | |||
| d2f9597b0c | |||
| c40dcf0e25 | |||
| 28653e3a9f | |||
| 7c3246c6e7 | |||
| bd48f57ce0 | |||
| a6ec21accc | |||
| 320cf40f41 | |||
| 3beca10429 | |||
| 8bea5dac6c | |||
| 12f8bbf8f5 | |||
| d15790b948 | |||
| d80f76e386 | |||
| 07bce8699a | |||
| 9b84508ff4 | |||
| 6fdf0ad3c5 | |||
| 4f7bfcc3ad | |||
| 3c53d6fecd | |||
| e239967a7b | |||
| 9d57d3875a | |||
| 0e299d7f6f | |||
| c6f544c5d7 | |||
| 9563094c2b |
@@ -213,3 +213,6 @@ docs/superpowers/
|
|||||||
|
|
||||||
# Launch settings
|
# Launch settings
|
||||||
**/Properties/launchSettings.json
|
**/Properties/launchSettings.json
|
||||||
|
|
||||||
|
# Local test config (contains user-specific paths to proprietary test assets)
|
||||||
|
OpenNest.Tests/test-config.json
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ static class NestConsole
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using var log = SetUpLog(options);
|
|
||||||
var nest = LoadOrCreateNest(options);
|
var nest = LoadOrCreateNest(options);
|
||||||
|
|
||||||
if (nest == null)
|
if (nest == null)
|
||||||
@@ -68,10 +67,6 @@ static class NestConsole
|
|||||||
|
|
||||||
var overlapCount = CheckOverlaps(plate, options);
|
var overlapCount = CheckOverlaps(plate, options);
|
||||||
|
|
||||||
// Flush and close the log before printing results.
|
|
||||||
Trace.Flush();
|
|
||||||
log?.Dispose();
|
|
||||||
|
|
||||||
PrintResults(success, plate, elapsed);
|
PrintResults(success, plate, elapsed);
|
||||||
Save(nest, options);
|
Save(nest, options);
|
||||||
PostProcess(nest, options);
|
PostProcess(nest, options);
|
||||||
@@ -112,9 +107,6 @@ static class NestConsole
|
|||||||
case "--no-save":
|
case "--no-save":
|
||||||
o.NoSave = true;
|
o.NoSave = true;
|
||||||
break;
|
break;
|
||||||
case "--no-log":
|
|
||||||
o.NoLog = true;
|
|
||||||
break;
|
|
||||||
case "--keep-parts":
|
case "--keep-parts":
|
||||||
o.KeepParts = true;
|
o.KeepParts = true;
|
||||||
break;
|
break;
|
||||||
@@ -153,28 +145,14 @@ static class NestConsole
|
|||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
static StreamWriter SetUpLog(Options options)
|
|
||||||
{
|
|
||||||
if (options.NoLog)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var baseDir = Path.GetDirectoryName(options.InputFiles[0]);
|
|
||||||
var logDir = Path.Combine(baseDir, "test-harness-logs");
|
|
||||||
Directory.CreateDirectory(logDir);
|
|
||||||
var logFile = Path.Combine(logDir, $"debug-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
|
||||||
var writer = new StreamWriter(logFile) { AutoFlush = true };
|
|
||||||
Trace.Listeners.Add(new TextWriterTraceListener(writer));
|
|
||||||
Console.WriteLine($"Debug log: {logFile}");
|
|
||||||
return writer;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Nest LoadOrCreateNest(Options options)
|
static Nest LoadOrCreateNest(Options options)
|
||||||
{
|
{
|
||||||
var nestFile = options.InputFiles.FirstOrDefault(f =>
|
var nestFile = options.InputFiles.FirstOrDefault(f =>
|
||||||
f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)
|
f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)
|
||||||
|| f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
|| f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||||
var dxfFiles = options.InputFiles.Where(f =>
|
var dxfFiles = options.InputFiles.Where(f =>
|
||||||
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)).ToList();
|
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
f.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
|
||||||
// If we have a nest file, load it and optionally add DXFs.
|
// If we have a nest file, load it and optionally add DXFs.
|
||||||
if (nestFile != null)
|
if (nestFile != null)
|
||||||
@@ -210,7 +188,7 @@ static class NestConsole
|
|||||||
// DXF-only mode: create a fresh nest.
|
// DXF-only mode: create a fresh nest.
|
||||||
if (dxfFiles.Count == 0)
|
if (dxfFiles.Count == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("Error: no nest (.nest) or DXF (.dxf) files specified");
|
Console.Error.WriteLine("Error: no nest (.nest) or CAD (.dxf/.dwg) files specified");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +462,7 @@ static class NestConsole
|
|||||||
Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [options]");
|
Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [options]");
|
||||||
Console.Error.WriteLine();
|
Console.Error.WriteLine();
|
||||||
Console.Error.WriteLine("Arguments:");
|
Console.Error.WriteLine("Arguments:");
|
||||||
Console.Error.WriteLine(" input-files One or more .nest nest files or .dxf drawing files");
|
Console.Error.WriteLine(" input-files One or more .nest nest files or .dxf/.dwg drawing files");
|
||||||
Console.Error.WriteLine();
|
Console.Error.WriteLine();
|
||||||
Console.Error.WriteLine("Modes:");
|
Console.Error.WriteLine("Modes:");
|
||||||
Console.Error.WriteLine(" <nest.nest> Load nest and fill (existing behavior)");
|
Console.Error.WriteLine(" <nest.nest> Load nest and fill (existing behavior)");
|
||||||
@@ -503,7 +481,6 @@ static class NestConsole
|
|||||||
Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling");
|
Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling");
|
||||||
Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
|
Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
|
||||||
Console.Error.WriteLine(" --no-save Skip saving output file");
|
Console.Error.WriteLine(" --no-save Skip saving output file");
|
||||||
Console.Error.WriteLine(" --no-log Skip writing debug log file");
|
|
||||||
Console.Error.WriteLine(" --post <name> Run a post processor after nesting");
|
Console.Error.WriteLine(" --post <name> Run a post processor after nesting");
|
||||||
Console.Error.WriteLine(" --post-output <path> Output file for post processor (default: <input>.cnc)");
|
Console.Error.WriteLine(" --post-output <path> Output file for post processor (default: <input>.cnc)");
|
||||||
Console.Error.WriteLine(" --posts-dir <path> Directory containing post processor DLLs (default: Posts/)");
|
Console.Error.WriteLine(" --posts-dir <path> Directory containing post processor DLLs (default: Posts/)");
|
||||||
@@ -522,7 +499,6 @@ static class NestConsole
|
|||||||
public Size? PlateSize;
|
public Size? PlateSize;
|
||||||
public bool CheckOverlaps;
|
public bool CheckOverlaps;
|
||||||
public bool NoSave;
|
public bool NoSave;
|
||||||
public bool NoLog;
|
|
||||||
public bool KeepParts;
|
public bool KeepParts;
|
||||||
public bool AutoNest;
|
public bool AutoNest;
|
||||||
public string TemplateFile;
|
public string TemplateFile;
|
||||||
|
|||||||
@@ -305,9 +305,6 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
subPgm.Codes.AddRange(leadIn.Generate(relativePoint, normal, winding));
|
subPgm.Codes.AddRange(leadIn.Generate(relativePoint, normal, winding));
|
||||||
var reindexed = relativeShape.ReindexAt(relativePoint, relativeCircle);
|
var reindexed = relativeShape.ReindexAt(relativePoint, relativeCircle);
|
||||||
|
|
||||||
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
|
||||||
reindexed = TrimShapeForTab(reindexed, relativePoint, Parameters.TabConfig.Size);
|
|
||||||
|
|
||||||
subPgm.Codes.AddRange(ConvertShapeToMoves(reindexed, relativePoint));
|
subPgm.Codes.AddRange(ConvertShapeToMoves(reindexed, relativePoint));
|
||||||
subPgm.Codes.AddRange(leadOut.Generate(relativePoint, normal, winding));
|
subPgm.Codes.AddRange(leadOut.Generate(relativePoint, normal, winding));
|
||||||
subPgm.Mode = Mode.Incremental;
|
subPgm.Mode = Mode.Incremental;
|
||||||
@@ -331,7 +328,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
var reindexedShape = shape.ReindexAt(point, entity);
|
var reindexedShape = shape.ReindexAt(point, entity);
|
||||||
|
|
||||||
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
if (Parameters.TabsEnabled && Parameters.TabConfig != null && contourType == ContourType.External)
|
||||||
reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size);
|
reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size);
|
||||||
|
|
||||||
program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point));
|
program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point));
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var code = Codes[i];
|
var code = Codes[i];
|
||||||
|
|
||||||
|
if (code is SubProgramCall subpgm)
|
||||||
|
{
|
||||||
|
subpgm.Offset = new Geometry.Vector(
|
||||||
|
subpgm.Offset.X + x, subpgm.Offset.Y + y);
|
||||||
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
if (code is Motion == false)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -150,6 +156,12 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var code = Codes[i];
|
var code = Codes[i];
|
||||||
|
|
||||||
|
if (code is SubProgramCall subpgm)
|
||||||
|
{
|
||||||
|
subpgm.Offset = new Geometry.Vector(
|
||||||
|
subpgm.Offset.X + voffset.X, subpgm.Offset.Y + voffset.Y);
|
||||||
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
if (code is Motion == false)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC
|
||||||
|
{
|
||||||
|
public static class RapidEnumerator
|
||||||
|
{
|
||||||
|
public readonly record struct Segment(Vector From, Vector To);
|
||||||
|
|
||||||
|
public static List<Segment> Enumerate(Program pgm, Vector basePos, Vector startPos)
|
||||||
|
{
|
||||||
|
var results = new List<Segment>();
|
||||||
|
|
||||||
|
// Draw the rapid from the previous tool position to the program's first
|
||||||
|
// pierce point. This also primes pos so the interior walk interprets
|
||||||
|
// Incremental deltas from the correct absolute location (basePos), which
|
||||||
|
// matters for raw pre-lead-in programs that are emitted Incremental.
|
||||||
|
var firstPierce = FirstPiercePoint(pgm, basePos);
|
||||||
|
results.Add(new Segment(startPos, firstPierce));
|
||||||
|
|
||||||
|
var pos = firstPierce;
|
||||||
|
Walk(pgm, basePos, ref pos, skipFirst: true, results);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector FirstPiercePoint(Program pgm, Vector basePos)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < pgm.Length; i++)
|
||||||
|
{
|
||||||
|
if (pgm[i] is SubProgramCall call && call.Program != null)
|
||||||
|
return FirstPiercePoint(call.Program, basePos + call.Offset);
|
||||||
|
|
||||||
|
if (pgm[i] is Motion motion)
|
||||||
|
return motion.EndPoint + basePos;
|
||||||
|
}
|
||||||
|
return basePos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Walk(Program pgm, Vector basePos, ref Vector pos, bool skipFirst, List<Segment> results)
|
||||||
|
{
|
||||||
|
var skipped = !skipFirst;
|
||||||
|
|
||||||
|
for (var i = 0; i < pgm.Length; ++i)
|
||||||
|
{
|
||||||
|
var code = pgm[i];
|
||||||
|
|
||||||
|
if (code is SubProgramCall { Program: { } program } call)
|
||||||
|
{
|
||||||
|
var holeBase = basePos + call.Offset;
|
||||||
|
var firstPierce = FirstPiercePoint(program, holeBase);
|
||||||
|
|
||||||
|
if (!skipped)
|
||||||
|
skipped = true;
|
||||||
|
else
|
||||||
|
results.Add(new Segment(pos, firstPierce));
|
||||||
|
|
||||||
|
var subPos = holeBase;
|
||||||
|
Walk(program, holeBase, ref subPos, skipFirst: true, results);
|
||||||
|
pos = subPos;
|
||||||
|
}
|
||||||
|
else if (code is Motion motion)
|
||||||
|
{
|
||||||
|
var endpt = pgm.Mode == Mode.Incremental
|
||||||
|
? motion.EndPoint + pos
|
||||||
|
: motion.EndPoint + basePos;
|
||||||
|
|
||||||
|
if (code.Type == CodeType.RapidMove)
|
||||||
|
{
|
||||||
|
if (!skipped)
|
||||||
|
skipped = true;
|
||||||
|
else
|
||||||
|
results.Add(new Segment(pos, endpt));
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = endpt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the rotation that maps a drawing to its canonical (MBR-axis-aligned) frame.
|
||||||
|
/// Lives in OpenNest.Core so Drawing.Program setter can invoke it directly without
|
||||||
|
/// a circular dependency on OpenNest.Engine.
|
||||||
|
/// </summary>
|
||||||
|
public static class CanonicalAngle
|
||||||
|
{
|
||||||
|
/// <summary>Angles with |v| below this (radians) are snapped to 0.</summary>
|
||||||
|
public const double SnapToZero = 0.001;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Derives the canonical angle from a pre-computed MBR. Used both by Compute (which
|
||||||
|
/// computes the MBR itself) and by PartClassifier (which already has one). Single formula
|
||||||
|
/// across both callers.
|
||||||
|
/// </summary>
|
||||||
|
public static double FromMbr(BoundingRectangleResult mbr)
|
||||||
|
{
|
||||||
|
if (mbr.Area <= OpenNest.Math.Tolerance.Epsilon)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
// The MBR edge angle can represent any of four equivalent orientations
|
||||||
|
// (edge-i, edge-i + π/2, edge-i + π, edge-i - π/2) depending on which hull
|
||||||
|
// edge the algorithm happened to pick. Normalize -mbr.Angle to the
|
||||||
|
// representative in [-π/4, π/4] so snap-to-zero works for inputs near
|
||||||
|
// ANY of the equivalent orientations.
|
||||||
|
var angle = -mbr.Angle;
|
||||||
|
const double halfPi = System.Math.PI / 2.0;
|
||||||
|
angle -= halfPi * System.Math.Round(angle / halfPi);
|
||||||
|
|
||||||
|
if (System.Math.Abs(angle) < SnapToZero)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
return angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double Compute(Drawing drawing)
|
||||||
|
{
|
||||||
|
if (drawing?.Program == null)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
|
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
if (shapes.Count == 0)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
var perimeter = shapes[0];
|
||||||
|
var perimeterArea = perimeter.Area();
|
||||||
|
for (var i = 1; i < shapes.Count; i++)
|
||||||
|
{
|
||||||
|
var area = shapes[i].Area();
|
||||||
|
if (area > perimeterArea)
|
||||||
|
{
|
||||||
|
perimeter = shapes[i];
|
||||||
|
perimeterArea = area;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var polygon = perimeter.ToPolygonWithTolerance(0.1);
|
||||||
|
if (polygon == null || polygon.Vertices.Count < 3)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
var hull = ConvexHull.Compute(polygon.Vertices);
|
||||||
|
if (hull.Vertices.Count < 3)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
var mbr = RotatingCalipers.MinimumBoundingRectangle(hull);
|
||||||
|
return FromMbr(mbr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.Converters
|
namespace OpenNest.Converters
|
||||||
@@ -81,12 +82,21 @@ namespace OpenNest.Converters
|
|||||||
var startpt = arc.StartPoint();
|
var startpt = arc.StartPoint();
|
||||||
var endpt = arc.EndPoint();
|
var endpt = arc.EndPoint();
|
||||||
|
|
||||||
if (startpt != lastpt)
|
if (startpt.DistanceTo(lastpt) > Tolerance.ChainTolerance)
|
||||||
pgm.MoveTo(startpt);
|
pgm.MoveTo(startpt);
|
||||||
|
|
||||||
lastpt = endpt;
|
lastpt = endpt;
|
||||||
|
|
||||||
|
var sweep = System.Math.Abs(arc.SweepAngle());
|
||||||
|
if (sweep < Tolerance.Epsilon || sweep.IsEqualTo(Angle.TwoPI))
|
||||||
|
{
|
||||||
|
pgm.LineTo(endpt);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
pgm.ArcTo(endpt, arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW);
|
pgm.ArcTo(endpt, arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW);
|
||||||
|
}
|
||||||
|
|
||||||
return lastpt;
|
return lastpt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +104,7 @@ namespace OpenNest.Converters
|
|||||||
{
|
{
|
||||||
var startpt = new Vector(circle.Center.X + circle.Radius, circle.Center.Y);
|
var startpt = new Vector(circle.Center.X + circle.Radius, circle.Center.Y);
|
||||||
|
|
||||||
if (startpt != lastpt)
|
if (startpt.DistanceTo(lastpt) > Tolerance.ChainTolerance)
|
||||||
pgm.MoveTo(startpt);
|
pgm.MoveTo(startpt);
|
||||||
|
|
||||||
pgm.ArcTo(startpt, circle.Center, circle.Rotation);
|
pgm.ArcTo(startpt, circle.Center, circle.Rotation);
|
||||||
@@ -105,7 +115,7 @@ namespace OpenNest.Converters
|
|||||||
|
|
||||||
private static Vector AddLine(Program pgm, Vector lastpt, Line line)
|
private static Vector AddLine(Program pgm, Vector lastpt, Line line)
|
||||||
{
|
{
|
||||||
if (line.StartPoint != lastpt)
|
if (line.StartPoint.DistanceTo(lastpt) > Tolerance.ChainTolerance)
|
||||||
pgm.MoveTo(line.StartPoint);
|
pgm.MoveTo(line.StartPoint);
|
||||||
|
|
||||||
var move = new LinearMove(line.EndPoint);
|
var move = new LinearMove(line.EndPoint);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace OpenNest
|
|||||||
private static int nextColorIndex;
|
private static int nextColorIndex;
|
||||||
private Program program;
|
private Program program;
|
||||||
|
|
||||||
public static readonly Color[] PartColors = new Color[]
|
public static Color[] PartColors = new Color[]
|
||||||
{
|
{
|
||||||
Color.FromArgb(205, 92, 92), // Indian Red
|
Color.FromArgb(205, 92, 92), // Indian Red
|
||||||
Color.FromArgb(148, 103, 189), // Medium Purple
|
Color.FromArgb(148, 103, 189), // Medium Purple
|
||||||
@@ -54,9 +54,9 @@ namespace OpenNest
|
|||||||
Id = Interlocked.Increment(ref nextId);
|
Id = Interlocked.Increment(ref nextId);
|
||||||
Name = name;
|
Name = name;
|
||||||
Material = new Material();
|
Material = new Material();
|
||||||
Program = pgm;
|
|
||||||
Constraints = new NestConstraints();
|
Constraints = new NestConstraints();
|
||||||
Source = new SourceInfo();
|
Source = new SourceInfo();
|
||||||
|
Program = pgm;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
@@ -78,9 +78,29 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
program = value;
|
program = value;
|
||||||
UpdateArea();
|
UpdateArea();
|
||||||
|
RecomputeCanonicalAngle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recomputes and stores the canonical angle from the current Program.
|
||||||
|
/// Callers that mutate Program in place (rather than reassigning it) must invoke this explicitly.
|
||||||
|
/// Cut-off drawings are left with Angle=0.
|
||||||
|
/// </summary>
|
||||||
|
public void RecomputeCanonicalAngle()
|
||||||
|
{
|
||||||
|
if (Source == null)
|
||||||
|
Source = new SourceInfo();
|
||||||
|
|
||||||
|
if (program == null || IsCutOff)
|
||||||
|
{
|
||||||
|
Source.Angle = 0.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Source.Angle = CanonicalAngle.Compute(this);
|
||||||
|
}
|
||||||
|
|
||||||
public Color Color { get; set; }
|
public Color Color { get; set; }
|
||||||
|
|
||||||
public bool IsCutOff { get; set; }
|
public bool IsCutOff { get; set; }
|
||||||
@@ -163,5 +183,15 @@ namespace OpenNest
|
|||||||
/// Offset distances to the original location.
|
/// Offset distances to the original location.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector Offset { get; set; }
|
public Vector Offset { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rotation (radians) that maps the source program geometry to its canonical
|
||||||
|
/// (MBR-axis-aligned) frame. Populated automatically by the <see cref="Drawing.Program"/>
|
||||||
|
/// setter via <see cref="CanonicalAngle.Compute"/>. A value of 0 means the drawing is
|
||||||
|
/// already canonical or <see cref="Drawing.IsCutOff"/> is true. Callers that mutate
|
||||||
|
/// <see cref="Drawing.Program"/> in place must invoke
|
||||||
|
/// <see cref="Drawing.RecomputeCanonicalAngle"/> to refresh.
|
||||||
|
/// </summary>
|
||||||
|
public double Angle { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsFullCircle() =>
|
||||||
|
SweepAngle() >= Angle.TwoPI - Tolerance.Epsilon;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Angle in radians between start and end angles.
|
/// Angle in radians between start and end angles.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -404,10 +407,12 @@ namespace OpenNest.Geometry
|
|||||||
maxY = startpt.Y;
|
maxY = startpt.Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sweep = SweepAngle();
|
||||||
|
if (sweep > Tolerance.Epsilon)
|
||||||
|
{
|
||||||
var angle1 = StartAngle;
|
var angle1 = StartAngle;
|
||||||
var angle2 = EndAngle;
|
var angle2 = EndAngle;
|
||||||
|
|
||||||
// switch the angle to counter clockwise.
|
|
||||||
if (IsReversed)
|
if (IsReversed)
|
||||||
Generic.Swap(ref angle1, ref angle2);
|
Generic.Swap(ref angle1, ref angle2);
|
||||||
|
|
||||||
@@ -424,6 +429,7 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
if (Angle.IsBetweenRad(Angle.TwoPI, angle1, angle2))
|
if (Angle.IsBetweenRad(Angle.TwoPI, angle1, angle2))
|
||||||
maxX = Center.X + Radius;
|
maxX = Center.X + Radius;
|
||||||
|
}
|
||||||
|
|
||||||
boundingBox.X = minX;
|
boundingBox.X = minX;
|
||||||
boundingBox.Y = minY;
|
boundingBox.Y = minY;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using OpenNest.Math;
|
using System;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
public class Box
|
public class Box : IComparable<Box>
|
||||||
{
|
{
|
||||||
public static readonly Box Empty = new Box();
|
public static readonly Box Empty = new Box();
|
||||||
|
|
||||||
@@ -214,5 +215,19 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
return string.Format("[Box: X={0}, Y={1}, Width={2}, Length={3}]", X, Y, Width, Length);
|
return string.Format("[Box: X={0}, Y={1}, Width={2}, Length={3}]", X, Y, Width, Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int CompareTo(Box other)
|
||||||
|
{
|
||||||
|
var cmp = Width.CompareTo(other.Width);
|
||||||
|
return cmp != 0 ? cmp : Length.CompareTo(other.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator >(Box a, Box b) => a.CompareTo(b) > 0;
|
||||||
|
|
||||||
|
public static bool operator <(Box a, Box b) => a.CompareTo(b) < 0;
|
||||||
|
|
||||||
|
public static bool operator >=(Box a, Box b) => a.CompareTo(b) >= 0;
|
||||||
|
|
||||||
|
public static bool operator <=(Box a, Box b) => a.CompareTo(b) <= 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,11 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
if (maxDev <= tolerance)
|
if (maxDev <= tolerance)
|
||||||
{
|
{
|
||||||
results.Add(CreateArc(arcCenter, radius, center, semiMajor, semiMinor, rotation, t0, t1));
|
var arc = CreateArc(arcCenter, radius, center, semiMajor, semiMinor, rotation, t0, t1);
|
||||||
|
if (arc.SweepAngle() < Tolerance.Epsilon)
|
||||||
|
results.Add(new Line(p0, p1));
|
||||||
|
else
|
||||||
|
results.Add(arc);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,38 @@ namespace OpenNest.Geometry
|
|||||||
(list, item, i) => list.GetCollinearLines(item, i),
|
(list, item, i) => list.GetCollinearLines(item, i),
|
||||||
(Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined));
|
(Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined));
|
||||||
|
|
||||||
|
public static void Deduplicate(IList<Circle> circles)
|
||||||
|
{
|
||||||
|
for (var i = circles.Count - 1; i >= 1; i--)
|
||||||
|
{
|
||||||
|
for (var j = i - 1; j >= 0; j--)
|
||||||
|
{
|
||||||
|
if (circles[i].Center.DistanceTo(circles[j].Center) <= Tolerance.Epsilon
|
||||||
|
&& circles[i].Radius.IsEqualTo(circles[j].Radius))
|
||||||
|
{
|
||||||
|
circles.RemoveAt(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Deduplicate(IList<Circle> circles, IList<Arc> arcs)
|
||||||
|
{
|
||||||
|
for (var i = circles.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
for (var j = arcs.Count - 1; j >= 0; j--)
|
||||||
|
{
|
||||||
|
if (arcs[j].Center.DistanceTo(circles[i].Center) <= Tolerance.Epsilon
|
||||||
|
&& arcs[j].Radius.IsEqualTo(circles[i].Radius)
|
||||||
|
&& arcs[j].IsFullCircle())
|
||||||
|
{
|
||||||
|
arcs.RemoveAt(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private delegate bool TryJoin<T>(T a, T b, out T joined);
|
private delegate bool TryJoin<T>(T a, T b, out T joined);
|
||||||
|
|
||||||
private static void MergePass<T>(IList<T> items,
|
private static void MergePass<T>(IList<T> items,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
public static class ShapeBuilder
|
public static class ShapeBuilder
|
||||||
{
|
{
|
||||||
public static List<Shape> GetShapes(IEnumerable<Entity> entities)
|
public static List<Shape> GetShapes(IEnumerable<Entity> entities, double? weldTolerance = null)
|
||||||
{
|
{
|
||||||
var lines = new List<Line>();
|
var lines = new List<Line>();
|
||||||
var arcs = new List<Arc>();
|
var arcs = new List<Arc>();
|
||||||
@@ -57,6 +58,9 @@ namespace OpenNest.Geometry
|
|||||||
entityList.AddRange(lines);
|
entityList.AddRange(lines);
|
||||||
entityList.AddRange(arcs);
|
entityList.AddRange(arcs);
|
||||||
|
|
||||||
|
if (weldTolerance.HasValue)
|
||||||
|
WeldEndpoints(entityList, weldTolerance.Value);
|
||||||
|
|
||||||
while (entityList.Count > 0)
|
while (entityList.Count > 0)
|
||||||
{
|
{
|
||||||
var next = entityList[0];
|
var next = entityList[0];
|
||||||
@@ -107,6 +111,93 @@ namespace OpenNest.Geometry
|
|||||||
return shapes;
|
return shapes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void WeldEndpoints(List<Entity> entities, double tolerance)
|
||||||
|
{
|
||||||
|
var endpointGroups = new List<List<(Entity entity, bool isStart, Vector point)>>();
|
||||||
|
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
var (start, end) = GetEndpoints(entity);
|
||||||
|
if (!start.IsValid() || !end.IsValid())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
AddToGroup(endpointGroups, entity, true, start, tolerance);
|
||||||
|
AddToGroup(endpointGroups, entity, false, end, tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in endpointGroups)
|
||||||
|
{
|
||||||
|
if (group.Count <= 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var avgX = group.Average(g => g.point.X);
|
||||||
|
var avgY = group.Average(g => g.point.Y);
|
||||||
|
var weldedPoint = new Vector(avgX, avgY);
|
||||||
|
|
||||||
|
foreach (var (entity, isStart, _) in group)
|
||||||
|
ApplyWeld(entity, isStart, weldedPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddToGroup(
|
||||||
|
List<List<(Entity entity, bool isStart, Vector point)>> groups,
|
||||||
|
Entity entity, bool isStart, Vector point, double tolerance)
|
||||||
|
{
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
if (group[0].point.DistanceTo(point) <= tolerance)
|
||||||
|
{
|
||||||
|
group.Add((entity, isStart, point));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.Add(new List<(Entity, bool, Vector)> { (entity, isStart, point) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Vector start, Vector end) GetEndpoints(Entity entity)
|
||||||
|
{
|
||||||
|
switch (entity.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
var arc = (Arc)entity;
|
||||||
|
return (arc.StartPoint(), arc.EndPoint());
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
var line = (Line)entity;
|
||||||
|
return (line.StartPoint, line.EndPoint);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (Vector.Invalid, Vector.Invalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyWeld(Entity entity, bool isStart, Vector weldedPoint)
|
||||||
|
{
|
||||||
|
switch (entity.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Line:
|
||||||
|
var line = (Line)entity;
|
||||||
|
if (isStart)
|
||||||
|
line.StartPoint = weldedPoint;
|
||||||
|
else
|
||||||
|
line.EndPoint = weldedPoint;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Arc:
|
||||||
|
var arc = (Arc)entity;
|
||||||
|
var deltaX = weldedPoint.X - arc.Center.X;
|
||||||
|
var deltaY = weldedPoint.Y - arc.Center.Y;
|
||||||
|
var angle = System.Math.Atan2(deltaY, deltaX);
|
||||||
|
|
||||||
|
if (isStart)
|
||||||
|
arc.StartAngle = angle;
|
||||||
|
else
|
||||||
|
arc.EndAngle = angle;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static Entity GetConnected(Vector pt, IEnumerable<Entity> geometry)
|
internal static Entity GetConnected(Vector pt, IEnumerable<Entity> geometry)
|
||||||
{
|
{
|
||||||
var tol = Tolerance.ChainTolerance;
|
var tol = Tolerance.ChainTolerance;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public interface IMaterialProvidingPostProcessor
|
||||||
|
{
|
||||||
|
IEnumerable<string> GetMaterialNames();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public interface IPostProcessorNestAware
|
||||||
|
{
|
||||||
|
void PrepareForNest(Nest nest);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace OpenNest.IO.Bom
|
namespace OpenNest.Math
|
||||||
{
|
{
|
||||||
public static class Fraction
|
public static class Fraction
|
||||||
{
|
{
|
||||||
@@ -7,6 +7,8 @@ namespace OpenNest.Shapes
|
|||||||
{
|
{
|
||||||
public double Diameter { get; set; }
|
public double Diameter { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName() => $"Circle {Dim(Diameter)} Dia";
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
Diameter = 8;
|
Diameter = 8;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ namespace OpenNest.Shapes
|
|||||||
public double Base { get; set; }
|
public double Base { get; set; }
|
||||||
public double Height { get; set; }
|
public double Height { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName() => $"Isosceles Triangle {Dim(Base)}x{Dim(Height)}";
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
Base = 8;
|
Base = 8;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ namespace OpenNest.Shapes
|
|||||||
public double LegWidth { get; set; }
|
public double LegWidth { get; set; }
|
||||||
public double LegHeight { get; set; }
|
public double LegHeight { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName() => $"L {Dim(Width)}x{Dim(Height)}";
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
Width = 8;
|
Width = 8;
|
||||||
|
|||||||
@@ -3,33 +3,40 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace OpenNest.Shapes
|
namespace OpenNest.Shapes
|
||||||
{
|
{
|
||||||
public class OctagonShape : ShapeDefinition
|
public class NgonShape : ShapeDefinition
|
||||||
{
|
{
|
||||||
|
public int Sides { get; set; }
|
||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName() => $"{Sides}-Sided Polygon {Dim(Width)}";
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
|
Sides = 8;
|
||||||
Width = 8;
|
Width = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
|
var n = Sides < 3 ? 3 : Sides;
|
||||||
var center = Width / 2.0;
|
var center = Width / 2.0;
|
||||||
var circumRadius = Width / (2.0 * System.Math.Cos(System.Math.PI / 8.0));
|
var circumRadius = Width / (2.0 * System.Math.Cos(System.Math.PI / n));
|
||||||
|
var step = 2.0 * System.Math.PI / n;
|
||||||
|
var start = System.Math.PI / n;
|
||||||
|
|
||||||
var vertices = new Vector[8];
|
var vertices = new Vector[n];
|
||||||
for (var i = 0; i < 8; i++)
|
for (var i = 0; i < n; i++)
|
||||||
{
|
{
|
||||||
var angle = System.Math.PI / 8.0 + i * System.Math.PI / 4.0;
|
var angle = start + i * step;
|
||||||
vertices[i] = new Vector(
|
vertices[i] = new Vector(
|
||||||
center + circumRadius * System.Math.Cos(angle),
|
center + circumRadius * System.Math.Cos(angle),
|
||||||
center + circumRadius * System.Math.Sin(angle));
|
center + circumRadius * System.Math.Sin(angle));
|
||||||
}
|
}
|
||||||
|
|
||||||
var entities = new List<Entity>();
|
var entities = new List<Entity>();
|
||||||
for (var i = 0; i < 8; i++)
|
for (var i = 0; i < n; i++)
|
||||||
{
|
{
|
||||||
var next = (i + 1) % 8;
|
var next = (i + 1) % n;
|
||||||
entities.Add(new Line(vertices[i], vertices[next]));
|
entities.Add(new Line(vertices[i], vertices[next]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,6 +13,14 @@ namespace OpenNest.Shapes
|
|||||||
public double PipeClearance { get; set; }
|
public double PipeClearance { get; set; }
|
||||||
public bool Blind { get; set; }
|
public bool Blind { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName()
|
||||||
|
{
|
||||||
|
var name = $"Pipe Flange {Dim(OD)} OD";
|
||||||
|
if (!string.IsNullOrEmpty(PipeSize))
|
||||||
|
name += $" {PipeSize} Pipe";
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
OD = 7.5;
|
OD = 7.5;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ namespace OpenNest.Shapes
|
|||||||
public double Length { get; set; }
|
public double Length { get; set; }
|
||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName() => $"Rectangle {Dim(Length)}x{Dim(Width)}";
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
Length = 12;
|
Length = 12;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ namespace OpenNest.Shapes
|
|||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
public double Height { get; set; }
|
public double Height { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName() => $"Right Triangle {Dim(Width)}x{Dim(Height)}";
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
Width = 8;
|
Width = 8;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ namespace OpenNest.Shapes
|
|||||||
public double OuterDiameter { get; set; }
|
public double OuterDiameter { get; set; }
|
||||||
public double InnerDiameter { get; set; }
|
public double InnerDiameter { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName() => $"Ring {Dim(OuterDiameter)}x{Dim(InnerDiameter)}";
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
OuterDiameter = 10;
|
OuterDiameter = 10;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ namespace OpenNest.Shapes
|
|||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
public double Radius { get; set; }
|
public double Radius { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName() => $"Rounded Rectangle {Dim(Length)}x{Dim(Width)} R{Dim(Radius)}";
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
Length = 12;
|
Length = 12;
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ namespace OpenNest.Shapes
|
|||||||
|
|
||||||
public abstract Drawing GetDrawing();
|
public abstract Drawing GetDrawing();
|
||||||
|
|
||||||
|
public virtual string GenerateName()
|
||||||
|
{
|
||||||
|
var typeName = GetType().Name;
|
||||||
|
return typeName.EndsWith("Shape")
|
||||||
|
? typeName.Substring(0, typeName.Length - 5)
|
||||||
|
: typeName;
|
||||||
|
}
|
||||||
|
|
||||||
public virtual void SetPreviewDefaults() { }
|
public virtual void SetPreviewDefaults() { }
|
||||||
|
|
||||||
public static List<T> LoadFromJson<T>(string path) where T : ShapeDefinition
|
public static List<T> LoadFromJson<T>(string path) where T : ShapeDefinition
|
||||||
@@ -34,6 +42,8 @@ namespace OpenNest.Shapes
|
|||||||
return JsonSerializer.Deserialize<List<T>>(json, JsonOptions);
|
return JsonSerializer.Deserialize<List<T>>(json, JsonOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static string Dim(double value) => value.ToString("0.###");
|
||||||
|
|
||||||
protected Drawing CreateDrawing(List<Entity> entities)
|
protected Drawing CreateDrawing(List<Entity> entities)
|
||||||
{
|
{
|
||||||
var pgm = ConvertGeometry.ToProgram(entities);
|
var pgm = ConvertGeometry.ToProgram(entities);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ namespace OpenNest.Shapes
|
|||||||
public double StemWidth { get; set; }
|
public double StemWidth { get; set; }
|
||||||
public double BarHeight { get; set; }
|
public double BarHeight { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName() => $"T {Dim(Width)}x{Dim(Height)}";
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
Width = 10;
|
Width = 10;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace OpenNest.Shapes
|
|||||||
public double BottomWidth { get; set; }
|
public double BottomWidth { get; set; }
|
||||||
public double Height { get; set; }
|
public double Height { get; set; }
|
||||||
|
|
||||||
|
public override string GenerateName() => $"Trapezoid {Dim(TopWidth)}x{Dim(BottomWidth)}x{Dim(Height)}";
|
||||||
|
|
||||||
public override void SetPreviewDefaults()
|
public override void SetPreviewDefaults()
|
||||||
{
|
{
|
||||||
TopWidth = 6;
|
TopWidth = 6;
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ namespace OpenNest.Engine.BestFit
|
|||||||
if (_cache.TryGetValue(key, out var cached))
|
if (_cache.TryGetValue(key, out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
|
// Operate on the canonical frame so cached pair positions are orientation-invariant.
|
||||||
|
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
|
||||||
|
|
||||||
IPairEvaluator evaluator = null;
|
IPairEvaluator evaluator = null;
|
||||||
ISlideComputer slideComputer = null;
|
ISlideComputer slideComputer = null;
|
||||||
|
|
||||||
@@ -31,7 +34,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
{
|
{
|
||||||
if (CreateEvaluator != null)
|
if (CreateEvaluator != null)
|
||||||
{
|
{
|
||||||
try { evaluator = CreateEvaluator(drawing, spacing); }
|
try { evaluator = CreateEvaluator(canonical, spacing); }
|
||||||
catch { /* fall back to default evaluator */ }
|
catch { /* fall back to default evaluator */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +45,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
}
|
}
|
||||||
|
|
||||||
var finder = new BestFitFinder(plateWidth, plateHeight, evaluator, slideComputer);
|
var finder = new BestFitFinder(plateWidth, plateHeight, evaluator, slideComputer);
|
||||||
var results = finder.FindBestFits(drawing, spacing, StepSize);
|
var results = finder.FindBestFits(canonical, spacing, StepSize);
|
||||||
|
|
||||||
_cache.TryAdd(key, results);
|
_cache.TryAdd(key, results);
|
||||||
return results;
|
return results;
|
||||||
@@ -86,9 +89,12 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Operate on the canonical frame so cached pair positions are orientation-invariant.
|
||||||
|
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
|
||||||
|
|
||||||
if (CreateEvaluator != null)
|
if (CreateEvaluator != null)
|
||||||
{
|
{
|
||||||
try { evaluator = CreateEvaluator(drawing, spacing); }
|
try { evaluator = CreateEvaluator(canonical, spacing); }
|
||||||
catch { /* fall back to default evaluator */ }
|
catch { /* fall back to default evaluator */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +106,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
// Compute candidates and evaluate once with the largest plate.
|
// Compute candidates and evaluate once with the largest plate.
|
||||||
var finder = new BestFitFinder(maxWidth, maxHeight, evaluator, slideComputer);
|
var finder = new BestFitFinder(maxWidth, maxHeight, evaluator, slideComputer);
|
||||||
var baseResults = finder.FindBestFits(drawing, spacing, StepSize);
|
var baseResults = finder.FindBestFits(canonical, spacing, StepSize);
|
||||||
|
|
||||||
// Cache a filtered copy for each plate size.
|
// Cache a filtered copy for each plate size.
|
||||||
foreach (var size in needed)
|
foreach (var size in needed)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenNest.Engine.BestFit
|
namespace OpenNest.Engine.BestFit
|
||||||
{
|
{
|
||||||
@@ -54,6 +57,68 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
return new List<Part> { part1, part2 };
|
return new List<Part> { part1, part2 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Part> BuildCanonicalParts()
|
||||||
|
{
|
||||||
|
return NormalizeToCutOrigin(BuildParts(Candidate.Drawing));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Part> BuildSourceParts(Drawing drawing)
|
||||||
|
{
|
||||||
|
var parts = BuildCanonicalParts();
|
||||||
|
var sourceAngle = drawing?.Source?.Angle ?? 0.0;
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var p = parts[i];
|
||||||
|
var rebound = Part.CreateAtOrigin(drawing, p.Rotation);
|
||||||
|
var delta = p.BoundingBox.Location - rebound.BoundingBox.Location;
|
||||||
|
rebound.Offset(delta);
|
||||||
|
rebound.UpdateBounds();
|
||||||
|
parts[i] = rebound;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NormalizeToCutOrigin(CanonicalFrame.FromCanonical(parts, sourceAngle));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Box GetCutBounds(List<Part> parts)
|
||||||
|
{
|
||||||
|
return GetCutBoundingBox(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Part> NormalizeToCutOrigin(List<Part> parts)
|
||||||
|
{
|
||||||
|
if (parts == null || parts.Count == 0)
|
||||||
|
return parts;
|
||||||
|
|
||||||
|
var bounds = GetCutBoundingBox(parts);
|
||||||
|
var offset = new Vector(-bounds.Left, -bounds.Bottom);
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
part.Offset(offset);
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Box GetCutBoundingBox(List<Part> parts)
|
||||||
|
{
|
||||||
|
var entities = new List<IBoundable>();
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
var partEntities = ConvertProgram.ToGeometry(part.Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var entity in partEntities)
|
||||||
|
{
|
||||||
|
entity.Offset(part.Location);
|
||||||
|
entities.Add(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities.GetBoundingBox();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum BestFitSortField
|
public enum BestFitSortField
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace OpenNest.Engine.BestFit
|
namespace OpenNest.Engine.BestFit
|
||||||
{
|
{
|
||||||
public class NfpSlideStrategy : IBestFitStrategy
|
public class NfpSlideStrategy : IBestFitStrategy
|
||||||
{
|
{
|
||||||
private static readonly string LogPath = Path.Combine(
|
|
||||||
System.Environment.GetFolderPath(System.Environment.SpecialFolder.Desktop),
|
|
||||||
"nfp-slide-debug.log");
|
|
||||||
|
|
||||||
private static readonly object LogLock = new object();
|
|
||||||
|
|
||||||
private readonly double _part2Rotation;
|
private readonly double _part2Rotation;
|
||||||
private readonly Polygon _stationaryPerimeter;
|
private readonly Polygon _stationaryPerimeter;
|
||||||
private readonly Polygon _stationaryHull;
|
private readonly Polygon _stationaryHull;
|
||||||
@@ -46,12 +38,6 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
var hull = ConvexHull.Compute(result.Polygon.Vertices);
|
var hull = ConvexHull.Compute(result.Polygon.Vertices);
|
||||||
|
|
||||||
Log($"=== Create: drawing={drawing.Name}, rotation={Angle.ToDegrees(part2Rotation):F1}deg ===");
|
|
||||||
Log($" Perimeter: {result.Polygon.Vertices.Count} verts, bounds={FormatBounds(result.Polygon)}");
|
|
||||||
Log($" Hull: {hull.Vertices.Count} verts, bounds={FormatBounds(hull)}");
|
|
||||||
Log($" Correction: ({result.Correction.X:F4}, {result.Correction.Y:F4})");
|
|
||||||
Log($" ProgramBBox: {drawing.Program.BoundingBox()}");
|
|
||||||
|
|
||||||
return new NfpSlideStrategy(part2Rotation, type, description,
|
return new NfpSlideStrategy(part2Rotation, type, description,
|
||||||
result.Polygon, hull, result.Correction);
|
result.Polygon, hull, result.Correction);
|
||||||
}
|
}
|
||||||
@@ -63,40 +49,17 @@ namespace OpenNest.Engine.BestFit
|
|||||||
if (stepSize <= 0)
|
if (stepSize <= 0)
|
||||||
return candidates;
|
return candidates;
|
||||||
|
|
||||||
Log($"--- GenerateCandidates: drawing={drawing.Name}, part2Rot={Angle.ToDegrees(_part2Rotation):F1}deg, spacing={spacing}, stepSize={stepSize} ---");
|
|
||||||
|
|
||||||
// Orbiting polygon: same shape rotated to Part2's angle.
|
|
||||||
var orbitingPerimeter = PolygonHelper.RotatePolygon(_stationaryPerimeter, _part2Rotation, reNormalize: true);
|
var orbitingPerimeter = PolygonHelper.RotatePolygon(_stationaryPerimeter, _part2Rotation, reNormalize: true);
|
||||||
var orbitingPoly = ConvexHull.Compute(orbitingPerimeter.Vertices);
|
var orbitingPoly = ConvexHull.Compute(orbitingPerimeter.Vertices);
|
||||||
|
|
||||||
Log($" Stationary hull: {_stationaryHull.Vertices.Count} verts, bounds={FormatBounds(_stationaryHull)}");
|
|
||||||
Log($" Orbiting perimeter (rotated): {orbitingPerimeter.Vertices.Count} verts, bounds={FormatBounds(orbitingPerimeter)}");
|
|
||||||
Log($" Orbiting hull: {orbitingPoly.Vertices.Count} verts, bounds={FormatBounds(orbitingPoly)}");
|
|
||||||
|
|
||||||
var nfp = NoFitPolygon.ComputeConvex(_stationaryHull, orbitingPoly);
|
var nfp = NoFitPolygon.ComputeConvex(_stationaryHull, orbitingPoly);
|
||||||
|
|
||||||
if (nfp == null || nfp.Vertices.Count < 3)
|
if (nfp == null || nfp.Vertices.Count < 3)
|
||||||
{
|
|
||||||
Log($" NFP failed or degenerate (verts={nfp?.Vertices.Count ?? 0})");
|
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
|
||||||
|
|
||||||
var verts = nfp.Vertices;
|
var verts = nfp.Vertices;
|
||||||
var vertCount = nfp.IsClosed() ? verts.Count - 1 : verts.Count;
|
var vertCount = nfp.IsClosed() ? verts.Count - 1 : verts.Count;
|
||||||
|
|
||||||
Log($" NFP: {verts.Count} verts (closed={nfp.IsClosed()}, walking {vertCount}), bounds={FormatBounds(nfp)}");
|
|
||||||
Log($" Correction: ({_correction.X:F4}, {_correction.Y:F4})");
|
|
||||||
|
|
||||||
// Log NFP vertices
|
|
||||||
for (var v = 0; v < vertCount; v++)
|
|
||||||
Log($" NFP vert[{v}]: ({verts[v].X:F4}, {verts[v].Y:F4}) -> corrected: ({verts[v].X - _correction.X:F4}, {verts[v].Y - _correction.Y:F4})");
|
|
||||||
|
|
||||||
// Compare with what RotationSlideStrategy would produce
|
|
||||||
var part1 = Part.CreateAtOrigin(drawing);
|
|
||||||
var part2 = Part.CreateAtOrigin(drawing, _part2Rotation);
|
|
||||||
Log($" Part1 (rot=0): loc=({part1.Location.X:F4}, {part1.Location.Y:F4}), bbox={part1.BoundingBox}");
|
|
||||||
Log($" Part2 (rot={Angle.ToDegrees(_part2Rotation):F1}): loc=({part2.Location.X:F4}, {part2.Location.Y:F4}), bbox={part2.BoundingBox}");
|
|
||||||
|
|
||||||
var testNumber = 0;
|
var testNumber = 0;
|
||||||
|
|
||||||
for (var i = 0; i < vertCount; i++)
|
for (var i = 0; i < vertCount; i++)
|
||||||
@@ -125,20 +88,6 @@ namespace OpenNest.Engine.BestFit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log overlap check for vertex candidates (first few)
|
|
||||||
var checkCount = System.Math.Min(vertCount, 8);
|
|
||||||
for (var c = 0; c < checkCount; c++)
|
|
||||||
{
|
|
||||||
var cand = candidates[c];
|
|
||||||
var p2 = Part.CreateAtOrigin(drawing, cand.Part2Rotation);
|
|
||||||
p2.Location = cand.Part2Offset;
|
|
||||||
var overlaps = part1.Intersects(p2, out _);
|
|
||||||
Log($" Candidate[{c}]: offset=({cand.Part2Offset.X:F4}, {cand.Part2Offset.Y:F4}), overlaps={overlaps}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Log($" Total candidates: {candidates.Count}");
|
|
||||||
Log("");
|
|
||||||
|
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,20 +109,5 @@ namespace OpenNest.Engine.BestFit
|
|||||||
Spacing = spacing
|
Spacing = spacing
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatBounds(Polygon polygon)
|
|
||||||
{
|
|
||||||
polygon.UpdateBounds();
|
|
||||||
var bb = polygon.BoundingBox;
|
|
||||||
return $"[({bb.Left:F4}, {bb.Bottom:F4})-({bb.Right:F4}, {bb.Top:F4}), {bb.Width:F2}x{bb.Length:F2}]";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Log(string message)
|
|
||||||
{
|
|
||||||
lock (LogLock)
|
|
||||||
{
|
|
||||||
File.AppendAllText(LogPath, message + "\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Produces transient canonical (MBR-axis-aligned) copies of drawings for engine consumption
|
||||||
|
/// and un-rotates placed parts back to the drawing's original frame.
|
||||||
|
/// </summary>
|
||||||
|
public static class CanonicalFrame
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a new Drawing whose Program geometry is rotated to the canonical frame.
|
||||||
|
/// The source drawing is not mutated.
|
||||||
|
/// </summary>
|
||||||
|
public static Drawing AsCanonicalCopy(Drawing drawing)
|
||||||
|
{
|
||||||
|
if (drawing == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var angle = drawing.Source?.Angle ?? 0.0;
|
||||||
|
|
||||||
|
// Clone program (never mutate the source).
|
||||||
|
var pgm = (drawing.Program.Clone() as OpenNest.CNC.Program)
|
||||||
|
?? new OpenNest.CNC.Program();
|
||||||
|
|
||||||
|
if (!Tolerance.IsEqualTo(angle, 0))
|
||||||
|
pgm.Rotate(angle, pgm.BoundingBox().Center);
|
||||||
|
|
||||||
|
var copy = new Drawing(drawing.Name ?? string.Empty, pgm)
|
||||||
|
{
|
||||||
|
Color = drawing.Color,
|
||||||
|
Constraints = drawing.Constraints,
|
||||||
|
Material = drawing.Material,
|
||||||
|
Priority = drawing.Priority,
|
||||||
|
Customer = drawing.Customer,
|
||||||
|
IsCutOff = drawing.IsCutOff,
|
||||||
|
Source = new SourceInfo
|
||||||
|
{
|
||||||
|
Path = drawing.Source?.Path,
|
||||||
|
Offset = drawing.Source?.Offset ?? new Vector(0, 0),
|
||||||
|
Angle = 0.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Composes the source drawing's canonical angle onto each placed part so the
|
||||||
|
/// returned list is in the drawing's original (visible) frame.
|
||||||
|
///
|
||||||
|
/// Derivation: let sourceAngle = S (rotation mapping source -> canonical).
|
||||||
|
/// Canonical part at rotation R shows visible orientation R.
|
||||||
|
/// Source part at rotation R' shows visible orientation R' + (-S), because the
|
||||||
|
/// source geometry is already rotated by -S relative to canonical.
|
||||||
|
/// Setting equal gives R' = R + S, so we ADD sourceAngle to each placed part.
|
||||||
|
///
|
||||||
|
/// Rotation is performed around the part's Location so its placement position is preserved;
|
||||||
|
/// only the orientation composes.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Part> FromCanonical(List<Part> placed, double sourceAngle)
|
||||||
|
{
|
||||||
|
if (placed == null || placed.Count == 0)
|
||||||
|
return placed;
|
||||||
|
if (Tolerance.IsEqualTo(sourceAngle, 0))
|
||||||
|
return placed;
|
||||||
|
|
||||||
|
foreach (var p in placed)
|
||||||
|
p.Rotate(sourceAngle, p.Location);
|
||||||
|
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,14 +47,29 @@ namespace OpenNest
|
|||||||
PhaseResults.Clear();
|
PhaseResults.Clear();
|
||||||
AngleResults.Clear();
|
AngleResults.Clear();
|
||||||
|
|
||||||
// Fast path: for very small quantities, skip the full strategy pipeline.
|
// Replace the item's Drawing with a canonical copy for the duration of this fill.
|
||||||
if (item.Quantity > 0 && item.Quantity <= 2)
|
// All internal methods see canonical geometry; this wrapper un-canonicalizes the final result.
|
||||||
|
var sourceAngle = item.Drawing?.Source?.Angle ?? 0.0;
|
||||||
|
var originalDrawing = item.Drawing;
|
||||||
|
var canonicalItem = new NestItem
|
||||||
{
|
{
|
||||||
var fast = TryFillSmallQuantity(item, workArea);
|
Drawing = CanonicalFrame.AsCanonicalCopy(item.Drawing),
|
||||||
if (fast != null && fast.Count >= item.Quantity)
|
Quantity = item.Quantity,
|
||||||
|
Priority = item.Priority,
|
||||||
|
RotationStart = item.RotationStart,
|
||||||
|
RotationEnd = item.RotationEnd,
|
||||||
|
StepAngle = item.StepAngle,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fast path for qty 1-2.
|
||||||
|
if (canonicalItem.Quantity > 0 && canonicalItem.Quantity <= 2)
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"[Fill] Fast path: placed {fast.Count} parts for qty={item.Quantity}");
|
var fast = TryFillSmallQuantity(canonicalItem, workArea);
|
||||||
|
if (fast != null && fast.Count >= canonicalItem.Quantity)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[Fill] Fast path: placed {fast.Count} parts for qty={canonicalItem.Quantity}");
|
||||||
WinnerPhase = NestPhase.Pairs;
|
WinnerPhase = NestPhase.Pairs;
|
||||||
|
fast = RebindAndUnCanonicalize(fast, originalDrawing, sourceAngle);
|
||||||
ReportProgress(progress, new ProgressReport
|
ReportProgress(progress, new ProgressReport
|
||||||
{
|
{
|
||||||
Phase = WinnerPhase,
|
Phase = WinnerPhase,
|
||||||
@@ -68,32 +83,30 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For low quantities, shrink the work area in both dimensions to avoid
|
|
||||||
// running expensive strategies against the full plate.
|
|
||||||
var effectiveWorkArea = workArea;
|
var effectiveWorkArea = workArea;
|
||||||
if (item.Quantity > 0)
|
if (canonicalItem.Quantity > 0)
|
||||||
{
|
{
|
||||||
effectiveWorkArea = ShrinkWorkArea(item, workArea, Plate.PartSpacing);
|
effectiveWorkArea = ShrinkWorkArea(canonicalItem, workArea, Plate.PartSpacing);
|
||||||
|
|
||||||
if (effectiveWorkArea != workArea)
|
if (effectiveWorkArea != workArea)
|
||||||
Debug.WriteLine($"[Fill] Low-qty shrink: {item.Quantity} requested, " +
|
Debug.WriteLine($"[Fill] Low-qty shrink: {canonicalItem.Quantity} requested, " +
|
||||||
$"from {workArea.Width:F1}x{workArea.Length:F1} " +
|
$"from {workArea.Width:F1}x{workArea.Length:F1} " +
|
||||||
$"to {effectiveWorkArea.Width:F1}x{effectiveWorkArea.Length:F1}");
|
$"to {effectiveWorkArea.Width:F1}x{effectiveWorkArea.Length:F1}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var best = RunFillPipeline(item, effectiveWorkArea, progress, token);
|
var best = RunFillPipeline(canonicalItem, effectiveWorkArea, progress, token);
|
||||||
|
|
||||||
// Fallback: if the reduced area didn't yield enough, retry with full area.
|
if (canonicalItem.Quantity > 0 && best.Count < canonicalItem.Quantity && effectiveWorkArea != workArea)
|
||||||
if (item.Quantity > 0 && best.Count < item.Quantity && effectiveWorkArea != workArea)
|
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"[Fill] Low-qty fallback: got {best.Count}, need {item.Quantity}, retrying full area");
|
Debug.WriteLine($"[Fill] Low-qty fallback: got {best.Count}, need {canonicalItem.Quantity}, retrying full area");
|
||||||
PhaseResults.Clear();
|
PhaseResults.Clear();
|
||||||
AngleResults.Clear();
|
AngleResults.Clear();
|
||||||
best = RunFillPipeline(item, workArea, progress, token);
|
best = RunFillPipeline(canonicalItem, workArea, progress, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Quantity > 0 && best.Count > item.Quantity)
|
if (canonicalItem.Quantity > 0 && best.Count > canonicalItem.Quantity)
|
||||||
best = ShrinkFiller.TrimToCount(best, item.Quantity, TrimAxis);
|
best = ShrinkFiller.TrimToCount(best, canonicalItem.Quantity, TrimAxis);
|
||||||
|
|
||||||
|
best = RebindAndUnCanonicalize(best, originalDrawing, sourceAngle);
|
||||||
|
|
||||||
ReportProgress(progress, new ProgressReport
|
ReportProgress(progress, new ProgressReport
|
||||||
{
|
{
|
||||||
@@ -108,6 +121,31 @@ namespace OpenNest
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Single exit point for canonical -> source frame conversion. Rebinds every Part to the
|
||||||
|
/// original Drawing (so consumers see the user's drawing identity, not the transient canonical copy)
|
||||||
|
/// and composes sourceAngle onto each Part's rotation via CanonicalFrame.FromCanonical.
|
||||||
|
/// </summary>
|
||||||
|
private static List<Part> RebindAndUnCanonicalize(List<Part> parts, Drawing original, double sourceAngle)
|
||||||
|
{
|
||||||
|
if (parts == null || parts.Count == 0)
|
||||||
|
return parts;
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var p = parts[i];
|
||||||
|
// Rebind to `original` while preserving world pose. CreateAtOrigin rotates
|
||||||
|
// at the origin (keeping bbox at world (0,0)) then we offset to match p's bbox.
|
||||||
|
var rebound = Part.CreateAtOrigin(original, p.Rotation);
|
||||||
|
var delta = p.BoundingBox.Location - rebound.BoundingBox.Location;
|
||||||
|
rebound.Offset(delta);
|
||||||
|
rebound.UpdateBounds();
|
||||||
|
parts[i] = rebound;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CanonicalFrame.FromCanonical(parts, sourceAngle);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fast path for qty 1-2: place a single part or a best-fit pair
|
/// Fast path for qty 1-2: place a single part or a best-fit pair
|
||||||
/// without running the full strategy pipeline.
|
/// without running the full strategy pipeline.
|
||||||
@@ -139,6 +177,10 @@ namespace OpenNest
|
|||||||
var bestFits = BestFitCache.GetOrCompute(
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||||
|
|
||||||
|
// Build pair candidates with a canonical drawing so their geometry matches
|
||||||
|
// the coordinate frame of the cached fit results.
|
||||||
|
var canonicalDrawing = CanonicalFrame.AsCanonicalCopy(drawing);
|
||||||
|
|
||||||
List<Part> bestPlacement = null;
|
List<Part> bestPlacement = null;
|
||||||
|
|
||||||
foreach (var fit in bestFits)
|
foreach (var fit in bestFits)
|
||||||
@@ -152,7 +194,7 @@ namespace OpenNest
|
|||||||
if (fit.LongestSide > System.Math.Max(workArea.Width, workArea.Length) + Tolerance.Epsilon)
|
if (fit.LongestSide > System.Math.Max(workArea.Width, workArea.Length) + Tolerance.Epsilon)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var landscape = fit.BuildParts(drawing);
|
var landscape = fit.BuildParts(canonicalDrawing);
|
||||||
var portrait = RotatePair90(landscape);
|
var portrait = RotatePair90(landscape);
|
||||||
|
|
||||||
var lFits = TryOffsetToWorkArea(landscape, workArea);
|
var lFits = TryOffsetToWorkArea(landscape, workArea);
|
||||||
@@ -174,6 +216,8 @@ namespace OpenNest
|
|||||||
bestPlacement = candidate;
|
bestPlacement = candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parts are returned in canonical frame, bound to the canonical drawing.
|
||||||
|
// The outer Fill wrapper (Task 7) rebinds to `drawing` and composes sourceAngle onto rotation.
|
||||||
return bestPlacement;
|
return bestPlacement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Fill
|
namespace OpenNest.Engine.Fill
|
||||||
{
|
{
|
||||||
@@ -14,7 +15,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||||
{
|
{
|
||||||
var obstacleParts = plate.Parts
|
var obstacleParts = plate.Parts
|
||||||
.Where(p => !movingParts.Contains(p))
|
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||||
@@ -26,7 +27,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
public static double Push(List<Part> movingParts, Plate plate, double angle)
|
public static double Push(List<Part> movingParts, Plate plate, double angle)
|
||||||
{
|
{
|
||||||
var obstacleParts = plate.Parts
|
var obstacleParts = plate.Parts
|
||||||
.Where(p => !movingParts.Contains(p))
|
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
||||||
@@ -99,6 +100,13 @@ namespace OpenNest.Engine.Fill
|
|||||||
: PartGeometry.GetPerimeterEntities(obstacleParts[i]);
|
: PartGeometry.GetPerimeterEntities(obstacleParts[i]);
|
||||||
|
|
||||||
var d = SpatialQuery.DirectionalDistance(movingEntities, obstacleEntities[i], direction);
|
var d = SpatialQuery.DirectionalDistance(movingEntities, obstacleEntities[i], direction);
|
||||||
|
if (d <= Tolerance.Epsilon
|
||||||
|
&& partSpacing <= Tolerance.Epsilon
|
||||||
|
&& CanNudgeWithoutOverlap(moving, obstacleParts[i], direction))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (d < distance)
|
if (d < distance)
|
||||||
distance = d;
|
distance = d;
|
||||||
}
|
}
|
||||||
@@ -115,6 +123,31 @@ namespace OpenNest.Engine.Fill
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IntersectsAny(Part candidate, List<Part> parts)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
if (candidate.Intersects(parts[i], out _))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanNudgeWithoutOverlap(Part moving, Part obstacle, Vector direction)
|
||||||
|
{
|
||||||
|
var nudge = direction * (Tolerance.Epsilon * 10);
|
||||||
|
|
||||||
|
moving.Offset(nudge);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return !moving.Intersects(obstacle, out _);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
moving.Offset(-nudge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
|
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
|
||||||
Box workArea, double partSpacing, PushDirection direction)
|
Box workArea, double partSpacing, PushDirection direction)
|
||||||
{
|
{
|
||||||
@@ -130,7 +163,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
public static double PushBoundingBox(List<Part> movingParts, Plate plate, PushDirection direction)
|
public static double PushBoundingBox(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||||
{
|
{
|
||||||
var obstacleParts = plate.Parts
|
var obstacleParts = plate.Parts
|
||||||
.Where(p => !movingParts.Contains(p))
|
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||||
|
|||||||
@@ -61,92 +61,91 @@ namespace OpenNest.Engine.Fill
|
|||||||
: NestDirection.Horizontal;
|
: NestDirection.Horizontal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Computes the slide distance for the push algorithm, returning the
|
|
||||||
/// geometry-aware copy distance along the given axis.
|
|
||||||
/// </summary>
|
|
||||||
private double ComputeCopyDistance(double bboxDim, double slideDistance)
|
|
||||||
{
|
|
||||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
|
||||||
return bboxDim + PartSpacing;
|
|
||||||
|
|
||||||
// The geometry-aware slide can produce a copy distance smaller than
|
|
||||||
// the part itself when inflated corner/arc vertices interact spuriously.
|
|
||||||
// Clamp to bboxDim + PartSpacing to prevent bounding box overlap.
|
|
||||||
return System.Math.Max(bboxDim - slideDistance, bboxDim + PartSpacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds the geometry-aware copy distance between two identical parts along an axis.
|
/// Finds the geometry-aware copy distance between two identical parts along an axis.
|
||||||
/// Both parts are inflated by half-spacing for symmetric spacing.
|
/// Uses native Line/Arc entities (inflated by half-spacing) so curves are handled
|
||||||
|
/// exactly without polygon sampling error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private double FindCopyDistance(Part partA, NestDirection direction, PartBoundary boundary)
|
private double FindCopyDistance(Part partA, NestDirection direction)
|
||||||
{
|
{
|
||||||
var bboxDim = GetDimension(partA.BoundingBox, direction);
|
var bboxDim = GetDimension(partA.BoundingBox, direction);
|
||||||
var pushDir = GetPushDirection(direction);
|
var pushDir = GetPushDirection(direction);
|
||||||
|
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
|
||||||
|
var offset = MakeOffset(direction, startOffset);
|
||||||
|
|
||||||
var locationBOffset = MakeOffset(direction, bboxDim);
|
var stationaryEntities = PartGeometry.GetOffsetPerimeterEntities(partA, HalfSpacing);
|
||||||
|
var movingEntities = PartGeometry.GetOffsetPerimeterEntities(
|
||||||
|
partA.CloneAtOffset(offset), HalfSpacing);
|
||||||
|
|
||||||
// Use the most efficient array-based overload to avoid all allocations.
|
|
||||||
var slideDistance = SpatialQuery.DirectionalDistance(
|
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||||
boundary.GetEdges(pushDir), partA.Location + locationBOffset,
|
movingEntities, stationaryEntities, pushDir);
|
||||||
boundary.GetEdges(SpatialQuery.OppositeDirection(pushDir)), partA.Location,
|
|
||||||
pushDir);
|
|
||||||
|
|
||||||
return ComputeCopyDistance(bboxDim, slideDistance);
|
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||||
|
return bboxDim + PartSpacing;
|
||||||
|
|
||||||
|
return startOffset - slideDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds the geometry-aware copy distance between two identical patterns along an axis.
|
/// Finds the geometry-aware copy distance between two identical patterns along an axis.
|
||||||
/// Checks every pair of parts across adjacent patterns so that multi-part
|
/// Checks every pair of parts across adjacent pattern copies so multi-part patterns
|
||||||
/// patterns (e.g. interlocking pairs) maintain spacing between ALL parts.
|
/// (e.g. interlocking pairs) maintain spacing between ALL parts. Uses native entity
|
||||||
/// Both sides are inflated by half-spacing for symmetric spacing.
|
/// geometry inflated by half-spacing — same primitive the Compactor uses — so arcs
|
||||||
|
/// are exact and no bbox clamp is needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private double FindPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary[] boundaries)
|
private double FindPatternCopyDistance(Pattern patternA, NestDirection direction)
|
||||||
{
|
{
|
||||||
if (patternA.Parts.Count <= 1)
|
if (patternA.Parts.Count == 1)
|
||||||
return FindSinglePartPatternCopyDistance(patternA, direction, boundaries[0]);
|
return FindCopyDistance(patternA.Parts[0], direction);
|
||||||
|
|
||||||
var bboxDim = GetDimension(patternA.BoundingBox, direction);
|
var bboxDim = GetDimension(patternA.BoundingBox, direction);
|
||||||
var pushDir = GetPushDirection(direction);
|
var pushDir = GetPushDirection(direction);
|
||||||
var opposite = SpatialQuery.OppositeDirection(pushDir);
|
var opposite = SpatialQuery.OppositeDirection(pushDir);
|
||||||
|
var dirVec = SpatialQuery.DirectionToOffset(pushDir, 1.0);
|
||||||
|
|
||||||
// bboxDim already spans max(upper) - min(lower) across all parts,
|
// bboxDim already spans max(upper) - min(lower) across all parts,
|
||||||
// so the start offset just needs to push beyond that plus spacing.
|
// so the start offset just needs to push beyond that plus spacing.
|
||||||
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
|
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
|
||||||
var offset = MakeOffset(direction, startOffset);
|
var offset = MakeOffset(direction, startOffset);
|
||||||
|
|
||||||
var maxCopyDistance = FindMaxPairDistance(
|
var parts = patternA.Parts;
|
||||||
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
|
var stationaryBoxes = new Box[parts.Count];
|
||||||
|
var movingBoxes = new Box[parts.Count];
|
||||||
|
var stationaryEntities = new List<Entity>[parts.Count];
|
||||||
|
var movingEntities = new List<Entity>[parts.Count];
|
||||||
|
|
||||||
// The copy distance must be at least bboxDim + PartSpacing to prevent
|
for (var i = 0; i < parts.Count; i++)
|
||||||
// bounding box overlap. Cross-pair slides can underestimate when the
|
{
|
||||||
// circumscribed polygon boundary overshoots the true arc, creating
|
stationaryBoxes[i] = parts[i].BoundingBox;
|
||||||
// spurious contacts between diagonal parts in adjacent copies.
|
movingBoxes[i] = stationaryBoxes[i].Translate(offset);
|
||||||
return System.Math.Max(maxCopyDistance, bboxDim + PartSpacing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests every pair of parts across adjacent pattern copies and returns the
|
|
||||||
/// maximum copy distance found. Returns 0 if no valid slide was found.
|
|
||||||
/// </summary>
|
|
||||||
private static double FindMaxPairDistance(
|
|
||||||
List<Part> parts, PartBoundary[] boundaries, Vector offset,
|
|
||||||
PushDirection pushDir, PushDirection opposite, double startOffset)
|
|
||||||
{
|
|
||||||
var maxCopyDistance = 0.0;
|
var maxCopyDistance = 0.0;
|
||||||
|
|
||||||
for (var j = 0; j < parts.Count; j++)
|
for (var j = 0; j < parts.Count; j++)
|
||||||
{
|
{
|
||||||
var movingEdges = boundaries[j].GetEdges(pushDir);
|
var movingBox = movingBoxes[j];
|
||||||
var locationB = parts[j].Location + offset;
|
|
||||||
|
|
||||||
for (var i = 0; i < parts.Count; i++)
|
for (var i = 0; i < parts.Count; i++)
|
||||||
{
|
{
|
||||||
|
var stationaryBox = stationaryBoxes[i];
|
||||||
|
|
||||||
|
// Skip if stationary is already ahead of moving in the push direction
|
||||||
|
// (sliding forward would take them further apart).
|
||||||
|
if (SpatialQuery.DirectionalGap(movingBox, stationaryBox, opposite) > 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip if bboxes can't overlap along the axis perpendicular to the push.
|
||||||
|
if (!SpatialQuery.PerpendicularOverlap(movingBox, stationaryBox, dirVec))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
stationaryEntities[i] ??= PartGeometry.GetOffsetPerimeterEntities(
|
||||||
|
parts[i], HalfSpacing);
|
||||||
|
movingEntities[j] ??= PartGeometry.GetOffsetPerimeterEntities(
|
||||||
|
parts[j].CloneAtOffset(offset), HalfSpacing);
|
||||||
|
|
||||||
var slideDistance = SpatialQuery.DirectionalDistance(
|
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||||
movingEdges, locationB,
|
movingEntities[j], stationaryEntities[i], pushDir);
|
||||||
boundaries[i].GetEdges(opposite), parts[i].Location,
|
|
||||||
pushDir);
|
|
||||||
|
|
||||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||||
continue;
|
continue;
|
||||||
@@ -161,86 +160,15 @@ namespace OpenNest.Engine.Fill
|
|||||||
return maxCopyDistance;
|
return maxCopyDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fast path for single-part patterns — no cross-part conflicts possible.
|
|
||||||
/// </summary>
|
|
||||||
private double FindSinglePartPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary boundary)
|
|
||||||
{
|
|
||||||
var template = patternA.Parts[0];
|
|
||||||
return FindCopyDistance(template, direction, boundary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets offset boundary lines for all parts in a pattern using a shared boundary.
|
|
||||||
/// </summary>
|
|
||||||
private static List<Line> GetPatternLines(Pattern pattern, PartBoundary boundary, PushDirection direction)
|
|
||||||
{
|
|
||||||
var lines = new List<Line>();
|
|
||||||
|
|
||||||
foreach (var part in pattern.Parts)
|
|
||||||
lines.AddRange(boundary.GetLines(part.Location, direction));
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets boundary lines for all parts in a pattern, with an additional
|
|
||||||
/// location offset applied. Avoids cloning the pattern.
|
|
||||||
/// </summary>
|
|
||||||
private static List<Line> GetOffsetPatternLines(Pattern pattern, Vector offset, PartBoundary boundary, PushDirection direction)
|
|
||||||
{
|
|
||||||
var lines = new List<Line>();
|
|
||||||
|
|
||||||
foreach (var part in pattern.Parts)
|
|
||||||
lines.AddRange(boundary.GetLines(part.Location + offset, direction));
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates boundaries for all parts in a pattern. Parts that share the same
|
|
||||||
/// program geometry (same drawing and rotation) reuse the same boundary instance.
|
|
||||||
/// </summary>
|
|
||||||
private PartBoundary[] CreateBoundaries(Pattern pattern)
|
|
||||||
{
|
|
||||||
var boundaries = new PartBoundary[pattern.Parts.Count];
|
|
||||||
var cache = new List<(Drawing drawing, double rotation, PartBoundary boundary)>();
|
|
||||||
|
|
||||||
for (var i = 0; i < pattern.Parts.Count; i++)
|
|
||||||
{
|
|
||||||
var part = pattern.Parts[i];
|
|
||||||
PartBoundary found = null;
|
|
||||||
|
|
||||||
foreach (var entry in cache)
|
|
||||||
{
|
|
||||||
if (entry.drawing == part.BaseDrawing && entry.rotation.IsEqualTo(part.Rotation))
|
|
||||||
{
|
|
||||||
found = entry.boundary;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found == null)
|
|
||||||
{
|
|
||||||
found = new PartBoundary(part, HalfSpacing);
|
|
||||||
cache.Add((part.BaseDrawing, part.Rotation, found));
|
|
||||||
}
|
|
||||||
|
|
||||||
boundaries[i] = found;
|
|
||||||
}
|
|
||||||
|
|
||||||
return boundaries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tiles a pattern along the given axis, returning the cloned parts
|
/// Tiles a pattern along the given axis, returning the cloned parts
|
||||||
/// (does not include the original pattern's parts). For multi-part
|
/// (does not include the original pattern's parts). For multi-part
|
||||||
/// patterns, also adds individual parts from the next incomplete copy
|
/// patterns, also adds individual parts from the next incomplete copy
|
||||||
/// that still fit within the work area.
|
/// that still fit within the work area.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private List<Part> TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries)
|
private List<Part> TilePattern(Pattern basePattern, NestDirection direction)
|
||||||
{
|
{
|
||||||
var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries);
|
var copyDistance = FindPatternCopyDistance(basePattern, direction);
|
||||||
|
|
||||||
if (copyDistance <= 0)
|
if (copyDistance <= 0)
|
||||||
return new List<Part>();
|
return new List<Part>();
|
||||||
@@ -394,11 +322,10 @@ namespace OpenNest.Engine.Fill
|
|||||||
private List<Part> FillGrid(Pattern pattern, NestDirection direction)
|
private List<Part> FillGrid(Pattern pattern, NestDirection direction)
|
||||||
{
|
{
|
||||||
var perpAxis = PerpendicularAxis(direction);
|
var perpAxis = PerpendicularAxis(direction);
|
||||||
var boundaries = CreateBoundaries(pattern);
|
|
||||||
|
|
||||||
// Step 1: Tile along primary axis
|
// Step 1: Tile along primary axis
|
||||||
var row = new List<Part>(pattern.Parts);
|
var row = new List<Part>(pattern.Parts);
|
||||||
row.AddRange(TilePattern(pattern, direction, boundaries));
|
row.AddRange(TilePattern(pattern, direction));
|
||||||
|
|
||||||
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a1, out var b1))
|
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a1, out var b1))
|
||||||
{
|
{
|
||||||
@@ -410,7 +337,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
// If primary tiling didn't produce copies, just tile along perpendicular
|
// If primary tiling didn't produce copies, just tile along perpendicular
|
||||||
if (row.Count <= pattern.Parts.Count)
|
if (row.Count <= pattern.Parts.Count)
|
||||||
{
|
{
|
||||||
row.AddRange(TilePattern(pattern, perpAxis, boundaries));
|
row.AddRange(TilePattern(pattern, perpAxis));
|
||||||
|
|
||||||
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a2, out var b2))
|
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a2, out var b2))
|
||||||
{
|
{
|
||||||
@@ -427,9 +354,8 @@ namespace OpenNest.Engine.Fill
|
|||||||
rowPattern.Parts.AddRange(row);
|
rowPattern.Parts.AddRange(row);
|
||||||
rowPattern.UpdateBounds();
|
rowPattern.UpdateBounds();
|
||||||
|
|
||||||
var rowBoundaries = CreateBoundaries(rowPattern);
|
|
||||||
var gridResult = new List<Part>(rowPattern.Parts);
|
var gridResult = new List<Part>(rowPattern.Parts);
|
||||||
gridResult.AddRange(TilePattern(rowPattern, perpAxis, rowBoundaries));
|
gridResult.AddRange(TilePattern(rowPattern, perpAxis));
|
||||||
|
|
||||||
if (HasOverlappingParts(gridResult, out var a3, out var b3))
|
if (HasOverlappingParts(gridResult, out var a3, out var b3))
|
||||||
{
|
{
|
||||||
@@ -481,9 +407,8 @@ namespace OpenNest.Engine.Fill
|
|||||||
return seed;
|
return seed;
|
||||||
|
|
||||||
var template = seed.Parts[0];
|
var template = seed.Parts[0];
|
||||||
var boundary = new PartBoundary(template, HalfSpacing);
|
|
||||||
|
|
||||||
var copyDistance = FindCopyDistance(template, direction, boundary);
|
var copyDistance = FindCopyDistance(template, direction);
|
||||||
|
|
||||||
if (copyDistance <= 0)
|
if (copyDistance <= 0)
|
||||||
return seed;
|
return seed;
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ namespace OpenNest.Engine.ML
|
|||||||
{
|
{
|
||||||
public static PartFeatures Extract(Drawing drawing)
|
public static PartFeatures Extract(Drawing drawing)
|
||||||
{
|
{
|
||||||
var entities = OpenNest.Converters.ConvertProgram.ToGeometry(drawing.Program)
|
// Normalize to canonical frame so features are invariant to import orientation.
|
||||||
|
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
|
||||||
|
|
||||||
|
var entities = OpenNest.Converters.ConvertProgram.ToGeometry(canonical.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -45,18 +48,18 @@ namespace OpenNest.Engine.ML
|
|||||||
|
|
||||||
var features = new PartFeatures
|
var features = new PartFeatures
|
||||||
{
|
{
|
||||||
Area = drawing.Area,
|
Area = canonical.Area,
|
||||||
Convexity = drawing.Area / (hullArea > 0 ? hullArea : 1.0),
|
Convexity = canonical.Area / (hullArea > 0 ? hullArea : 1.0),
|
||||||
AspectRatio = bb.Length / (bb.Width > 0 ? bb.Width : 1.0),
|
AspectRatio = bb.Length / (bb.Width > 0 ? bb.Width : 1.0),
|
||||||
BoundingBoxFill = drawing.Area / (bb.Area() > 0 ? bb.Area() : 1.0),
|
BoundingBoxFill = canonical.Area / (bb.Area() > 0 ? bb.Area() : 1.0),
|
||||||
VertexCount = polygon.Vertices.Count,
|
VertexCount = polygon.Vertices.Count,
|
||||||
Bitmask = GenerateBitmask(polygon, 32)
|
Bitmask = GenerateBitmask(polygon, 32)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Circularity = 4 * PI * Area / Perimeter^2
|
// Circularity = 4 * PI * Area / Perimeter^2
|
||||||
var perimeterLen = polygon.Perimeter();
|
var perimeterLen = polygon.Perimeter();
|
||||||
features.Circularity = (4 * System.Math.PI * drawing.Area) / (perimeterLen * perimeterLen);
|
features.Circularity = (4 * System.Math.PI * canonical.Area) / (perimeterLen * perimeterLen);
|
||||||
features.PerimeterToAreaRatio = drawing.Area > 0 ? perimeterLen / drawing.Area : 0;
|
features.PerimeterToAreaRatio = canonical.Area > 0 ? perimeterLen / canonical.Area : 0;
|
||||||
|
|
||||||
return features;
|
return features;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,6 +334,12 @@ namespace OpenNest
|
|||||||
var bestFits = BestFitCache.GetOrCompute(
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||||
|
|
||||||
|
// BestFitCache stores pair coordinates in canonical frame. Build candidates
|
||||||
|
// from a canonical drawing copy so geometry and coords share a frame; rebind
|
||||||
|
// + un-rotate winning pair to the original drawing's frame before returning.
|
||||||
|
var canonicalDrawing = CanonicalFrame.AsCanonicalCopy(item.Drawing);
|
||||||
|
var sourceAngle = item.Drawing?.Source?.Angle ?? 0.0;
|
||||||
|
|
||||||
List<Part> bestPlacement = null;
|
List<Part> bestPlacement = null;
|
||||||
Box bestTarget = null;
|
Box bestTarget = null;
|
||||||
|
|
||||||
@@ -342,7 +348,7 @@ namespace OpenNest
|
|||||||
if (!fit.Keep)
|
if (!fit.Keep)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var parts = fit.BuildParts(item.Drawing);
|
var parts = fit.BuildParts(canonicalDrawing);
|
||||||
var pairBbox = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
|
var pairBbox = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
|
||||||
var pairW = pairBbox.Width;
|
var pairW = pairBbox.Width;
|
||||||
var pairL = pairBbox.Length;
|
var pairL = pairBbox.Length;
|
||||||
@@ -374,6 +380,10 @@ namespace OpenNest
|
|||||||
|
|
||||||
if (bestPlacement == null) continue;
|
if (bestPlacement == null) continue;
|
||||||
|
|
||||||
|
// Rebind to the original drawing and compose sourceAngle onto rotation so the
|
||||||
|
// final placed parts sit in the user's visible frame.
|
||||||
|
bestPlacement = RebindPairToOriginal(bestPlacement, item.Drawing, sourceAngle);
|
||||||
|
|
||||||
result.AddRange(bestPlacement);
|
result.AddRange(bestPlacement);
|
||||||
item.Quantity = 0;
|
item.Quantity = 0;
|
||||||
|
|
||||||
@@ -388,6 +398,30 @@ namespace OpenNest
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rebinds each canonical-frame Part in the pair to the original Drawing at its current
|
||||||
|
/// world pose, then composes sourceAngle onto each via CanonicalFrame.FromCanonical so
|
||||||
|
/// the returned list is in the original drawing's visible frame. Mirrors
|
||||||
|
/// DefaultNestEngine.RebindAndUnCanonicalize.
|
||||||
|
/// </summary>
|
||||||
|
private static List<Part> RebindPairToOriginal(List<Part> parts, Drawing original, double sourceAngle)
|
||||||
|
{
|
||||||
|
if (parts == null || parts.Count == 0)
|
||||||
|
return parts;
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var p = parts[i];
|
||||||
|
var rebound = Part.CreateAtOrigin(original, p.Rotation);
|
||||||
|
var delta = p.BoundingBox.Location - rebound.BoundingBox.Location;
|
||||||
|
rebound.Offset(delta);
|
||||||
|
rebound.UpdateBounds();
|
||||||
|
parts[i] = rebound;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CanonicalFrame.FromCanonical(parts, sourceAngle);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether a drawing should use grid-fill (true) or bin-pack (false).
|
/// Determines whether a drawing should use grid-fill (true) or bin-pack (false).
|
||||||
/// Low-quantity items whose total area is a small fraction of the plate are
|
/// Low-quantity items whose total area is a small fraction of the plate are
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ namespace OpenNest.Engine
|
|||||||
var mbrArea = mbr.Area;
|
var mbrArea = mbr.Area;
|
||||||
var mbrPerimeter = 2 * (mbr.Width + mbr.Height);
|
var mbrPerimeter = 2 * (mbr.Width + mbr.Height);
|
||||||
|
|
||||||
// Store primary angle (negated to align MBR with axes, same as RotationAnalysis).
|
// Share the single angle formula with CanonicalAngle (no duplicate MBR compute).
|
||||||
result.PrimaryAngle = -mbr.Angle;
|
result.PrimaryAngle = CanonicalAngle.FromMbr(mbr);
|
||||||
|
|
||||||
// Drawing perimeter for circularity and perimeter ratio.
|
// Drawing perimeter for circularity and perimeter ratio.
|
||||||
var drawingPerimeter = polygon.Perimeter();
|
var drawingPerimeter = polygon.Perimeter();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace OpenNest.Engine.Strategies
|
|||||||
public int PlateNumber { get; init; }
|
public int PlateNumber { get; init; }
|
||||||
public CancellationToken Token { get; init; }
|
public CancellationToken Token { get; init; }
|
||||||
public IProgress<NestProgress> Progress { get; init; }
|
public IProgress<NestProgress> Progress { get; init; }
|
||||||
public FillPolicy Policy { get; init; }
|
public FillPolicy Policy { get; init; } = new FillPolicy(new DefaultFillComparer());
|
||||||
public int MaxQuantity { get; init; }
|
public int MaxQuantity { get; init; }
|
||||||
public PartType PartType { get; set; }
|
public PartType PartType { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ namespace OpenNest.IO.Bom
|
|||||||
var nameWithoutExt = Path.GetFileNameWithoutExtension(file);
|
var nameWithoutExt = Path.GetFileNameWithoutExtension(file);
|
||||||
dxfFiles[nameWithoutExt] = file;
|
dxfFiles[nameWithoutExt] = file;
|
||||||
}
|
}
|
||||||
|
foreach (var file in Directory.GetFiles(dxfFolder, "*.dwg"))
|
||||||
|
{
|
||||||
|
var nameWithoutExt = Path.GetFileNameWithoutExtension(file);
|
||||||
|
dxfFiles.TryAdd(nameWithoutExt, file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Partition items into: skipped, unmatched, or matched (grouped)
|
// Partition items into: skipped, unmatched, or matched (grouped)
|
||||||
@@ -57,8 +62,8 @@ namespace OpenNest.IO.Bom
|
|||||||
|
|
||||||
var lookupName = item.FileName;
|
var lookupName = item.FileName;
|
||||||
|
|
||||||
// Strip .dxf extension if the BOM includes it
|
if (lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)
|
||||||
if (lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase))
|
|| lookupName.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase))
|
||||||
lookupName = Path.GetFileNameWithoutExtension(lookupName);
|
lookupName = Path.GetFileNameWithoutExtension(lookupName);
|
||||||
|
|
||||||
if (!folderExists)
|
if (!folderExists)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using ACadSharp;
|
||||||
using OpenNest.Bending;
|
using OpenNest.Bending;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
@@ -38,5 +39,11 @@ namespace OpenNest.IO
|
|||||||
/// Default drawing name (filename without extension, unless overridden).
|
/// Default drawing name (filename without extension, unless overridden).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The raw CAD document from the source file. Available for callers
|
||||||
|
/// that need access to non-geometry entities (e.g., text annotations).
|
||||||
|
/// </summary>
|
||||||
|
public CadDocument Document { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using OpenNest.Bending;
|
|||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.IO.Bending;
|
using OpenNest.IO.Bending;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.IO
|
namespace OpenNest.IO
|
||||||
{
|
{
|
||||||
@@ -25,6 +26,9 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
var dxf = Dxf.Import(path);
|
var dxf = Dxf.Import(path);
|
||||||
|
|
||||||
|
RemoveDuplicateArcs(dxf.Entities);
|
||||||
|
RemoveZeroSweepArcs(dxf.Entities);
|
||||||
|
|
||||||
var bends = new List<Bend>();
|
var bends = new List<Bend>();
|
||||||
if (options.DetectBends && dxf.Document != null)
|
if (options.DetectBends && dxf.Document != null)
|
||||||
{
|
{
|
||||||
@@ -44,6 +48,7 @@ namespace OpenNest.IO
|
|||||||
Bounds = dxf.Entities.GetBoundingBox(),
|
Bounds = dxf.Entities.GetBoundingBox(),
|
||||||
SourcePath = path,
|
SourcePath = path,
|
||||||
Name = options.Name ?? Path.GetFileNameWithoutExtension(path),
|
Name = options.Name ?? Path.GetFileNameWithoutExtension(path),
|
||||||
|
Document = dxf.Document,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,5 +141,39 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
return drawing;
|
return drawing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void RemoveZeroSweepArcs(List<Entity> entities)
|
||||||
|
{
|
||||||
|
entities.RemoveAll(e =>
|
||||||
|
e is Arc arc && arc.StartAngle.IsEqualTo(arc.EndAngle, Tolerance.ChainTolerance));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void RemoveDuplicateArcs(List<Entity> entities)
|
||||||
|
{
|
||||||
|
var circles = entities.OfType<Circle>().ToList();
|
||||||
|
var arcs = entities.OfType<Arc>().ToList();
|
||||||
|
var arcsToRemove = new List<Arc>();
|
||||||
|
|
||||||
|
foreach (var arc in arcs)
|
||||||
|
{
|
||||||
|
foreach (var circle in circles)
|
||||||
|
{
|
||||||
|
if (arc.Layer?.Name != circle.Layer?.Name)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!arc.Center.DistanceTo(circle.Center).IsEqualTo(0))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!arc.Radius.IsEqualTo(circle.Radius))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
arcsToRemove.Add(arc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var arc in arcsToRemove)
|
||||||
|
entities.Remove(arc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,369 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public class ChrFont
|
||||||
|
{
|
||||||
|
private readonly Dictionary<int, ChrGlyph> glyphs = new();
|
||||||
|
|
||||||
|
public string Name { get; internal set; }
|
||||||
|
public string Version { get; internal set; }
|
||||||
|
public double CapHeight { get; internal set; } = 5000;
|
||||||
|
|
||||||
|
internal void AddGlyph(int charCode, ChrGlyph glyph)
|
||||||
|
{
|
||||||
|
glyphs[charCode] = glyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasGlyph(int charCode) => glyphs.ContainsKey(charCode);
|
||||||
|
|
||||||
|
public ChrGlyph GetGlyph(int charCode) =>
|
||||||
|
glyphs.TryGetValue(charCode, out var g) ? g : null;
|
||||||
|
|
||||||
|
public double MeasureTextWidth(string text, double height)
|
||||||
|
{
|
||||||
|
var scale = height / CapHeight;
|
||||||
|
double width = 0;
|
||||||
|
foreach (var ch in text)
|
||||||
|
{
|
||||||
|
var glyph = GetGlyph(ch);
|
||||||
|
if (glyph == null)
|
||||||
|
{
|
||||||
|
var space = GetGlyph(' ');
|
||||||
|
width += (space?.AdvanceWidth ?? CapHeight * 0.6) * scale;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
width += glyph.AdvanceWidth * scale;
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Entity> RenderText(string text, double height, Vector position, Layer layer = null)
|
||||||
|
{
|
||||||
|
var scale = height / CapHeight;
|
||||||
|
var entities = new List<Entity>();
|
||||||
|
var cursorX = position.X;
|
||||||
|
|
||||||
|
foreach (var ch in text)
|
||||||
|
{
|
||||||
|
var glyph = GetGlyph(ch);
|
||||||
|
if (glyph == null)
|
||||||
|
{
|
||||||
|
var space = GetGlyph(' ');
|
||||||
|
cursorX += (space?.AdvanceWidth ?? CapHeight * 0.6) * scale;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var glyphEntities = glyph.ToEntities(scale, cursorX, position.Y, layer);
|
||||||
|
entities.AddRange(glyphEntities);
|
||||||
|
cursorX += glyph.AdvanceWidth * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ChrFont Read(string path, byte? xorKey = null)
|
||||||
|
{
|
||||||
|
var raw = File.ReadAllBytes(path);
|
||||||
|
|
||||||
|
// The whole file is obfuscated with a single-byte XOR. Different
|
||||||
|
// GravoStyle versions use different keys (0x2F in older releases,
|
||||||
|
// 0xCF in 7000-series). The font name at offset 0 is ASCII stored
|
||||||
|
// as UTF-16LE, so the high byte of its first character is 0x00 in
|
||||||
|
// plaintext — which means raw[1] is exactly the XOR key. Detect it
|
||||||
|
// from the file unless the caller forces a specific key.
|
||||||
|
var key = xorKey ?? (raw.Length > 1 ? raw[1] : (byte)0x2F);
|
||||||
|
|
||||||
|
var data = new byte[raw.Length];
|
||||||
|
for (var i = 0; i < raw.Length; i++)
|
||||||
|
data[i] = (byte)(raw[i] ^ key);
|
||||||
|
|
||||||
|
return Parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChrFont Parse(byte[] data)
|
||||||
|
{
|
||||||
|
var font = new ChrFont();
|
||||||
|
|
||||||
|
font.Name = Encoding.Unicode.GetString(data, 0, 26).TrimEnd('\0').Trim();
|
||||||
|
font.Version = Encoding.ASCII.GetString(data, 26, 12).TrimEnd('\0').Trim();
|
||||||
|
|
||||||
|
var charTable = new List<(int charCode, int offset)>();
|
||||||
|
var i = 0x40;
|
||||||
|
while (i + 5 < data.Length)
|
||||||
|
{
|
||||||
|
var charCode = data[i] | (data[i + 1] << 8);
|
||||||
|
var offset = data[i + 2] | (data[i + 3] << 8) | (data[i + 4] << 16) | (data[i + 5] << 24);
|
||||||
|
|
||||||
|
if (charCode < 0x20 || offset == 0 || offset >= data.Length)
|
||||||
|
break;
|
||||||
|
|
||||||
|
charTable.Add((charCode, offset));
|
||||||
|
i += 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var c = 0; c < charTable.Count; c++)
|
||||||
|
{
|
||||||
|
var (charCode, offset) = charTable[c];
|
||||||
|
|
||||||
|
var nextOffset = c + 1 < charTable.Count
|
||||||
|
? FindNextOffset(charTable, offset, data.Length)
|
||||||
|
: data.Length;
|
||||||
|
|
||||||
|
var glyph = ParseGlyph(data, offset, nextOffset);
|
||||||
|
if (glyph != null)
|
||||||
|
font.AddGlyph(charCode, glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (font.glyphs.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var g in font.glyphs.Values)
|
||||||
|
{
|
||||||
|
if (g.CapHeight > 0)
|
||||||
|
{
|
||||||
|
font.CapHeight = g.CapHeight;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindNextOffset(List<(int charCode, int offset)> table, int currentOffset, int fileLength)
|
||||||
|
{
|
||||||
|
var best = fileLength;
|
||||||
|
foreach (var (_, off) in table)
|
||||||
|
{
|
||||||
|
if (off > currentOffset && off < best)
|
||||||
|
best = off;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChrGlyph ParseGlyph(byte[] data, int offset, int endOffset)
|
||||||
|
{
|
||||||
|
if (offset + 92 > data.Length)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var glyph = new ChrGlyph();
|
||||||
|
glyph.CapHeight = ReadBE16(data, offset + 15 * 2);
|
||||||
|
var bearing = System.Math.Abs(ReadBE16(data, offset + 18 * 2));
|
||||||
|
glyph.AdvanceWidth = ReadBE16(data, offset + 22 * 2) + bearing;
|
||||||
|
|
||||||
|
var strokeStart = offset + 92;
|
||||||
|
var pos = strokeStart;
|
||||||
|
var currentStroke = new List<ChrStrokePoint>();
|
||||||
|
|
||||||
|
while (pos + 5 < endOffset)
|
||||||
|
{
|
||||||
|
var cmd = ReadBE16(data, pos);
|
||||||
|
var x = ReadBE16(data, pos + 2);
|
||||||
|
var y = ReadBE16(data, pos + 4);
|
||||||
|
pos += 6;
|
||||||
|
|
||||||
|
if (System.Math.Abs(x) > 15000 || System.Math.Abs(y) > 15000)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (cmd < -1000)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var type = cmd switch
|
||||||
|
{
|
||||||
|
1 => ChrPointType.Vertex,
|
||||||
|
4 => ChrPointType.Control,
|
||||||
|
5 => ChrPointType.EndPoint,
|
||||||
|
_ => ChrPointType.Vertex,
|
||||||
|
};
|
||||||
|
|
||||||
|
currentStroke.Add(new ChrStrokePoint(type, x, y));
|
||||||
|
|
||||||
|
if (type == ChrPointType.EndPoint)
|
||||||
|
{
|
||||||
|
if (currentStroke.Count > 0)
|
||||||
|
glyph.Strokes.Add(currentStroke);
|
||||||
|
currentStroke = new List<ChrStrokePoint>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStroke.Count > 0)
|
||||||
|
glyph.Strokes.Add(currentStroke);
|
||||||
|
|
||||||
|
return glyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadBE16(byte[] data, int offset)
|
||||||
|
{
|
||||||
|
var val = (data[offset] << 8) | data[offset + 1];
|
||||||
|
if (val > 32767) val -= 65536;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum ChrPointType
|
||||||
|
{
|
||||||
|
Vertex,
|
||||||
|
Control,
|
||||||
|
EndPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal struct ChrStrokePoint
|
||||||
|
{
|
||||||
|
public ChrPointType Type;
|
||||||
|
public double X;
|
||||||
|
public double Y;
|
||||||
|
|
||||||
|
public ChrStrokePoint(ChrPointType type, double x, double y)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
X = x;
|
||||||
|
Y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChrGlyph
|
||||||
|
{
|
||||||
|
internal readonly List<List<ChrStrokePoint>> Strokes = new();
|
||||||
|
|
||||||
|
public double AdvanceWidth { get; internal set; }
|
||||||
|
public double CapHeight { get; internal set; }
|
||||||
|
|
||||||
|
private const int ArcSamples = 16;
|
||||||
|
|
||||||
|
public List<Entity> ToEntities(double scale, double offsetX, double offsetY, Layer layer = null)
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>();
|
||||||
|
layer ??= Layer.Default;
|
||||||
|
|
||||||
|
foreach (var stroke in Strokes)
|
||||||
|
{
|
||||||
|
if (stroke.Count < 2) continue;
|
||||||
|
|
||||||
|
var segments = BuildSegments(stroke);
|
||||||
|
foreach (var seg in segments)
|
||||||
|
{
|
||||||
|
if (seg.Points.Count < 2) continue;
|
||||||
|
|
||||||
|
var scaled = new List<Vector>(seg.Points.Count);
|
||||||
|
foreach (var pt in seg.Points)
|
||||||
|
scaled.Add(new Vector(pt.X * scale + offsetX, pt.Y * scale + offsetY));
|
||||||
|
|
||||||
|
var converted = PointsToLines(scaled);
|
||||||
|
|
||||||
|
foreach (var e in converted)
|
||||||
|
{
|
||||||
|
e.Layer = layer;
|
||||||
|
entities.Add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Entity> PointsToLines(List<Vector> points)
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>();
|
||||||
|
for (var i = 0; i < points.Count - 1; i++)
|
||||||
|
{
|
||||||
|
if (points[i].DistanceTo(points[i + 1]) < 0.001)
|
||||||
|
continue;
|
||||||
|
entities.Add(new Line(points[i], points[i + 1]));
|
||||||
|
}
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<StrokeSegment> BuildSegments(List<ChrStrokePoint> stroke)
|
||||||
|
{
|
||||||
|
var segments = new List<StrokeSegment>();
|
||||||
|
var current = new StrokeSegment();
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (i < stroke.Count)
|
||||||
|
{
|
||||||
|
var pt = stroke[i];
|
||||||
|
|
||||||
|
if (pt.Type == ChrPointType.Vertex || pt.Type == ChrPointType.EndPoint)
|
||||||
|
{
|
||||||
|
if (i + 1 < stroke.Count && stroke[i + 1].Type == ChrPointType.Control)
|
||||||
|
{
|
||||||
|
var p0 = new Vector(pt.X, pt.Y);
|
||||||
|
var pMid = new Vector(stroke[i + 1].X, stroke[i + 1].Y);
|
||||||
|
var p2End = i + 2 < stroke.Count ? stroke[i + 2] : stroke[i + 1];
|
||||||
|
var p1 = new Vector(p2End.X, p2End.Y);
|
||||||
|
|
||||||
|
SampleCircularArc(current.Points, p0, pMid, p1, ArcSamples);
|
||||||
|
current.HasCurves = true;
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current.Points.Add(new Vector(pt.X, pt.Y));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.Points.Count >= 2)
|
||||||
|
segments.Add(current);
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class StrokeSegment
|
||||||
|
{
|
||||||
|
public readonly List<Vector> Points = new();
|
||||||
|
public bool HasCurves;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SampleCircularArc(List<Vector> output, Vector p0, Vector pMid, Vector p1, int samples)
|
||||||
|
{
|
||||||
|
if (output.Count == 0 || output[^1].DistanceTo(p0) > 0.01)
|
||||||
|
output.Add(p0);
|
||||||
|
|
||||||
|
double ax = p0.X, ay = p0.Y;
|
||||||
|
double bx = pMid.X, by = pMid.Y;
|
||||||
|
double cx = p1.X, cy = p1.Y;
|
||||||
|
|
||||||
|
var d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
||||||
|
|
||||||
|
if (System.Math.Abs(d) < 1e-6)
|
||||||
|
{
|
||||||
|
output.Add(pMid);
|
||||||
|
output.Add(p1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
|
||||||
|
var uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
|
||||||
|
var radius = System.Math.Sqrt((ax - ux) * (ax - ux) + (ay - uy) * (ay - uy));
|
||||||
|
|
||||||
|
var a0 = System.Math.Atan2(ay - uy, ax - ux);
|
||||||
|
var am = System.Math.Atan2(by - uy, bx - ux);
|
||||||
|
var a1 = System.Math.Atan2(cy - uy, cx - ux);
|
||||||
|
|
||||||
|
var ccwSweep = a1 - a0;
|
||||||
|
while (ccwSweep <= 0) ccwSweep += 2 * System.Math.PI;
|
||||||
|
|
||||||
|
var midRel = am - a0;
|
||||||
|
while (midRel < 0) midRel += 2 * System.Math.PI;
|
||||||
|
|
||||||
|
var sweep = midRel < ccwSweep ? ccwSweep : ccwSweep - 2 * System.Math.PI;
|
||||||
|
|
||||||
|
for (var i = 1; i <= samples; i++)
|
||||||
|
{
|
||||||
|
var t = (double)i / samples;
|
||||||
|
var angle = a0 + sweep * t;
|
||||||
|
output.Add(new Vector(ux + radius * System.Math.Cos(angle), uy + radius * System.Math.Sin(angle)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
-7
@@ -27,8 +27,7 @@ namespace OpenNest.IO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static DxfImportResult Import(string path)
|
public static DxfImportResult Import(string path)
|
||||||
{
|
{
|
||||||
using var reader = new DxfReader(path);
|
var doc = ReadDocument(path);
|
||||||
var doc = reader.Read();
|
|
||||||
|
|
||||||
return new DxfImportResult
|
return new DxfImportResult
|
||||||
{
|
{
|
||||||
@@ -41,8 +40,7 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var reader = new DxfReader(path);
|
var doc = ReadDocument(path);
|
||||||
var doc = reader.Read();
|
|
||||||
return ConvertEntities(doc);
|
return ConvertEntities(doc);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -67,6 +65,36 @@ namespace OpenNest.IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<Entity> GetGeometry(string path, Func<string, bool> layerFilter)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var reader = new DxfReader(path);
|
||||||
|
var doc = reader.Read();
|
||||||
|
return ConvertEntities(doc, layerFilter);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine(ex.Message);
|
||||||
|
return new List<Entity>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Entity> GetGeometry(Stream stream, Func<string, bool> layerFilter)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var reader = new DxfReader(stream);
|
||||||
|
var doc = reader.Read();
|
||||||
|
return ConvertEntities(doc, layerFilter);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine(ex.Message);
|
||||||
|
return new List<Entity>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Export
|
#region Export
|
||||||
@@ -113,15 +141,34 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
#region Private
|
#region Private
|
||||||
|
|
||||||
private static List<Entity> ConvertEntities(CadDocument doc)
|
private static bool IsDwg(string path) =>
|
||||||
|
Path.GetExtension(path).Equals(".dwg", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static CadDocument ReadDocument(string path)
|
||||||
|
{
|
||||||
|
if (IsDwg(path))
|
||||||
|
{
|
||||||
|
using var reader = new DwgReader(path);
|
||||||
|
return reader.Read();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var reader = new DxfReader(path);
|
||||||
|
return reader.Read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Entity> ConvertEntities(CadDocument doc, Func<string, bool> layerFilter = null)
|
||||||
{
|
{
|
||||||
var entities = new List<Entity>();
|
var entities = new List<Entity>();
|
||||||
var lines = new List<Line>();
|
var lines = new List<Line>();
|
||||||
var arcs = new List<Arc>();
|
var arcs = new List<Arc>();
|
||||||
|
var circles = new List<Circle>();
|
||||||
|
var filter = layerFilter ?? IsNonCutLayer;
|
||||||
|
|
||||||
foreach (var entity in doc.Entities)
|
foreach (var entity in doc.Entities)
|
||||||
{
|
{
|
||||||
if (IsNonCutLayer(entity.Layer?.Name))
|
if (filter(entity.Layer?.Name))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
switch (entity)
|
switch (entity)
|
||||||
@@ -135,7 +182,7 @@ namespace OpenNest.IO
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ACadSharp.Entities.Circle circle:
|
case ACadSharp.Entities.Circle circle:
|
||||||
entities.Add(circle.ToOpenNest());
|
circles.Add(circle.ToOpenNest());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ACadSharp.Entities.Spline spline:
|
case ACadSharp.Entities.Spline spline:
|
||||||
@@ -166,7 +213,10 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
GeometryOptimizer.Optimize(lines);
|
GeometryOptimizer.Optimize(lines);
|
||||||
GeometryOptimizer.Optimize(arcs);
|
GeometryOptimizer.Optimize(arcs);
|
||||||
|
GeometryOptimizer.Deduplicate(circles);
|
||||||
|
GeometryOptimizer.Deduplicate(circles, arcs);
|
||||||
|
|
||||||
|
entities.AddRange(circles);
|
||||||
entities.AddRange(lines);
|
entities.AddRange(lines);
|
||||||
entities.AddRange(arcs);
|
entities.AddRange(arcs);
|
||||||
|
|
||||||
|
|||||||
@@ -181,13 +181,22 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
var center = new Vector(ellipse.Center.X, ellipse.Center.Y);
|
var center = new Vector(ellipse.Center.X, ellipse.Center.Y);
|
||||||
var majorAxis = new Vector(ellipse.MajorAxisEndPoint.X, ellipse.MajorAxisEndPoint.Y);
|
var majorAxis = new Vector(ellipse.MajorAxisEndPoint.X, ellipse.MajorAxisEndPoint.Y);
|
||||||
var semiMajor = System.Math.Sqrt(majorAxis.X * majorAxis.X + majorAxis.Y * majorAxis.Y);
|
|
||||||
var semiMinor = semiMajor * ellipse.RadiusRatio;
|
|
||||||
var rotation = System.Math.Atan2(majorAxis.Y, majorAxis.X);
|
|
||||||
|
|
||||||
var startParam = ellipse.StartParameter;
|
var startParam = ellipse.StartParameter;
|
||||||
var endParam = ellipse.EndParameter;
|
var endParam = ellipse.EndParameter;
|
||||||
|
|
||||||
|
if (ellipse.Normal.Z < 0)
|
||||||
|
{
|
||||||
|
var newStart = OpenNest.Math.Angle.TwoPI - endParam;
|
||||||
|
var newEnd = OpenNest.Math.Angle.TwoPI - startParam;
|
||||||
|
startParam = newStart;
|
||||||
|
endParam = newEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
var semiMajor = System.Math.Sqrt(majorAxis.X * majorAxis.X + majorAxis.Y * majorAxis.Y);
|
||||||
|
var semiMinor = semiMajor * ellipse.RadiusRatio;
|
||||||
|
var rotation = System.Math.Atan2(majorAxis.Y, majorAxis.X);
|
||||||
|
|
||||||
var layer = ellipse.Layer.ToOpenNest();
|
var layer = ellipse.Layer.ToOpenNest();
|
||||||
var color = ellipse.ResolveColor();
|
var color = ellipse.ResolveColor();
|
||||||
var lineTypeName = ellipse.ResolveLineTypeName();
|
var lineTypeName = ellipse.ResolveLineTypeName();
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
<RootNamespace>OpenNest.IO</RootNamespace>
|
<RootNamespace>OpenNest.IO</RootNamespace>
|
||||||
<AssemblyName>OpenNest.IO</AssemblyName>
|
<AssemblyName>OpenNest.IO</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="OpenNest.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
|
|||||||
@@ -16,11 +16,16 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
{
|
{
|
||||||
private readonly CincinnatiPostConfig _config;
|
private readonly CincinnatiPostConfig _config;
|
||||||
private readonly CincinnatiFeatureWriter _featureWriter;
|
private readonly CincinnatiFeatureWriter _featureWriter;
|
||||||
|
private readonly CoordinateFormatter _fmt;
|
||||||
|
private readonly Dictionary<int, int> _holeSubprograms;
|
||||||
|
|
||||||
public CincinnatiPartSubprogramWriter(CincinnatiPostConfig config)
|
public CincinnatiPartSubprogramWriter(CincinnatiPostConfig config,
|
||||||
|
Dictionary<int, int> holeSubprograms = null)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_featureWriter = new CincinnatiFeatureWriter(config);
|
_featureWriter = new CincinnatiFeatureWriter(config);
|
||||||
|
_fmt = new CoordinateFormatter(config.PostedAccuracy);
|
||||||
|
_holeSubprograms = holeSubprograms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -44,6 +49,15 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
for (var i = 0; i < ordered.Count; i++)
|
for (var i = 0; i < ordered.Count; i++)
|
||||||
{
|
{
|
||||||
var (codes, isEtch) = ordered[i];
|
var (codes, isEtch) = ordered[i];
|
||||||
|
var isLastFeature = i == ordered.Count - 1;
|
||||||
|
|
||||||
|
// SubProgramCall features are emitted as M98 hole calls
|
||||||
|
if (codes.Count == 1 && codes[0] is SubProgramCall holeCall)
|
||||||
|
{
|
||||||
|
WriteHoleSubprogramCall(w, holeCall, i, isLastFeature);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var featureNumber = i == 0
|
var featureNumber = i == 0
|
||||||
? _config.FeatureLineNumberStart
|
? _config.FeatureLineNumberStart
|
||||||
: 1000 + i + 1;
|
: 1000 + i + 1;
|
||||||
@@ -55,7 +69,7 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
FeatureNumber = featureNumber,
|
FeatureNumber = featureNumber,
|
||||||
PartName = drawingName,
|
PartName = drawingName,
|
||||||
IsFirstFeatureOfPart = false,
|
IsFirstFeatureOfPart = false,
|
||||||
IsLastFeatureOnSheet = i == ordered.Count - 1,
|
IsLastFeatureOnSheet = isLastFeature,
|
||||||
IsSafetyHeadraise = false,
|
IsSafetyHeadraise = false,
|
||||||
IsExteriorFeature = false,
|
IsExteriorFeature = false,
|
||||||
IsEtch = isEtch,
|
IsEtch = isEtch,
|
||||||
@@ -70,6 +84,30 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
w.WriteLine($"M99 (END OF {drawingName})");
|
w.WriteLine($"M99 (END OF {drawingName})");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void WriteHoleSubprogramCall(TextWriter w, SubProgramCall call,
|
||||||
|
int featureIndex, bool isLastFeature)
|
||||||
|
{
|
||||||
|
var postSubNum = _holeSubprograms != null && _holeSubprograms.TryGetValue(call.Id, out var num)
|
||||||
|
? num : call.Id;
|
||||||
|
|
||||||
|
var featureNumber = featureIndex == 0
|
||||||
|
? _config.FeatureLineNumberStart
|
||||||
|
: 1000 + featureIndex + 1;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
if (_config.UseLineNumbers)
|
||||||
|
sb.Append($"N{featureNumber} ");
|
||||||
|
sb.Append($"G52 X{_fmt.FormatCoord(call.Offset.X)} Y{_fmt.FormatCoord(call.Offset.Y)}");
|
||||||
|
w.WriteLine(sb.ToString());
|
||||||
|
|
||||||
|
w.WriteLine($"M98 P{postSubNum}");
|
||||||
|
|
||||||
|
w.WriteLine("G52 X0 Y0");
|
||||||
|
|
||||||
|
if (!isLastFeature)
|
||||||
|
w.WriteLine("M47");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If the program has no leading rapid, inserts a synthetic rapid at the
|
/// If the program has no leading rapid, inserts a synthetic rapid at the
|
||||||
/// last motion endpoint (the contour return point). This ensures the feature
|
/// last motion endpoint (the contour return point). This ensures the feature
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenNest.Posts.Cincinnati
|
namespace OpenNest.Posts.Cincinnati
|
||||||
{
|
{
|
||||||
@@ -277,6 +279,24 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
[DisplayName("Etch Libraries")]
|
[DisplayName("Etch Libraries")]
|
||||||
[Description("Gas-to-library mapping for etch operations.")]
|
[Description("Gas-to-library mapping for etch operations.")]
|
||||||
public List<EtchLibraryEntry> EtchLibraries { get; set; } = new();
|
public List<EtchLibraryEntry> EtchLibraries { get; set; } = new();
|
||||||
|
|
||||||
|
[Category("B. Libraries")]
|
||||||
|
[DisplayName("Selected Library")]
|
||||||
|
[Description("Overrides Material/Thickness/Gas auto-resolution. Pick an existing entry from Material Libraries, or leave blank to auto-resolve.")]
|
||||||
|
[TypeConverter(typeof(MaterialLibraryNameConverter))]
|
||||||
|
public string SelectedLibrary { get; set; } = "";
|
||||||
|
|
||||||
|
public string FindBestLibrary(string materialName, double thickness)
|
||||||
|
{
|
||||||
|
if (MaterialLibraries == null || string.IsNullOrEmpty(materialName))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
return MaterialLibraries
|
||||||
|
.Where(e => string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(e => System.Math.Abs(e.Thickness - thickness))
|
||||||
|
.Select(e => e.Library)
|
||||||
|
.FirstOrDefault() ?? "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MaterialLibraryEntry
|
public class MaterialLibraryEntry
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using OpenNest.CNC;
|
|||||||
|
|
||||||
namespace OpenNest.Posts.Cincinnati
|
namespace OpenNest.Posts.Cincinnati
|
||||||
{
|
{
|
||||||
public sealed class CincinnatiPostProcessor : IConfigurablePostProcessor
|
public sealed class CincinnatiPostProcessor : IConfigurablePostProcessor, IPostProcessorNestAware, IMaterialProvidingPostProcessor
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
@@ -25,6 +25,23 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
|
|
||||||
object IConfigurablePostProcessor.Config => Config;
|
object IConfigurablePostProcessor.Config => Config;
|
||||||
|
|
||||||
|
public IEnumerable<string> GetMaterialNames()
|
||||||
|
{
|
||||||
|
if (Config?.MaterialLibraries == null)
|
||||||
|
return System.Array.Empty<string>();
|
||||||
|
|
||||||
|
return Config.MaterialLibraries
|
||||||
|
.Select(e => e.Material)
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrepareForNest(Nest nest)
|
||||||
|
{
|
||||||
|
var materialName = nest?.Material?.Name ?? "";
|
||||||
|
var thickness = nest?.Thickness ?? 0.0;
|
||||||
|
Config.SelectedLibrary = Config.FindBestLibrary(materialName, thickness);
|
||||||
|
}
|
||||||
|
|
||||||
public CincinnatiPostProcessor()
|
public CincinnatiPostProcessor()
|
||||||
{
|
{
|
||||||
var configPath = GetConfigPath();
|
var configPath = GetConfigPath();
|
||||||
@@ -128,7 +145,8 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
// Part sub-programs (if enabled)
|
// Part sub-programs (if enabled)
|
||||||
if (subprogramEntries != null)
|
if (subprogramEntries != null)
|
||||||
{
|
{
|
||||||
var partSubWriter = new CincinnatiPartSubprogramWriter(Config);
|
var partSubWriter = new CincinnatiPartSubprogramWriter(Config,
|
||||||
|
holeMapping.Count > 0 ? holeMapping : null);
|
||||||
var sheetDiagonal = firstPlate != null
|
var sheetDiagonal = firstPlate != null
|
||||||
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
|
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
|
||||||
+ firstPlate.Size.Length * firstPlate.Size.Length)
|
+ firstPlate.Size.Length * firstPlate.Size.Length)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.Cincinnati
|
||||||
|
{
|
||||||
|
public sealed class MaterialLibraryNameConverter : StringConverter
|
||||||
|
{
|
||||||
|
public override bool GetStandardValuesSupported(ITypeDescriptorContext context) => true;
|
||||||
|
|
||||||
|
public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) => false;
|
||||||
|
|
||||||
|
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
|
||||||
|
{
|
||||||
|
var config = context?.Instance as CincinnatiPostConfig;
|
||||||
|
var names = new List<string> { "" };
|
||||||
|
|
||||||
|
if (config?.MaterialLibraries != null)
|
||||||
|
{
|
||||||
|
names.AddRange(config.MaterialLibraries
|
||||||
|
.Select(e => e.Library)
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StandardValuesCollection(names);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,15 +10,20 @@ public sealed class MaterialLibraryResolver
|
|||||||
|
|
||||||
private readonly List<MaterialLibraryEntry> _materialLibraries;
|
private readonly List<MaterialLibraryEntry> _materialLibraries;
|
||||||
private readonly List<EtchLibraryEntry> _etchLibraries;
|
private readonly List<EtchLibraryEntry> _etchLibraries;
|
||||||
|
private readonly string _selectedLibrary;
|
||||||
|
|
||||||
public MaterialLibraryResolver(CincinnatiPostConfig config)
|
public MaterialLibraryResolver(CincinnatiPostConfig config)
|
||||||
{
|
{
|
||||||
_materialLibraries = config.MaterialLibraries ?? new List<MaterialLibraryEntry>();
|
_materialLibraries = config.MaterialLibraries ?? new List<MaterialLibraryEntry>();
|
||||||
_etchLibraries = config.EtchLibraries ?? new List<EtchLibraryEntry>();
|
_etchLibraries = config.EtchLibraries ?? new List<EtchLibraryEntry>();
|
||||||
|
_selectedLibrary = config.SelectedLibrary ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ResolveCutLibrary(string materialName, double thickness, string gas)
|
public string ResolveCutLibrary(string materialName, double thickness, string gas)
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_selectedLibrary))
|
||||||
|
return EnsureLibExtension(_selectedLibrary);
|
||||||
|
|
||||||
var entry = _materialLibraries.FirstOrDefault(e =>
|
var entry = _materialLibraries.FirstOrDefault(e =>
|
||||||
string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase) &&
|
string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase) &&
|
||||||
System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance &&
|
System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance &&
|
||||||
|
|||||||
@@ -6,11 +6,19 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="OpenNest.Posts.Cincinnati.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
<Target Name="CopyToPostsDir" AfterTargets="Build">
|
<Target Name="CopyToPostsDir" AfterTargets="Build">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir>
|
<PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir>
|
||||||
|
<ConfigJson>$(MSBuildProjectDirectory)\OpenNest.Posts.Cincinnati.json</ConfigJson>
|
||||||
|
<DeployedConfigJson>$(PostsDir)OpenNest.Posts.Cincinnati.json</DeployedConfigJson>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<MakeDir Directories="$(PostsDir)" />
|
<MakeDir Directories="$(PostsDir)" />
|
||||||
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" />
|
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" />
|
||||||
|
<Copy SourceFiles="$(ConfigJson)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" Condition="!Exists('$(DeployedConfigJson)')" />
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"ConfigurationName": "CL940",
|
||||||
|
"PostedUnits": "Inches",
|
||||||
|
"PostedAccuracy": 4,
|
||||||
|
"UseLineNumbers": true,
|
||||||
|
"FeatureLineNumberStart": 1,
|
||||||
|
"UseSheetSubprograms": true,
|
||||||
|
"SheetSubprogramStart": 101,
|
||||||
|
"UsePartSubprograms": false,
|
||||||
|
"PartSubprogramStart": 200,
|
||||||
|
"VariableDeclarationSubprogram": 100,
|
||||||
|
"CoordModeBetweenParts": "G92",
|
||||||
|
"ProcessParameterMode": "LibraryFile",
|
||||||
|
"DefaultAssistGas": "O2",
|
||||||
|
"DefaultEtchGas": "N2",
|
||||||
|
"UseExactStopMode": false,
|
||||||
|
"UseSpeedGas": false,
|
||||||
|
"UseAntiDive": true,
|
||||||
|
"UseSmartRapids": false,
|
||||||
|
"KerfCompensation": "ControllerSide",
|
||||||
|
"DefaultKerfSide": "Left",
|
||||||
|
"InteriorM47": "Always",
|
||||||
|
"ExteriorM47": "Always",
|
||||||
|
"M47OverrideDistanceThreshold": null,
|
||||||
|
"SafetyHeadraiseDistance": 2000,
|
||||||
|
"PalletExchange": "EndOfSheet",
|
||||||
|
"LeadInFeedratePercent": 0.5,
|
||||||
|
"LeadInArcLine2FeedratePercent": 0.5,
|
||||||
|
"LeadOutFeedratePercent": 0.5,
|
||||||
|
"CircleFeedrateMultiplier": 0.8,
|
||||||
|
"ArcFeedrate": "None",
|
||||||
|
"ArcFeedrateRanges": [
|
||||||
|
{ "MaxRadius": 0.125, "FeedratePercent": 0.25, "VariableNumber": 123 },
|
||||||
|
{ "MaxRadius": 0.75, "FeedratePercent": 0.5, "VariableNumber": 124 },
|
||||||
|
{ "MaxRadius": 4.5, "FeedratePercent": 0.8, "VariableNumber": 125 }
|
||||||
|
],
|
||||||
|
"UserVariableStart": 200,
|
||||||
|
"SheetWidthVariable": 110,
|
||||||
|
"SheetLengthVariable": 111,
|
||||||
|
"MaterialLibraries": [
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.032, "Gas": "AIR", "Library": "AL032AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.032, "Gas": "N2", "Library": "AL032N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.032, "Gas": "O2", "Library": "AL032O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.050, "Gas": "AIR", "Library": "AL050AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.050, "Gas": "N2", "Library": "AL050N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.050, "Gas": "O2", "Library": "AL050O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.063, "Gas": "AIR", "Library": "AL063AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.063, "Gas": "N2", "Library": "AL063N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.063, "Gas": "O2", "Library": "AL063O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.080, "Gas": "AIR", "Library": "AL080AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.080, "Gas": "N2", "Library": "AL080N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.080, "Gas": "O2", "Library": "AL080O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.090, "Gas": "AIR", "Library": "AL090AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.090, "Gas": "N2", "Library": "AL090N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.090, "Gas": "O2", "Library": "AL090O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.100, "Gas": "AIR", "Library": "AL100AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.100, "Gas": "N2", "Library": "AL100N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.100, "Gas": "O2", "Library": "AL100O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.125, "Gas": "AIR", "Library": "AL125AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.125, "Gas": "N2", "Library": "AL125N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.125, "Gas": "O2", "Library": "AL125O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.190, "Gas": "AIR", "Library": "AL190AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.190, "Gas": "N2", "Library": "AL190N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.190, "Gas": "O2", "Library": "AL190O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.250, "Gas": "AIR", "Library": "AL250AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.250, "Gas": "N2", "Library": "AL250N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.250, "Gas": "O2", "Library": "AL250O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.375, "Gas": "AIR", "Library": "AL375AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.375, "Gas": "N2", "Library": "AL375N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.375, "Gas": "O2", "Library": "AL375O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.500, "Gas": "AIR", "Library": "AL500AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.500, "Gas": "N2", "Library": "AL500N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.500, "Gas": "O2", "Library": "AL500O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.625, "Gas": "N2", "Library": "AL625N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.750, "Gas": "AIR", "Library": "AL750AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.750, "Gas": "N2", "Library": "AL750N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.750, "Gas": "O2", "Library": "AL750O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 1.000, "Gas": "AIR", "Library": "AL1000AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 1.000, "Gas": "N2", "Library": "AL1000N2" },
|
||||||
|
|
||||||
|
{ "Material": "Galvanized Steel", "Thickness": 0.135, "Gas": "N2", "Library": "GALV135N2" },
|
||||||
|
{ "Material": "Galvanized Steel", "Thickness": 0.188, "Gas": "N2", "Library": "GALV188N2" },
|
||||||
|
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.036, "Gas": "AIR", "Library": "MS036AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.036, "Gas": "N2", "Library": "MS036N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.048, "Gas": "AIR", "Library": "MS048AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.048, "Gas": "N2", "Library": "MS048N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.060, "Gas": "AIR", "Library": "MS060AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.060, "Gas": "N2", "Library": "MS060N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.075, "Gas": "AIR", "Library": "MS075AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.075, "Gas": "N2", "Library": "MS075N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.075, "Gas": "N2", "Library": "MS075N2FE" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.090, "Gas": "N2", "Library": "MS090N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.105, "Gas": "AIR", "Library": "MS105AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.105, "Gas": "N2", "Library": "MS105N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.120, "Gas": "AIR", "Library": "MS120AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.120, "Gas": "N2", "Library": "MS120N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.120, "Gas": "N2", "Library": "MS120N2FE" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "AIR", "Library": "MS135AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "N2", "Library": "MS135N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "N2", "Library": "MS135N2FE" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "N2", "Library": "MS135N2Panel" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "AIR", "Library": "MS188AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "N2", "Library": "MS188N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "N2", "Library": "MS188N2FLOORPLATE" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "O2", "Library": "MS188O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "AIR", "Library": "MS250AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "N2", "Library": "MS250N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "N2", "Library": "MS250N2FLOORPLATE" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "O2", "Library": "MS250O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.313, "Gas": "O2", "Library": "MS313O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.375, "Gas": "O2", "Library": "MS375O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.500, "Gas": "N2", "Library": "MS500N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.500, "Gas": "O2", "Library": "MS500O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.625, "Gas": "O2", "Library": "MS625O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.750, "Gas": "O2", "Library": "MS750O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 1.000, "Gas": "O2", "Library": "MS1000O2" },
|
||||||
|
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.036, "Gas": "AIR", "Library": "SS036AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.036, "Gas": "N2", "Library": "SS036N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.048, "Gas": "AIR", "Library": "SS048AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.048, "Gas": "N2", "Library": "SS048N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.060, "Gas": "AIR", "Library": "SS060AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.060, "Gas": "N2", "Library": "SS060N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.075, "Gas": "AIR", "Library": "SS075AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.075, "Gas": "N2", "Library": "SS075N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.075, "Gas": "N2", "Library": "SS075N2FE" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.105, "Gas": "AIR", "Library": "SS105AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.105, "Gas": "N2", "Library": "SS105N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.105, "Gas": "N2", "Library": "SS105N2FE" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.120, "Gas": "AIR", "Library": "SS120AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.120, "Gas": "N2", "Library": "SS120N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.120, "Gas": "N2", "Library": "SS120N2FE" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.135, "Gas": "AIR", "Library": "SS135AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.135, "Gas": "N2", "Library": "SS135N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.135, "Gas": "N2", "Library": "SS135N2FE" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.188, "Gas": "AIR", "Library": "SS188AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.188, "Gas": "N2", "Library": "SS188N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.250, "Gas": "AIR", "Library": "SS250AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.250, "Gas": "N2", "Library": "SS250N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.313, "Gas": "N2", "Library": "SS313N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.375, "Gas": "AIR", "Library": "SS375AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.375, "Gas": "N2", "Library": "SS375N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.500, "Gas": "AIR", "Library": "SS500AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.500, "Gas": "N2", "Library": "SS500N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.625, "Gas": "N2", "Library": "SS625N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.750, "Gas": "AIR", "Library": "SS750AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.750, "Gas": "N2", "Library": "SS750N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 1.000, "Gas": "AIR", "Library": "SS1000AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 1.000, "Gas": "N2", "Library": "SS1000N2" },
|
||||||
|
|
||||||
|
{ "Material": "Phenolic", "Thickness": 0.0, "Gas": "", "Library": "Phenolic" },
|
||||||
|
{ "Material": "Gasket", "Thickness": 0.250, "Gas": "N2", "Library": "GASKET250N2" }
|
||||||
|
],
|
||||||
|
"EtchLibraries": [
|
||||||
|
{ "Gas": "AIR", "Library": "EtchAIR" },
|
||||||
|
{ "Gas": "N2", "Library": "EtchN2" },
|
||||||
|
{ "Gas": "N2", "Library": "EtchN2_fast" },
|
||||||
|
{ "Gas": "N2", "Library": "Etchn2_no_mark_pvc" },
|
||||||
|
{ "Gas": "O2", "Library": "EtchO2" },
|
||||||
|
{ "Gas": "O2", "Library": "ETCHO2FINE" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.GravographIS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serial streamer for the Gravograph IS8000. 9600 8-N-1; flow control is
|
||||||
|
/// configurable and defaults to RTS/CTS (the controller is buffered and drops
|
||||||
|
/// CTS to apply backpressure). The job is sent in modest chunks rather than as
|
||||||
|
/// one giant write so the handshake can pause the write mid-stream.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GravographISPort : IDisposable
|
||||||
|
{
|
||||||
|
private SerialPort port;
|
||||||
|
|
||||||
|
public const int DefaultBaudRate = 9600;
|
||||||
|
public const int DefaultChunkSize = 256;
|
||||||
|
public const int DefaultWriteTimeoutMs = 30000;
|
||||||
|
|
||||||
|
public int ChunkSize { get; set; } = DefaultChunkSize;
|
||||||
|
public int WriteTimeoutMs { get; set; } = DefaultWriteTimeoutMs;
|
||||||
|
|
||||||
|
public bool IsOpen => port != null && port.IsOpen;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens the port at the controller's required line settings (9600 8-N-1)
|
||||||
|
/// with the given <paramref name="handshake"/>. Throws if the port is
|
||||||
|
/// already open or if opening fails.
|
||||||
|
/// </summary>
|
||||||
|
public void Open(string portName, Handshake handshake = Handshake.RequestToSend)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(portName))
|
||||||
|
throw new ArgumentException("Port name is required.", nameof(portName));
|
||||||
|
if (port != null)
|
||||||
|
throw new InvalidOperationException("Port is already open.");
|
||||||
|
|
||||||
|
port = new SerialPort(portName, DefaultBaudRate, Parity.None, 8, StopBits.One)
|
||||||
|
{
|
||||||
|
Handshake = handshake,
|
||||||
|
WriteTimeout = WriteTimeoutMs,
|
||||||
|
ReadTimeout = WriteTimeoutMs,
|
||||||
|
// DTR/RTS are needed for some USB-serial bridges and for RTS/CTS flow:
|
||||||
|
DtrEnable = true,
|
||||||
|
RtsEnable = handshake != Handshake.RequestToSend &&
|
||||||
|
handshake != Handshake.RequestToSendXOnXOff,
|
||||||
|
};
|
||||||
|
|
||||||
|
port.Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Streams the encoded job to the port in chunks. Cancellable. The chunked
|
||||||
|
/// write is intentional — Write() blocks until the OS accepts the bytes,
|
||||||
|
/// which with RTS/CTS or XOn/XOff yields cleanly when the controller's
|
||||||
|
/// buffer is full.
|
||||||
|
/// </summary>
|
||||||
|
public void StreamJob(byte[] data, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (data == null) throw new ArgumentNullException(nameof(data));
|
||||||
|
if (port == null || !port.IsOpen)
|
||||||
|
throw new InvalidOperationException("Port is not open.");
|
||||||
|
|
||||||
|
var chunk = ChunkSize > 0 ? ChunkSize : DefaultChunkSize;
|
||||||
|
var offset = 0;
|
||||||
|
|
||||||
|
while (offset < data.Length)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var count = System.Math.Min(chunk, data.Length - offset);
|
||||||
|
port.Write(data, offset, count);
|
||||||
|
offset += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block until the OS has handed the last bytes to the line. SerialPort
|
||||||
|
// doesn't expose flush-and-drain directly; BaseStream.Flush is a no-op
|
||||||
|
// on Windows, so this is best-effort.
|
||||||
|
try { port.BaseStream.Flush(); }
|
||||||
|
catch { /* ignored — Flush is advisory on SerialPort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
if (port == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (port.IsOpen) port.Close();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
port.Dispose();
|
||||||
|
port = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.GravographIS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IPostProcessor implementation for the Gravograph IS8000. <see cref="Post(Nest, Stream)"/>
|
||||||
|
/// writes the binary HPGL bytes. For serial streaming, use <see cref="Stream(Nest, string, Handshake, CancellationToken)"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GravographISPostProcessor : IPostProcessor
|
||||||
|
{
|
||||||
|
public string Name => "Gravograph IS8000";
|
||||||
|
public string Author => "OpenNest";
|
||||||
|
public string Description => "Gravograph IS8000 mechanical engraver (binary HPGL over serial)";
|
||||||
|
|
||||||
|
public GravographISWriterOptions WriterOptions { get; } = new GravographISWriterOptions();
|
||||||
|
|
||||||
|
public NestPolylineExtractor Extractor { get; } = new NestPolylineExtractor();
|
||||||
|
|
||||||
|
public double StitchTolerance { get; set; } = PolylinePrePass.DefaultStitchTolerance;
|
||||||
|
|
||||||
|
public bool AllowReverse { get; set; } = true;
|
||||||
|
|
||||||
|
public void Post(Nest nest, Stream outputStream)
|
||||||
|
{
|
||||||
|
if (nest == null) throw new ArgumentNullException(nameof(nest));
|
||||||
|
if (outputStream == null) throw new ArgumentNullException(nameof(outputStream));
|
||||||
|
|
||||||
|
var polylines = Extractor.Extract(nest);
|
||||||
|
var prepared = PolylinePrePass.Prepare(polylines, StitchTolerance, AllowReverse);
|
||||||
|
new GravographISWriter(WriterOptions).Write(prepared, outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Post(Nest nest, string outputFile)
|
||||||
|
{
|
||||||
|
using var fs = new FileStream(outputFile, FileMode.Create, FileAccess.Write);
|
||||||
|
Post(nest, fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Buffers the encoded job in memory, then streams it to the named COM port.
|
||||||
|
/// </summary>
|
||||||
|
public void Stream(Nest nest, string portName,
|
||||||
|
Handshake handshake = Handshake.RequestToSend,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
byte[] bytes;
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
Post(nest, ms);
|
||||||
|
bytes = ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var port = new GravographISPort();
|
||||||
|
port.Open(portName, handshake);
|
||||||
|
port.StreamJob(bytes, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.GravographIS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes polylines (in inches) into the Gravograph IS8000 native "binary HPGL"
|
||||||
|
/// wire format. The byte stream is byte-exact against captures from GravoStyle'98.
|
||||||
|
///
|
||||||
|
/// Scale: 80 steps/mm = 2032 steps/inch. Y (and Z) are negated on the wire.
|
||||||
|
/// Deltas are signed big-endian int16 (max ±32767 steps ≈ ±16 inches per move).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GravographISWriter
|
||||||
|
{
|
||||||
|
// 93-byte preamble — captured from GravoStyle'98 with the trailing
|
||||||
|
// job-specific travel block stripped. The VS, VZ and DZ operands are
|
||||||
|
// patched by the writer to reflect feed and depth options.
|
||||||
|
//
|
||||||
|
// The original capture ended with a DR command (FF FD 44 52 00 00)
|
||||||
|
// followed by three 8-byte int16 records — same format as PU/PD —
|
||||||
|
// that carried a chunked travel from the head's parked position to
|
||||||
|
// the original job's first vertex (cumulative ΔX ≈ 1", ΔY ≈ 47").
|
||||||
|
// Those frozen deltas have nothing to do with our job geometry, so
|
||||||
|
// replaying them sends the head to a fixed point regardless of where
|
||||||
|
// the operator set zero. Stripped for the same reason as the captured
|
||||||
|
// fixed return-to-home block.
|
||||||
|
private static readonly byte[] PreambleTemplate = new byte[]
|
||||||
|
{
|
||||||
|
0x21, 0x41, 0x53, 0x20, 0x33, 0x38, 0x3b, 0x01, 0x90, 0x01,
|
||||||
|
0xf4, 0x01, 0x90, 0x01, 0xf4, 0x01, 0x90, 0x01, 0xf4, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x09, 0x00, 0x00, 0x03, 0xe8, 0x05, 0x06, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfd, 0x32, 0x44, 0x00,
|
||||||
|
0x00, 0xff, 0xfd, 0x4d, 0x43, 0x00, 0x01, 0xff, 0xfd, 0x4f,
|
||||||
|
0x55, 0xff, 0xfb, 0xff, 0xfd, 0x4f, 0x55, 0xff, 0xfa, 0xff,
|
||||||
|
0xfd, 0x50, 0x5a, 0x00, 0x00, 0xff, 0xfd, 0x56, 0x53, 0x00,
|
||||||
|
0x23, 0xff, 0xfd, 0x56, 0x5a, 0x00, 0x23, 0xff, 0xfd, 0x44,
|
||||||
|
0x5a, 0x01, 0xfc,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stripped 36-byte postamble: lift, aux off, motor off, operator beep,
|
||||||
|
// job-finish. The 24-byte return-to-home block that appears in GravoStyle's
|
||||||
|
// captured postamble between MC and OP is intentionally OMITTED — those
|
||||||
|
// three 8-byte int16 records carry chunked job-specific return deltas
|
||||||
|
// (each record is [word1:int16][param:int16][ΔX:int16][ΔY:int16], same
|
||||||
|
// format as PU/PD records; the original capture chunked the long Y return
|
||||||
|
// across three records because each delta has to fit in int16). Reusing
|
||||||
|
// GravoStyle's frozen deltas on different geometry overshoots the X-axis
|
||||||
|
// limit. We emit calculated return deltas for the current job instead.
|
||||||
|
// The writer now replaces the captured fixed return block with a calculated
|
||||||
|
// lift + PU travel to the operator-set origin before these final commands.
|
||||||
|
private static readonly byte[] EndJobBytes = new byte[]
|
||||||
|
{
|
||||||
|
0xff, 0xfd, 0x4f, 0x55, 0xff, 0xfa, // OU 0xFFFA aux off
|
||||||
|
0xff, 0xfd, 0x4f, 0x55, 0xff, 0xfb, // OU 0xFFFB aux off
|
||||||
|
0xff, 0xfd, 0x4d, 0x43, 0x00, 0x00, // MC 0x0000 motor off
|
||||||
|
0xff, 0xfd, 0x4f, 0x50, 0x00, 0x00, // OP 0x0000 operator beep
|
||||||
|
0xff, 0xfd, 0x4a, 0x46, 0x00, 0x00, // JF 0x0000 job finish
|
||||||
|
};
|
||||||
|
|
||||||
|
// 80 steps/mm × 25.4 mm/in
|
||||||
|
internal const int StepsPerInch = 2032;
|
||||||
|
|
||||||
|
public GravographISWriterOptions Options { get; }
|
||||||
|
|
||||||
|
public GravographISWriter()
|
||||||
|
: this(new GravographISWriterOptions())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public GravographISWriter(GravographISWriterOptions options)
|
||||||
|
{
|
||||||
|
Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the full byte stream (preamble + geometry + postamble) for the given
|
||||||
|
/// polylines. Polyline coordinates are in inches, relative to the operator-set
|
||||||
|
/// work origin. The writer emits a leading DR travel to the first polyline
|
||||||
|
/// start before lowering for the first cut.
|
||||||
|
/// </summary>
|
||||||
|
public void Write(IEnumerable<IReadOnlyList<Vector>> polylines, Stream output)
|
||||||
|
{
|
||||||
|
if (polylines == null) throw new ArgumentNullException(nameof(polylines));
|
||||||
|
if (output == null) throw new ArgumentNullException(nameof(output));
|
||||||
|
|
||||||
|
var preamble = (byte[])PreambleTemplate.Clone();
|
||||||
|
PatchOperand(preamble, (byte)'V', (byte)'S', (short)Options.FeedMmPerSec);
|
||||||
|
PatchOperand(preamble, (byte)'V', (byte)'Z', (short)Options.FeedMmPerSec);
|
||||||
|
PatchOperand(preamble, (byte)'D', (byte)'Z', DepthInStepsAsInt16());
|
||||||
|
output.Write(preamble, 0, preamble.Length);
|
||||||
|
|
||||||
|
// Cumulative head position from the operator-set upper-left origin, in
|
||||||
|
// wire steps. The first polyline gets a leading DR travel from this
|
||||||
|
// origin before PD lowers for cutting. Used by the envelope guard to
|
||||||
|
// catch bad records before they ship to the engraver.
|
||||||
|
var headX = 0;
|
||||||
|
var headY = 0;
|
||||||
|
var envelopeXSteps = (int)System.Math.Round(Options.WorkEnvelopeXMm * StepsPerMm,
|
||||||
|
MidpointRounding.AwayFromZero);
|
||||||
|
var envelopeYSteps = (int)System.Math.Round(Options.WorkEnvelopeYMm * StepsPerMm,
|
||||||
|
MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
var firstPolyline = true;
|
||||||
|
var polyIndex = 0;
|
||||||
|
|
||||||
|
foreach (var poly in polylines)
|
||||||
|
{
|
||||||
|
polyIndex++;
|
||||||
|
if (poly == null || poly.Count < 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var (startX, startY) = ToWire(poly[0]);
|
||||||
|
WriteTravel(output,
|
||||||
|
firstPolyline ? (byte)'D' : (byte)'P',
|
||||||
|
firstPolyline ? (byte)'R' : (byte)'U',
|
||||||
|
checked(startX - headX), checked(startY - headY),
|
||||||
|
ref headX, ref headY, envelopeXSteps, envelopeYSteps, polyIndex);
|
||||||
|
|
||||||
|
// PD command + single records-follow flag, then one record per segment.
|
||||||
|
output.WriteByte(0xFF);
|
||||||
|
output.WriteByte(0xFD);
|
||||||
|
output.WriteByte((byte)'P');
|
||||||
|
output.WriteByte((byte)'D');
|
||||||
|
output.WriteByte(0x00);
|
||||||
|
output.WriteByte(0x00);
|
||||||
|
|
||||||
|
var prevX = startX;
|
||||||
|
var prevY = startY;
|
||||||
|
for (int i = 1; i < poly.Count; i++)
|
||||||
|
{
|
||||||
|
var (cx, cy) = ToWire(poly[i]);
|
||||||
|
var dx = checked(cx - prevX);
|
||||||
|
var dy = checked(cy - prevY);
|
||||||
|
EnsureEnvelope(headX + dx, headY + dy, envelopeXSteps, envelopeYSteps,
|
||||||
|
polyIndex, segment: i, isTravel: false);
|
||||||
|
WriteRecord(output, dx, dy);
|
||||||
|
prevX = cx;
|
||||||
|
prevY = cy;
|
||||||
|
headX += dx;
|
||||||
|
headY += dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstPolyline = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLiftOnly(output);
|
||||||
|
if (Options.ReturnToOriginAtEnd && !firstPolyline)
|
||||||
|
{
|
||||||
|
WriteTravel(output, (byte)'P', (byte)'U',
|
||||||
|
checked(-headX), checked(-headY),
|
||||||
|
ref headX, ref headY, envelopeXSteps, envelopeYSteps, polyIndex);
|
||||||
|
}
|
||||||
|
output.Write(EndJobBytes, 0, EndJobBytes.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const double StepsPerMm = 80.0;
|
||||||
|
|
||||||
|
private void EnsureEnvelope(int wireX, int wireY,
|
||||||
|
int envXSteps, int envYSteps,
|
||||||
|
int polyIndex, int segment, bool isTravel)
|
||||||
|
{
|
||||||
|
if (!Options.EnvelopeGuardEnabled) return;
|
||||||
|
|
||||||
|
// Wire frame: X is identity to input; Y is negated. With the operator
|
||||||
|
// origin set at the upper-left of the work envelope and an OpenNest
|
||||||
|
// quadrant-4 plate, valid part coordinates are +X/right and -Y/down:
|
||||||
|
// wireX ∈ [0, +envXSteps]
|
||||||
|
// wireY ∈ [0, +envYSteps]
|
||||||
|
if (wireX >= 0 && wireX <= envXSteps && wireY >= 0 && wireY <= envYSteps)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var inputX = wireX / (double)StepsPerInch;
|
||||||
|
var inputY = -wireY / (double)StepsPerInch;
|
||||||
|
var kind = isTravel ? "pen-up travel" : "cut segment";
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Polyline {polyIndex} {kind} (segment {segment}) would place the head at " +
|
||||||
|
$"({inputX:F3}\", {inputY:F3}\"), outside the {Options.WorkEnvelopeXMm}×{Options.WorkEnvelopeYMm} mm " +
|
||||||
|
$"work envelope from upper-left origin. Refusing to emit the record.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private short DepthInStepsAsInt16()
|
||||||
|
{
|
||||||
|
var steps = (long)System.Math.Round(Options.DepthInches * StepsPerInch, MidpointRounding.AwayFromZero);
|
||||||
|
if (steps < short.MinValue || steps > short.MaxValue)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(Options.DepthInches), $"Depth {Options.DepthInches} in. → {steps} steps overflows int16.");
|
||||||
|
return (short)steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int x, int y) ToWire(Vector v)
|
||||||
|
{
|
||||||
|
// Inches -> steps. With upper-left origin in OpenNest quadrant 4,
|
||||||
|
// negative input Y is down; Y is negated on the wire.
|
||||||
|
var x = (int)System.Math.Round(v.X * StepsPerInch, MidpointRounding.AwayFromZero);
|
||||||
|
var y = (int)System.Math.Round(-v.Y * StepsPerInch, MidpointRounding.AwayFromZero);
|
||||||
|
return (x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteTravel(Stream s, byte c0, byte c1, int dx, int dy,
|
||||||
|
ref int headX, ref int headY,
|
||||||
|
int envelopeXSteps, int envelopeYSteps,
|
||||||
|
int polyIndex)
|
||||||
|
{
|
||||||
|
if (dx == 0 && dy == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
s.WriteByte(0xFF);
|
||||||
|
s.WriteByte(0xFD);
|
||||||
|
s.WriteByte(c0);
|
||||||
|
s.WriteByte(c1);
|
||||||
|
s.WriteByte(0x00);
|
||||||
|
s.WriteByte(0x00);
|
||||||
|
|
||||||
|
var chunks = System.Math.Max(
|
||||||
|
(int)System.Math.Ceiling(System.Math.Abs(dx) / (double)short.MaxValue),
|
||||||
|
(int)System.Math.Ceiling(System.Math.Abs(dy) / (double)short.MaxValue));
|
||||||
|
if (chunks < 1) chunks = 1;
|
||||||
|
|
||||||
|
var emittedX = 0;
|
||||||
|
var emittedY = 0;
|
||||||
|
for (var i = 1; i <= chunks; i++)
|
||||||
|
{
|
||||||
|
var targetX = (int)System.Math.Round(dx * (i / (double)chunks), MidpointRounding.AwayFromZero);
|
||||||
|
var targetY = (int)System.Math.Round(dy * (i / (double)chunks), MidpointRounding.AwayFromZero);
|
||||||
|
var chunkX = checked(targetX - emittedX);
|
||||||
|
var chunkY = checked(targetY - emittedY);
|
||||||
|
|
||||||
|
EnsureEnvelope(headX + chunkX, headY + chunkY, envelopeXSteps, envelopeYSteps,
|
||||||
|
polyIndex, segment: 0, isTravel: true);
|
||||||
|
WriteRecord(s, chunkX, chunkY);
|
||||||
|
|
||||||
|
emittedX = targetX;
|
||||||
|
emittedY = targetY;
|
||||||
|
headX += chunkX;
|
||||||
|
headY += chunkY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteLiftOnly(Stream s)
|
||||||
|
{
|
||||||
|
s.WriteByte(0xFF);
|
||||||
|
s.WriteByte(0xFD);
|
||||||
|
s.WriteByte((byte)'P');
|
||||||
|
s.WriteByte((byte)'U');
|
||||||
|
s.WriteByte(0x00);
|
||||||
|
s.WriteByte(0x01);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteCommandWithRecord(Stream s, byte c0, byte c1, int dx, int dy)
|
||||||
|
{
|
||||||
|
s.WriteByte(0xFF);
|
||||||
|
s.WriteByte(0xFD);
|
||||||
|
s.WriteByte(c0);
|
||||||
|
s.WriteByte(c1);
|
||||||
|
// Records-follow flag (0x0000) emitted once per PU/PD packet.
|
||||||
|
s.WriteByte(0x00);
|
||||||
|
s.WriteByte(0x00);
|
||||||
|
WriteRecord(s, dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteRecord(Stream s, int dx, int dy)
|
||||||
|
{
|
||||||
|
if (dx < short.MinValue || dx > short.MaxValue ||
|
||||||
|
dy < short.MinValue || dy > short.MaxValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Move delta ({dx}, {dy}) steps overflows signed int16 — split moves upstream.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int word1;
|
||||||
|
int param;
|
||||||
|
|
||||||
|
var absDx = (double)System.Math.Abs(dx);
|
||||||
|
var absDy = (double)System.Math.Abs(dy);
|
||||||
|
var len = System.Math.Sqrt(absDx * absDx + absDy * absDy);
|
||||||
|
|
||||||
|
if (len < 1.0)
|
||||||
|
{
|
||||||
|
// Zero-length lift (PU 00 01) is the dedicated form; for a record-carrying
|
||||||
|
// packet a true zero-length move shouldn't occur, but stay numerically safe.
|
||||||
|
word1 = 16384;
|
||||||
|
param = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var maxAbs = System.Math.Max(absDx, absDy);
|
||||||
|
word1 = (int)System.Math.Round(16384.0 * maxAbs / len, MidpointRounding.AwayFromZero);
|
||||||
|
param = (int)System.Math.Round(len / 22.4, MidpointRounding.AwayFromZero);
|
||||||
|
if (param < 1) param = 1;
|
||||||
|
if (param > 180) param = 180;
|
||||||
|
if (word1 > 16384) word1 = 16384;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteBigEndianInt16(s, (short)word1);
|
||||||
|
WriteBigEndianInt16(s, (short)param);
|
||||||
|
WriteBigEndianInt16(s, (short)dx);
|
||||||
|
WriteBigEndianInt16(s, (short)dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteBigEndianInt16(Stream s, short value)
|
||||||
|
{
|
||||||
|
s.WriteByte((byte)((value >> 8) & 0xFF));
|
||||||
|
s.WriteByte((byte)(value & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locates the operand of a command (FF FD <c0> <c1> <hi> <lo>) and overwrites it.
|
||||||
|
// Throws if the command isn't present — that would mean the preamble was mis-edited.
|
||||||
|
private static void PatchOperand(byte[] buffer, byte c0, byte c1, short value)
|
||||||
|
{
|
||||||
|
for (int i = 0; i <= buffer.Length - 6; i++)
|
||||||
|
{
|
||||||
|
if (buffer[i] == 0xFF && buffer[i + 1] == 0xFD &&
|
||||||
|
buffer[i + 2] == c0 && buffer[i + 3] == c1)
|
||||||
|
{
|
||||||
|
buffer[i + 4] = (byte)((value >> 8) & 0xFF);
|
||||||
|
buffer[i + 5] = (byte)(value & 0xFF);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Command '{(char)c0}{(char)c1}' not found in preamble template.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace OpenNest.Posts.GravographIS
|
||||||
|
{
|
||||||
|
public sealed class GravographISWriterOptions
|
||||||
|
{
|
||||||
|
public double DepthInches { get; set; } = 0.25;
|
||||||
|
|
||||||
|
public int FeedMmPerSec { get; set; } = 35;
|
||||||
|
|
||||||
|
// IS8000 work envelope in millimeters, from the operator-set upper-left
|
||||||
|
// work origin. Defaults to the catalog 0.610 m x 1.220 m bed. With an
|
||||||
|
// OpenNest quadrant-4 plate, motion is allowed right (+X) and down (-Y).
|
||||||
|
public double WorkEnvelopeXMm { get; set; } = 610.0;
|
||||||
|
public double WorkEnvelopeYMm { get; set; } = 1220.0;
|
||||||
|
|
||||||
|
// When true, the writer throws an InvalidOperationException naming the
|
||||||
|
// offending polyline and segment before any out-of-envelope record is
|
||||||
|
// emitted. Disable only for off-machine encoding tests.
|
||||||
|
public bool EnvelopeGuardEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
// When true, lift at the end of the last cut and return to the
|
||||||
|
// operator-set origin before shutting the job down.
|
||||||
|
public bool ReturnToOriginAtEnd { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.GravographIS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lifts polylines out of an OpenNest <see cref="Nest"/> for the Gravograph
|
||||||
|
/// backend. Walks each <see cref="Part"/>'s <see cref="Program"/>, breaks
|
||||||
|
/// polylines at rapid moves, and tessellates arcs to a chord-deviation
|
||||||
|
/// tolerance (the wire format takes line segments only).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NestPolylineExtractor
|
||||||
|
{
|
||||||
|
public double ArcChordToleranceInches { get; set; } = 0.001;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts polylines from every non-cutoff part in every plate of the nest,
|
||||||
|
/// returning them in plate coordinates (inches).
|
||||||
|
/// </summary>
|
||||||
|
public List<List<Vector>> Extract(Nest nest)
|
||||||
|
{
|
||||||
|
if (nest == null) throw new ArgumentNullException(nameof(nest));
|
||||||
|
|
||||||
|
var result = new List<List<Vector>>();
|
||||||
|
|
||||||
|
foreach (var plate in nest.Plates)
|
||||||
|
{
|
||||||
|
foreach (var part in plate.Parts)
|
||||||
|
{
|
||||||
|
if (part.BaseDrawing != null && part.BaseDrawing.IsCutOff)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ExtractPart(part, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts polylines for a single part. Public so callers driving the
|
||||||
|
/// writer directly (e.g. from a console one-off) can use it.
|
||||||
|
/// </summary>
|
||||||
|
public List<List<Vector>> ExtractPart(Part part)
|
||||||
|
{
|
||||||
|
var list = new List<List<Vector>>();
|
||||||
|
ExtractPart(part, list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExtractPart(Part part, List<List<Vector>> sink)
|
||||||
|
{
|
||||||
|
var program = part.Program;
|
||||||
|
if (program == null) return;
|
||||||
|
|
||||||
|
// The walk below treats Motion.EndPoint as absolute. Convert a working
|
||||||
|
// copy to absolute mode so G91 programs (the form OpenNest's UI writes)
|
||||||
|
// produce correct geometry. Cloning keeps part.Program untouched.
|
||||||
|
if (program.Mode == Mode.Incremental)
|
||||||
|
{
|
||||||
|
program = (Program)program.Clone();
|
||||||
|
program.Mode = Mode.Absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = part.Location;
|
||||||
|
var pos = new Vector(0, 0);
|
||||||
|
List<Vector> current = null;
|
||||||
|
|
||||||
|
foreach (var code in program.Codes)
|
||||||
|
{
|
||||||
|
if (code is Motion m && m.Suppressed)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case RapidMove rapid:
|
||||||
|
{
|
||||||
|
FlushCurrent(sink, ref current);
|
||||||
|
pos = rapid.EndPoint;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LinearMove linear:
|
||||||
|
{
|
||||||
|
if (current == null)
|
||||||
|
{
|
||||||
|
current = new List<Vector> { pos + offset };
|
||||||
|
}
|
||||||
|
var end = linear.EndPoint;
|
||||||
|
current.Add(end + offset);
|
||||||
|
pos = end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ArcMove arc:
|
||||||
|
{
|
||||||
|
if (current == null)
|
||||||
|
{
|
||||||
|
current = new List<Vector> { pos + offset };
|
||||||
|
}
|
||||||
|
TessellateArc(pos, arc, offset, ArcChordToleranceInches, current);
|
||||||
|
pos = arc.EndPoint;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushCurrent(sink, ref current);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FlushCurrent(List<List<Vector>> sink, ref List<Vector> current)
|
||||||
|
{
|
||||||
|
if (current != null && current.Count >= 2)
|
||||||
|
sink.Add(current);
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample points along an arc to within chordTol of the true curve. start is
|
||||||
|
// the arc's start point (current pen position), arc.CenterPoint is absolute
|
||||||
|
// (G-code I/J in this codebase are stored as the absolute center), arc.EndPoint
|
||||||
|
// is absolute end. The starting point is assumed to already be in the polyline;
|
||||||
|
// intermediate samples and the endpoint are appended.
|
||||||
|
private static void TessellateArc(Vector start, ArcMove arc, Vector offset,
|
||||||
|
double chordTol, List<Vector> sink)
|
||||||
|
{
|
||||||
|
var c = arc.CenterPoint;
|
||||||
|
var r = c.DistanceTo(start);
|
||||||
|
if (r < 1e-9)
|
||||||
|
{
|
||||||
|
sink.Add(arc.EndPoint + offset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var a0 = System.Math.Atan2(start.Y - c.Y, start.X - c.X);
|
||||||
|
var a1 = System.Math.Atan2(arc.EndPoint.Y - c.Y, arc.EndPoint.X - c.X);
|
||||||
|
|
||||||
|
double sweep;
|
||||||
|
if (arc.Rotation == RotationType.CW)
|
||||||
|
{
|
||||||
|
sweep = a0 - a1;
|
||||||
|
if (sweep <= 0) sweep += 2 * System.Math.PI;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sweep = a1 - a0;
|
||||||
|
if (sweep <= 0) sweep += 2 * System.Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat a near-zero sweep with coincident start/end as a full circle.
|
||||||
|
if (sweep < 1e-9 &&
|
||||||
|
System.Math.Abs(start.X - arc.EndPoint.X) < 1e-9 &&
|
||||||
|
System.Math.Abs(start.Y - arc.EndPoint.Y) < 1e-9)
|
||||||
|
{
|
||||||
|
sweep = 2 * System.Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max angle step from chord-deviation tolerance: dev = r * (1 - cos(t/2)).
|
||||||
|
var maxAngleStep = 2.0 * System.Math.Acos(System.Math.Max(0.0, 1.0 - chordTol / r));
|
||||||
|
if (double.IsNaN(maxAngleStep) || maxAngleStep <= 0)
|
||||||
|
maxAngleStep = System.Math.PI / 32;
|
||||||
|
|
||||||
|
var steps = (int)System.Math.Ceiling(sweep / maxAngleStep);
|
||||||
|
if (steps < 1) steps = 1;
|
||||||
|
|
||||||
|
var direction = arc.Rotation == RotationType.CW ? -1.0 : 1.0;
|
||||||
|
for (int i = 1; i < steps; i++)
|
||||||
|
{
|
||||||
|
var t = sweep * (i / (double)steps);
|
||||||
|
var ang = a0 + direction * t;
|
||||||
|
var pt = new Vector(c.X + r * System.Math.Cos(ang), c.Y + r * System.Math.Sin(ang));
|
||||||
|
sink.Add(pt + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
sink.Add(arc.EndPoint + offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<RootNamespace>OpenNest.Posts.GravographIS</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="OpenNest.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Target Name="CopyToPostsDir" AfterTargets="Build">
|
||||||
|
<PropertyGroup>
|
||||||
|
<PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
<MakeDir Directories="$(PostsDir)" />
|
||||||
|
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" />
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.GravographIS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Geometry pre-pass for the Gravograph IS8000 backend. The machine is a dumb
|
||||||
|
/// executor — it never reorders geometry and always lifts between separate
|
||||||
|
/// entities — so we stitch shared-endpoint polylines together and reorder by
|
||||||
|
/// nearest-neighbor before encoding.
|
||||||
|
/// </summary>
|
||||||
|
public static class PolylinePrePass
|
||||||
|
{
|
||||||
|
public const double DefaultStitchTolerance = 1e-6;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Joins polylines whose endpoints coincide (within <paramref name="tolerance"/>)
|
||||||
|
/// into single continuous polylines. Polylines with fewer than two points are
|
||||||
|
/// dropped. Direction is reversed as needed to make a join. Each input polyline
|
||||||
|
/// is copied — the inputs are not mutated.
|
||||||
|
/// </summary>
|
||||||
|
public static List<List<Vector>> Stitch(
|
||||||
|
IEnumerable<IReadOnlyList<Vector>> polylines,
|
||||||
|
double tolerance = DefaultStitchTolerance)
|
||||||
|
{
|
||||||
|
if (polylines == null) throw new ArgumentNullException(nameof(polylines));
|
||||||
|
|
||||||
|
var segs = new List<List<Vector>>();
|
||||||
|
foreach (var p in polylines)
|
||||||
|
{
|
||||||
|
if (p == null || p.Count < 2)
|
||||||
|
continue;
|
||||||
|
segs.Add(new List<Vector>(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
changed = false;
|
||||||
|
for (int i = 0; i < segs.Count; i++)
|
||||||
|
{
|
||||||
|
var a = segs[i];
|
||||||
|
|
||||||
|
for (int j = 0; j < segs.Count; j++)
|
||||||
|
{
|
||||||
|
if (i == j) continue;
|
||||||
|
var b = segs[j];
|
||||||
|
|
||||||
|
// a-end ↔ b-start: append b to a (skip duplicated joint)
|
||||||
|
if (Near(a[a.Count - 1], b[0], tolerance))
|
||||||
|
{
|
||||||
|
for (int k = 1; k < b.Count; k++) a.Add(b[k]);
|
||||||
|
segs.RemoveAt(j);
|
||||||
|
if (j < i) i--;
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a-end ↔ b-end: append reversed b to a
|
||||||
|
if (Near(a[a.Count - 1], b[b.Count - 1], tolerance))
|
||||||
|
{
|
||||||
|
for (int k = b.Count - 2; k >= 0; k--) a.Add(b[k]);
|
||||||
|
segs.RemoveAt(j);
|
||||||
|
if (j < i) i--;
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a-start ↔ b-end: prepend b to a
|
||||||
|
if (Near(a[0], b[b.Count - 1], tolerance))
|
||||||
|
{
|
||||||
|
var combined = new List<Vector>(b.Count + a.Count - 1);
|
||||||
|
combined.AddRange(b);
|
||||||
|
for (int k = 1; k < a.Count; k++) combined.Add(a[k]);
|
||||||
|
segs[i] = combined;
|
||||||
|
segs.RemoveAt(j);
|
||||||
|
if (j < i) i--;
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a-start ↔ b-start: prepend reversed b to a
|
||||||
|
if (Near(a[0], b[0], tolerance))
|
||||||
|
{
|
||||||
|
var combined = new List<Vector>(b.Count + a.Count - 1);
|
||||||
|
for (int k = b.Count - 1; k >= 0; k--) combined.Add(b[k]);
|
||||||
|
for (int k = 1; k < a.Count; k++) combined.Add(a[k]);
|
||||||
|
segs[i] = combined;
|
||||||
|
segs.RemoveAt(j);
|
||||||
|
if (j < i) i--;
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (changed);
|
||||||
|
|
||||||
|
return segs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Greedy nearest-neighbor ordering of polylines starting from
|
||||||
|
/// <paramref name="origin"/> (defaults to 0,0 = the work origin = the first
|
||||||
|
/// polyline's first point on the wire). When <paramref name="allowReverse"/>
|
||||||
|
/// is true a polyline may be reversed if its tail is closer than its head.
|
||||||
|
/// </summary>
|
||||||
|
public static List<List<Vector>> Reorder(
|
||||||
|
IEnumerable<IReadOnlyList<Vector>> polylines,
|
||||||
|
bool allowReverse = true,
|
||||||
|
Vector? origin = null)
|
||||||
|
{
|
||||||
|
if (polylines == null) throw new ArgumentNullException(nameof(polylines));
|
||||||
|
|
||||||
|
var pool = new List<List<Vector>>();
|
||||||
|
foreach (var p in polylines)
|
||||||
|
{
|
||||||
|
if (p == null || p.Count < 2)
|
||||||
|
continue;
|
||||||
|
pool.Add(new List<Vector>(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
var ordered = new List<List<Vector>>(pool.Count);
|
||||||
|
var current = origin ?? new Vector(0, 0);
|
||||||
|
|
||||||
|
while (pool.Count > 0)
|
||||||
|
{
|
||||||
|
var bestIdx = -1;
|
||||||
|
var bestReverse = false;
|
||||||
|
var bestDistSq = double.PositiveInfinity;
|
||||||
|
|
||||||
|
for (int i = 0; i < pool.Count; i++)
|
||||||
|
{
|
||||||
|
var p = pool[i];
|
||||||
|
var dHead = SquaredDistance(current, p[0]);
|
||||||
|
if (dHead < bestDistSq)
|
||||||
|
{
|
||||||
|
bestDistSq = dHead;
|
||||||
|
bestIdx = i;
|
||||||
|
bestReverse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowReverse)
|
||||||
|
{
|
||||||
|
var dTail = SquaredDistance(current, p[p.Count - 1]);
|
||||||
|
if (dTail < bestDistSq)
|
||||||
|
{
|
||||||
|
bestDistSq = dTail;
|
||||||
|
bestIdx = i;
|
||||||
|
bestReverse = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pick = pool[bestIdx];
|
||||||
|
pool.RemoveAt(bestIdx);
|
||||||
|
if (bestReverse)
|
||||||
|
pick.Reverse();
|
||||||
|
ordered.Add(pick);
|
||||||
|
current = pick[pick.Count - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convenience: stitch then reorder.
|
||||||
|
/// </summary>
|
||||||
|
public static List<List<Vector>> Prepare(
|
||||||
|
IEnumerable<IReadOnlyList<Vector>> polylines,
|
||||||
|
double stitchTolerance = DefaultStitchTolerance,
|
||||||
|
bool allowReverse = true,
|
||||||
|
Vector? origin = null)
|
||||||
|
{
|
||||||
|
var stitched = Stitch(polylines, stitchTolerance);
|
||||||
|
return Reorder(stitched, allowReverse, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Near(Vector a, Vector b, double tol)
|
||||||
|
{
|
||||||
|
var dx = a.X - b.X;
|
||||||
|
var dy = a.Y - b.Y;
|
||||||
|
return (dx * dx + dy * dy) <= tol * tol;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double SquaredDistance(Vector a, Vector b)
|
||||||
|
{
|
||||||
|
var dx = a.X - b.X;
|
||||||
|
var dy = a.Y - b.Y;
|
||||||
|
return dx * dx + dy * dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using OpenNest.Shapes;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.BestFit;
|
||||||
|
|
||||||
|
public class BestFitResultFrameTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BuildCanonicalParts_NonAxisAlignedPairNormalizesActualBounds()
|
||||||
|
{
|
||||||
|
var drawing = new TShape { Width = 10, Height = 8 }.GetDrawing();
|
||||||
|
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
|
||||||
|
|
||||||
|
var result = EvaluateOffsetPair(canonical, new Vector(40, 30));
|
||||||
|
|
||||||
|
Assert.True(IsNonAxisAligned(result.OptimalRotation),
|
||||||
|
$"Expected a non-axis-aligned result, got {Angle.ToDegrees(result.OptimalRotation):F2} degrees.");
|
||||||
|
|
||||||
|
var parts = result.BuildCanonicalParts();
|
||||||
|
var bounds = result.GetCutBounds(parts);
|
||||||
|
|
||||||
|
Assert.Equal(0, bounds.Left, 3);
|
||||||
|
Assert.Equal(0, bounds.Bottom, 3);
|
||||||
|
Assert.Equal(result.BoundingWidth, bounds.Length, 2);
|
||||||
|
Assert.Equal(result.BoundingHeight, bounds.Width, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildSourceParts_RebindsCanonicalResultToRotatedSourceDrawing()
|
||||||
|
{
|
||||||
|
var drawing = new TShape { Width = 10, Height = 8 }.GetDrawing();
|
||||||
|
drawing.Program.Rotate(Angle.ToRadians(30), drawing.Program.BoundingBox().Center);
|
||||||
|
drawing.RecomputeCanonicalAngle();
|
||||||
|
|
||||||
|
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
|
||||||
|
var result = EvaluateOffsetPair(canonical, new Vector(40, 30));
|
||||||
|
|
||||||
|
var parts = result.BuildSourceParts(drawing);
|
||||||
|
var bounds = result.GetCutBounds(parts);
|
||||||
|
|
||||||
|
Assert.All(parts, p => Assert.Same(drawing, p.BaseDrawing));
|
||||||
|
Assert.Equal(0, bounds.Left, 3);
|
||||||
|
Assert.Equal(0, bounds.Bottom, 3);
|
||||||
|
Assert.False(parts[0].Intersects(parts[1], out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BestFitResult EvaluateOffsetPair(Drawing drawing, Vector offset)
|
||||||
|
{
|
||||||
|
var candidate = new PairCandidate
|
||||||
|
{
|
||||||
|
Drawing = drawing,
|
||||||
|
Part1Rotation = 0,
|
||||||
|
Part2Rotation = System.Math.PI,
|
||||||
|
Part2Offset = offset,
|
||||||
|
Spacing = 0.25
|
||||||
|
};
|
||||||
|
|
||||||
|
return new PairEvaluator().Evaluate(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNonAxisAligned(double angle)
|
||||||
|
{
|
||||||
|
var normalized = Angle.NormalizeRad(angle);
|
||||||
|
var nearestQuadrant = Angle.HalfPI * System.Math.Round(normalized / Angle.HalfPI);
|
||||||
|
var delta = System.Math.Abs(normalized - nearestQuadrant);
|
||||||
|
delta = System.Math.Min(delta, Angle.HalfPI - delta);
|
||||||
|
return delta > Angle.ToRadians(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.CNC
|
||||||
|
{
|
||||||
|
public class RapidEnumeratorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Enumerate_AbsoluteProgram_OffsetsMotionsByBasePos()
|
||||||
|
{
|
||||||
|
var pgm = new Program(Mode.Absolute);
|
||||||
|
pgm.Codes.Add(new RapidMove(1, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(2, 0));
|
||||||
|
pgm.Codes.Add(new RapidMove(3, 3));
|
||||||
|
|
||||||
|
var segments = RapidEnumerator.Enumerate(pgm, basePos: new Vector(100, 200), startPos: new Vector(0, 0));
|
||||||
|
|
||||||
|
// Origin → first pierce, then interior rapid from contour end to next rapid target.
|
||||||
|
Assert.Equal(2, segments.Count);
|
||||||
|
Assert.Equal(new Vector(0, 0), segments[0].From);
|
||||||
|
Assert.Equal(new Vector(101, 200), segments[0].To);
|
||||||
|
Assert.Equal(new Vector(102, 200), segments[1].From);
|
||||||
|
Assert.Equal(new Vector(103, 203), segments[1].To);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Enumerate_IncrementalProgram_InterpretsDeltasFromBasePos()
|
||||||
|
{
|
||||||
|
// Pre-lead-in raw program: first rapid normalized to (0,0), Mode=Incremental
|
||||||
|
// (matches ConvertGeometry.ToProgram output).
|
||||||
|
var pgm = new Program(Mode.Incremental);
|
||||||
|
pgm.Codes.Add(new RapidMove(0, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(5, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 5));
|
||||||
|
pgm.Codes.Add(new RapidMove(1, 1));
|
||||||
|
|
||||||
|
var segments = RapidEnumerator.Enumerate(pgm, basePos: new Vector(100, 200), startPos: new Vector(0, 0));
|
||||||
|
|
||||||
|
Assert.Equal(2, segments.Count);
|
||||||
|
// First rapid: plate origin → part pierce at basePos.
|
||||||
|
Assert.Equal(new Vector(0, 0), segments[0].From);
|
||||||
|
Assert.Equal(new Vector(100, 200), segments[0].To);
|
||||||
|
// Interior rapid: after deltas (5,0) and (0,5) from basePos, rapid delta (1,1).
|
||||||
|
Assert.Equal(new Vector(105, 205), segments[1].From);
|
||||||
|
Assert.Equal(new Vector(106, 206), segments[1].To);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Enumerate_SubProgramCall_RapidEndsAtAbsoluteHolePierce()
|
||||||
|
{
|
||||||
|
// Main program: lead-in rapid, a line, then a SubProgramCall for a hole.
|
||||||
|
// Sub-program (incremental) starts with RapidMove(radius, 0) to the hole pierce.
|
||||||
|
var sub = new Program(Mode.Incremental);
|
||||||
|
sub.Codes.Add(new RapidMove(0.5, 0));
|
||||||
|
sub.Codes.Add(new LinearMove(0, 0.1));
|
||||||
|
|
||||||
|
var pgm = new Program(Mode.Absolute);
|
||||||
|
pgm.Codes.Add(new RapidMove(0.2, 0.3)); // first pierce (perimeter lead-in)
|
||||||
|
pgm.Codes.Add(new LinearMove(1.0, 1.0)); // contour move
|
||||||
|
pgm.Codes.Add(new SubProgramCall
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Program = sub,
|
||||||
|
Offset = new Vector(2, 2), // hole center (drawing-local)
|
||||||
|
});
|
||||||
|
|
||||||
|
var basePos = new Vector(100, 200); // part.Location
|
||||||
|
var segments = RapidEnumerator.Enumerate(pgm, basePos, startPos: new Vector(0, 0));
|
||||||
|
|
||||||
|
// Expected rapids:
|
||||||
|
// 1. origin → first pierce (0.2+100, 0.3+200) = (100.2, 200.3)
|
||||||
|
// 2. end of contour (1+100, 1+200) = (101, 201) → hole pierce (2+100+0.5, 2+200) = (102.5, 202)
|
||||||
|
// The sub's internal first rapid is skipped (already drawn in #2).
|
||||||
|
Assert.Equal(2, segments.Count);
|
||||||
|
|
||||||
|
Assert.Equal(new Vector(0, 0), segments[0].From);
|
||||||
|
Assert.Equal(new Vector(100.2, 200.3), segments[0].To);
|
||||||
|
|
||||||
|
Assert.Equal(new Vector(101, 201), segments[1].From);
|
||||||
|
Assert.Equal(new Vector(102.5, 202), segments[1].To);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Engine;
|
||||||
|
|
||||||
|
public class CanonicalAngleTests
|
||||||
|
{
|
||||||
|
private const double AngleTol = 0.002; // ~0.11°
|
||||||
|
|
||||||
|
private static Drawing MakeRect(double w, double h)
|
||||||
|
{
|
||||||
|
var pgm = new OpenNest.CNC.Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
return new Drawing("rect", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Drawing RotateCopy(Drawing src, double angle)
|
||||||
|
{
|
||||||
|
var pgm = src.Program.Clone() as OpenNest.CNC.Program;
|
||||||
|
pgm.Rotate(angle, pgm.BoundingBox().Center);
|
||||||
|
return new Drawing("rotated", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AxisAlignedRectangle_ReturnsZero()
|
||||||
|
{
|
||||||
|
var d = MakeRect(100, 50);
|
||||||
|
Assert.Equal(0.0, CanonicalAngle.Compute(d), precision: 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Program.BoundingBox() has a pre-existing bug where minX/minY initialize to 0 and can
|
||||||
|
// only decrease, so programs whose extents stay in the positive half-plane report a
|
||||||
|
// too-large AABB. To validate MBR-axis-alignment without tripping that bug, extract the
|
||||||
|
// outer perimeter polygon and compute its true AABB from vertices.
|
||||||
|
private static (double length, double width) TrueAabb(OpenNest.CNC.Program pgm)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(pgm).Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
var outer = shapes.OrderByDescending(s => s.Area()).First();
|
||||||
|
var poly = outer.ToPolygonWithTolerance(0.1);
|
||||||
|
var minX = poly.Vertices.Min(v => v.X);
|
||||||
|
var maxX = poly.Vertices.Max(v => v.X);
|
||||||
|
var minY = poly.Vertices.Min(v => v.Y);
|
||||||
|
var maxY = poly.Vertices.Max(v => v.Y);
|
||||||
|
return (maxX - minX, maxY - minY);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0.3)]
|
||||||
|
[InlineData(0.7)]
|
||||||
|
[InlineData(1.2)]
|
||||||
|
public void Rectangle_ReturnsNegatedRotation_Modulo90(double theta)
|
||||||
|
{
|
||||||
|
var rotated = RotateCopy(MakeRect(100, 50), theta);
|
||||||
|
var angle = CanonicalAngle.Compute(rotated);
|
||||||
|
|
||||||
|
// Applying the returned angle should leave MBR axis-aligned.
|
||||||
|
var canonical = rotated.Program.Clone() as OpenNest.CNC.Program;
|
||||||
|
canonical.Rotate(angle, canonical.BoundingBox().Center);
|
||||||
|
|
||||||
|
var (length, width) = TrueAabb(canonical);
|
||||||
|
var longer = System.Math.Max(length, width);
|
||||||
|
var shorter = System.Math.Min(length, width);
|
||||||
|
Assert.InRange(longer, 100 - 0.1, 100 + 0.1);
|
||||||
|
Assert.InRange(shorter, 50 - 0.1, 50 + 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NearZeroInput_SnapsToZero()
|
||||||
|
{
|
||||||
|
var rotated = RotateCopy(MakeRect(100, 50), 0.0005);
|
||||||
|
Assert.Equal(0.0, CanonicalAngle.Compute(rotated), precision: 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DegeneratePolygon_ReturnsZero()
|
||||||
|
{
|
||||||
|
var pgm = new OpenNest.CNC.Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||||
|
var d = new Drawing("line", pgm);
|
||||||
|
Assert.Equal(0.0, CanonicalAngle.Compute(d), precision: 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyProgram_ReturnsZero()
|
||||||
|
{
|
||||||
|
var d = new Drawing("empty", new OpenNest.CNC.Program());
|
||||||
|
Assert.Equal(0.0, CanonicalAngle.Compute(d), precision: 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DrawingCanonicalAngleWiringTests
|
||||||
|
{
|
||||||
|
private static OpenNest.CNC.Program RotatedRectProgram(double w, double h, double theta)
|
||||||
|
{
|
||||||
|
var pgm = new OpenNest.CNC.Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
if (!OpenNest.Math.Tolerance.IsEqualTo(theta, 0))
|
||||||
|
pgm.Rotate(theta, pgm.BoundingBox().Center);
|
||||||
|
return pgm;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_ComputesAngleOnProgramAssignment()
|
||||||
|
{
|
||||||
|
var pgm = RotatedRectProgram(100, 50, 0.5);
|
||||||
|
var d = new Drawing("r", pgm);
|
||||||
|
Assert.InRange(d.Source.Angle, -0.52, -0.48);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetProgram_RecomputesAngle()
|
||||||
|
{
|
||||||
|
var d = new Drawing("r", RotatedRectProgram(100, 50, 0.0));
|
||||||
|
Assert.Equal(0.0, d.Source.Angle, precision: 6);
|
||||||
|
|
||||||
|
d.Program = RotatedRectProgram(100, 50, 0.5);
|
||||||
|
Assert.InRange(d.Source.Angle, -0.52, -0.48);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCutOff_SkipsAngleComputation()
|
||||||
|
{
|
||||||
|
var d = new Drawing("cut", RotatedRectProgram(100, 50, 0.5)) { IsCutOff = true };
|
||||||
|
// Re-assign after flag is set so the setter observes IsCutOff.
|
||||||
|
d.Program = RotatedRectProgram(100, 50, 0.5);
|
||||||
|
Assert.Equal(0.0, d.Source.Angle, precision: 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecomputeCanonicalAngle_UpdatesAfterMutation()
|
||||||
|
{
|
||||||
|
var d = new Drawing("r", RotatedRectProgram(100, 50, 0.0));
|
||||||
|
Assert.Equal(0.0, d.Source.Angle, precision: 6);
|
||||||
|
|
||||||
|
// Mutate in-place (doesn't trigger setter).
|
||||||
|
d.Program.Rotate(0.5, d.Program.BoundingBox().Center);
|
||||||
|
Assert.Equal(0.0, d.Source.Angle, precision: 6); // still stale
|
||||||
|
|
||||||
|
d.RecomputeCanonicalAngle();
|
||||||
|
Assert.InRange(d.Source.Angle, -0.52, -0.48);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Engine;
|
||||||
|
|
||||||
|
public class CanonicalFrameTests
|
||||||
|
{
|
||||||
|
private static Drawing MakeRect(double w, double h, double rotation)
|
||||||
|
{
|
||||||
|
var pgm = new OpenNest.CNC.Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
if (!Tolerance.IsEqualTo(rotation, 0))
|
||||||
|
pgm.Rotate(rotation, pgm.BoundingBox().Center);
|
||||||
|
return new Drawing("rect", pgm) { Source = new SourceInfo { Angle = -rotation } };
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AsCanonicalCopy_AxisAlignsMbr()
|
||||||
|
{
|
||||||
|
var d = MakeRect(100, 50, 0.6);
|
||||||
|
var canonical = CanonicalFrame.AsCanonicalCopy(d);
|
||||||
|
|
||||||
|
var bb = canonical.Program.BoundingBox();
|
||||||
|
var longer = System.Math.Max(bb.Length, bb.Width);
|
||||||
|
var shorter = System.Math.Min(bb.Length, bb.Width);
|
||||||
|
Assert.InRange(longer, 100 - 0.1, 100 + 0.1);
|
||||||
|
Assert.InRange(shorter, 50 - 0.1, 50 + 0.1);
|
||||||
|
Assert.Equal(0.0, canonical.Source.Angle, precision: 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AsCanonicalCopy_DoesNotMutateSource()
|
||||||
|
{
|
||||||
|
var d = MakeRect(100, 50, 0.6);
|
||||||
|
var originalBbox = d.Program.BoundingBox();
|
||||||
|
var originalAngle = d.Source.Angle;
|
||||||
|
|
||||||
|
CanonicalFrame.AsCanonicalCopy(d);
|
||||||
|
|
||||||
|
var afterBbox = d.Program.BoundingBox();
|
||||||
|
Assert.Equal(originalBbox.Width, afterBbox.Width, precision: 6);
|
||||||
|
Assert.Equal(originalBbox.Length, afterBbox.Length, precision: 6);
|
||||||
|
Assert.Equal(originalAngle, d.Source.Angle, precision: 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FromCanonical_ComposesSourceAngleOntoRotation()
|
||||||
|
{
|
||||||
|
var d = MakeRect(100, 50, 0.0);
|
||||||
|
var part = new Part(d);
|
||||||
|
part.Rotate(0.2); // engine returned a canonical-frame part at R = 0.2
|
||||||
|
|
||||||
|
var placed = CanonicalFrame.FromCanonical(new List<Part> { part }, sourceAngle: -0.5);
|
||||||
|
|
||||||
|
// R' = R + sourceAngle = 0.2 + (-0.5) = -0.3
|
||||||
|
// Part.Rotation comes from Program.Rotation which is normalized to [0, 2PI),
|
||||||
|
// so compare after normalizing the expected value as well.
|
||||||
|
Assert.Single(placed);
|
||||||
|
Assert.Equal(Angle.NormalizeRad(-0.3), placed[0].Rotation, precision: 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundTrip_RestoresGeometry()
|
||||||
|
{
|
||||||
|
var d = MakeRect(100, 50, 0.4);
|
||||||
|
var canonical = CanonicalFrame.AsCanonicalCopy(d);
|
||||||
|
|
||||||
|
// Place a part at origin in the canonical frame.
|
||||||
|
var part = Part.CreateAtOrigin(canonical);
|
||||||
|
var canonicalBbox = part.BoundingBox;
|
||||||
|
|
||||||
|
var placed = CanonicalFrame.FromCanonical(new List<Part> { part }, d.Source.Angle);
|
||||||
|
|
||||||
|
var originalBbox = d.Program.BoundingBox();
|
||||||
|
Assert.Equal(originalBbox.Width, placed[0].BoundingBox.Width, precision: 2);
|
||||||
|
Assert.Equal(originalBbox.Length, placed[0].BoundingBox.Length, precision: 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Engine;
|
||||||
|
|
||||||
|
public class NestInvarianceTests
|
||||||
|
{
|
||||||
|
private static OpenNest.CNC.Program MakeLShapedProgram()
|
||||||
|
{
|
||||||
|
// L-shape: 100x50 outer rect with a 50x30 notch removed from top-right.
|
||||||
|
var pgm = new OpenNest.CNC.Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(100, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(100, 20)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(50, 20)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(50, 50)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 50)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
return pgm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Drawing MakeImportedAt(double rotation)
|
||||||
|
{
|
||||||
|
var pgm = MakeLShapedProgram();
|
||||||
|
if (!Tolerance.IsEqualTo(rotation, 0))
|
||||||
|
pgm.Rotate(rotation, pgm.BoundingBox().Center);
|
||||||
|
return new Drawing("L", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Plate MakePlate() => new Plate(new Size(500, 500))
|
||||||
|
{
|
||||||
|
Quadrant = 1,
|
||||||
|
PartSpacing = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static int RunFillCount(Drawing drawing, Plate plate)
|
||||||
|
{
|
||||||
|
BestFitCache.Clear();
|
||||||
|
var engine = new DefaultNestEngine(plate);
|
||||||
|
var item = new NestItem { Drawing = drawing };
|
||||||
|
var parts = engine.Fill(item, plate.WorkArea(), progress: null, token: CancellationToken.None);
|
||||||
|
return parts?.Count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0.0)]
|
||||||
|
[InlineData(0.3)]
|
||||||
|
[InlineData(0.8)]
|
||||||
|
[InlineData(1.2)]
|
||||||
|
public void Fill_SameCount_AcrossImportOrientations(double theta)
|
||||||
|
{
|
||||||
|
var baseline = RunFillCount(MakeImportedAt(0.0), MakePlate());
|
||||||
|
var rotated = RunFillCount(MakeImportedAt(theta), MakePlate());
|
||||||
|
|
||||||
|
// Allow +/-1 tolerance for sweep quantization edge effects near plate boundaries.
|
||||||
|
Assert.InRange(rotated, baseline - 1, baseline + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_PlacedPartsStayWithinWorkArea_AcrossImportOrientations()
|
||||||
|
{
|
||||||
|
var plate = MakePlate();
|
||||||
|
var workArea = plate.WorkArea();
|
||||||
|
|
||||||
|
foreach (var theta in new[] { 0.0, 0.3, 0.8, 1.2 })
|
||||||
|
{
|
||||||
|
BestFitCache.Clear();
|
||||||
|
var engine = new DefaultNestEngine(plate);
|
||||||
|
var item = new NestItem { Drawing = MakeImportedAt(theta) };
|
||||||
|
var parts = engine.Fill(item, workArea, progress: null, token: CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.NotNull(parts);
|
||||||
|
foreach (var p in parts)
|
||||||
|
{
|
||||||
|
Assert.InRange(p.BoundingBox.Left, workArea.Left - 0.5, workArea.Right + 0.5);
|
||||||
|
Assert.InRange(p.BoundingBox.Bottom, workArea.Bottom - 0.5, workArea.Top + 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,6 +97,33 @@ namespace OpenNest.Tests.Fill
|
|||||||
return part;
|
return part;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Drawing MakeTriangleDrawing(params Vector[] points)
|
||||||
|
{
|
||||||
|
var pgm = new OpenNest.CNC.Program();
|
||||||
|
pgm.Codes.Add(new OpenNest.CNC.RapidMove(points[0]));
|
||||||
|
|
||||||
|
for (var i = 1; i < points.Length; i++)
|
||||||
|
pgm.Codes.Add(new OpenNest.CNC.LinearMove(points[i]));
|
||||||
|
|
||||||
|
pgm.Codes.Add(new OpenNest.CNC.LinearMove(points[0]));
|
||||||
|
return new Drawing("triangle", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Part MakeTrianglePart(params Vector[] points)
|
||||||
|
{
|
||||||
|
var part = new Part(MakeTriangleDrawing(points));
|
||||||
|
part.UpdateBounds();
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Part MakeTrianglePart(double x, double y, params Vector[] points)
|
||||||
|
{
|
||||||
|
var part = MakeTrianglePart(points);
|
||||||
|
part.Location = new Vector(x, y);
|
||||||
|
part.UpdateBounds();
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Push_Left_MovesPartTowardEdge()
|
public void Push_Left_MovesPartTowardEdge()
|
||||||
{
|
{
|
||||||
@@ -171,6 +198,86 @@ namespace OpenNest.Tests.Fill
|
|||||||
Assert.NotEqual(distNoSpacing, distWithSpacing);
|
Assert.NotEqual(distNoSpacing, distWithSpacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Push_Up_AllowsSharedDiagonalEdgeToSeparate()
|
||||||
|
{
|
||||||
|
var workArea = new Box(0, 0, 20, 20);
|
||||||
|
var obstacle = MakeTrianglePart(
|
||||||
|
new Vector(0, 0),
|
||||||
|
new Vector(10, 0),
|
||||||
|
new Vector(0, 10));
|
||||||
|
var movingPart = MakeTrianglePart(
|
||||||
|
new Vector(0, 10),
|
||||||
|
new Vector(10, 0),
|
||||||
|
new Vector(10, 10));
|
||||||
|
|
||||||
|
var distance = Compactor.Push(
|
||||||
|
new List<Part> { movingPart },
|
||||||
|
new List<Part> { obstacle },
|
||||||
|
workArea,
|
||||||
|
0,
|
||||||
|
PushDirection.Up);
|
||||||
|
|
||||||
|
Assert.True(distance > 0);
|
||||||
|
Assert.True(movingPart.BoundingBox.Top > 19.9);
|
||||||
|
Assert.False(movingPart.Intersects(obstacle, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Push_Up_MovesAfterRightTriangleIsPushedLeftIntoSharedEdge()
|
||||||
|
{
|
||||||
|
var workArea = new Box(0, 0, 24, 24);
|
||||||
|
var leftTriangle = MakeTrianglePart(
|
||||||
|
2, 2,
|
||||||
|
new Vector(0, 0),
|
||||||
|
new Vector(8, 0),
|
||||||
|
new Vector(4, 10));
|
||||||
|
var rightTriangle = MakeTrianglePart(
|
||||||
|
14, 4,
|
||||||
|
new Vector(0, 10),
|
||||||
|
new Vector(8, 10),
|
||||||
|
new Vector(4, 0));
|
||||||
|
|
||||||
|
var moving = new List<Part> { rightTriangle };
|
||||||
|
var obstacles = new List<Part> { leftTriangle };
|
||||||
|
|
||||||
|
var leftDistance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left);
|
||||||
|
var yBeforePushUp = rightTriangle.Location.Y;
|
||||||
|
var bottomBeforePushUp = rightTriangle.BoundingBox.Bottom;
|
||||||
|
|
||||||
|
var upDistance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Up);
|
||||||
|
|
||||||
|
Assert.True(leftDistance > 0);
|
||||||
|
Assert.True(upDistance > 0);
|
||||||
|
Assert.True(rightTriangle.Location.Y > yBeforePushUp);
|
||||||
|
Assert.True(rightTriangle.BoundingBox.Bottom > bottomBeforePushUp);
|
||||||
|
Assert.False(rightTriangle.Intersects(leftTriangle, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Push_Left_BlocksWhenSharedDiagonalEdgeWouldOverlap()
|
||||||
|
{
|
||||||
|
var workArea = new Box(0, 0, 20, 20);
|
||||||
|
var obstacle = MakeTrianglePart(
|
||||||
|
new Vector(0, 0),
|
||||||
|
new Vector(10, 0),
|
||||||
|
new Vector(0, 10));
|
||||||
|
var movingPart = MakeTrianglePart(
|
||||||
|
new Vector(0, 10),
|
||||||
|
new Vector(10, 0),
|
||||||
|
new Vector(10, 10));
|
||||||
|
|
||||||
|
var distance = Compactor.Push(
|
||||||
|
new List<Part> { movingPart },
|
||||||
|
new List<Part> { obstacle },
|
||||||
|
workArea,
|
||||||
|
0,
|
||||||
|
PushDirection.Left);
|
||||||
|
|
||||||
|
Assert.Equal(0, distance);
|
||||||
|
Assert.Equal(0, movingPart.BoundingBox.Left);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Push_AngleLeft_MovesPartTowardEdge()
|
public void Push_AngleLeft_MovesPartTowardEdge()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Geometry;
|
||||||
|
|
||||||
|
public class BoxComparisonTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GreaterThan_TallerBox_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var tall = new Box(0, 0, 10, 20);
|
||||||
|
var short_ = new Box(0, 0, 10, 10);
|
||||||
|
|
||||||
|
Assert.True(tall > short_);
|
||||||
|
Assert.False(short_ > tall);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GreaterThan_SameWidthLongerBox_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var longer = new Box(0, 0, 20, 10);
|
||||||
|
var shorter = new Box(0, 0, 10, 10);
|
||||||
|
|
||||||
|
Assert.True(longer > shorter);
|
||||||
|
Assert.False(shorter > longer);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LessThan_ShorterBox_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var tall = new Box(0, 0, 10, 20);
|
||||||
|
var short_ = new Box(0, 0, 10, 10);
|
||||||
|
|
||||||
|
Assert.True(short_ < tall);
|
||||||
|
Assert.False(tall < short_);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GreaterThanOrEqual_EqualBoxes_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var a = new Box(0, 0, 10, 20);
|
||||||
|
var b = new Box(0, 0, 10, 20);
|
||||||
|
|
||||||
|
Assert.True(a >= b);
|
||||||
|
Assert.True(b >= a);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LessThanOrEqual_EqualBoxes_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var a = new Box(0, 0, 10, 20);
|
||||||
|
var b = new Box(0, 0, 10, 20);
|
||||||
|
|
||||||
|
Assert.True(a <= b);
|
||||||
|
Assert.True(b <= a);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompareTo_TallerBox_ReturnsPositive()
|
||||||
|
{
|
||||||
|
var tall = new Box(0, 0, 10, 20);
|
||||||
|
var short_ = new Box(0, 0, 10, 10);
|
||||||
|
|
||||||
|
Assert.True(tall.CompareTo(short_) > 0);
|
||||||
|
Assert.True(short_.CompareTo(tall) < 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompareTo_EqualBoxes_ReturnsZero()
|
||||||
|
{
|
||||||
|
var a = new Box(0, 0, 10, 20);
|
||||||
|
var b = new Box(0, 0, 10, 20);
|
||||||
|
|
||||||
|
Assert.Equal(0, a.CompareTo(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sort_OrdersByWidthThenLength()
|
||||||
|
{
|
||||||
|
var boxes = new List<Box>
|
||||||
|
{
|
||||||
|
new Box(0, 0, 20, 10),
|
||||||
|
new Box(0, 0, 5, 30),
|
||||||
|
new Box(0, 0, 10, 10),
|
||||||
|
};
|
||||||
|
|
||||||
|
boxes.Sort();
|
||||||
|
|
||||||
|
Assert.Equal(10, boxes[0].Width);
|
||||||
|
Assert.Equal(10, boxes[0].Length);
|
||||||
|
Assert.Equal(10, boxes[1].Width);
|
||||||
|
Assert.Equal(20, boxes[1].Length);
|
||||||
|
Assert.Equal(30, boxes[2].Width);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenNest.Tests.Geometry;
|
namespace OpenNest.Tests.Geometry;
|
||||||
|
|
||||||
public class EllipseConverterTests
|
public class EllipseConverterTests
|
||||||
{
|
{
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
private const double Tol = 1e-10;
|
private const double Tol = 1e-10;
|
||||||
|
|
||||||
|
public EllipseConverterTests(ITestOutputHelper output) => _output = output;
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EvaluatePoint_AtZero_ReturnsMajorAxisEnd()
|
public void EvaluatePoint_AtZero_ReturnsMajorAxisEnd()
|
||||||
{
|
{
|
||||||
@@ -244,6 +249,101 @@ public class EllipseConverterTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DxfImport_ArcBoundingBoxes_Diagnostic()
|
||||||
|
{
|
||||||
|
var path = @"C:\Users\aisaacs\Desktop\11ga tab.dxf";
|
||||||
|
if (!System.IO.File.Exists(path)) return;
|
||||||
|
|
||||||
|
var result = Dxf.Import(path);
|
||||||
|
var all = (System.Collections.Generic.IEnumerable<IBoundable>)result.Entities;
|
||||||
|
var bbox = all.GetBoundingBox();
|
||||||
|
_output.WriteLine($"Overall: X={bbox.X:F4} Y={bbox.Y:F4} W={bbox.Length:F4} H={bbox.Width:F4}");
|
||||||
|
|
||||||
|
for (var i = 0; i < result.Entities.Count; i++)
|
||||||
|
{
|
||||||
|
var e = result.Entities[i];
|
||||||
|
var b = e.BoundingBox;
|
||||||
|
var flag = (b.Length > 1 || b.Width > 1) ? " ***" : "";
|
||||||
|
_output.WriteLine($"{i + 1,3}. {e.GetType().Name,-8} X={b.X:F4} Y={b.Y:F4} W={b.Length:F4} H={b.Width:F4}{flag}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToOpenNest_FlippedNormalZ_ProducesCorrectArcs()
|
||||||
|
{
|
||||||
|
var normal = new ACadSharp.Entities.Ellipse
|
||||||
|
{
|
||||||
|
Center = new CSMath.XYZ(-0.275, -0.245, 0),
|
||||||
|
MajorAxisEndPoint = new CSMath.XYZ(0.0001, 1.245, 0),
|
||||||
|
RadiusRatio = 0.28,
|
||||||
|
StartParameter = 0.017,
|
||||||
|
EndParameter = 1.571,
|
||||||
|
Normal = new CSMath.XYZ(0, 0, 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
var flipped = new ACadSharp.Entities.Ellipse
|
||||||
|
{
|
||||||
|
Center = new CSMath.XYZ(0.275, -0.245, 0),
|
||||||
|
MajorAxisEndPoint = new CSMath.XYZ(-0.0001, 1.245, 0),
|
||||||
|
RadiusRatio = 0.28,
|
||||||
|
StartParameter = 0.017,
|
||||||
|
EndParameter = 1.571,
|
||||||
|
Normal = new CSMath.XYZ(0, 0, -1)
|
||||||
|
};
|
||||||
|
|
||||||
|
var normalArcs = normal.ToOpenNest();
|
||||||
|
var flippedArcs = flipped.ToOpenNest();
|
||||||
|
|
||||||
|
Assert.True(normalArcs.Count > 0);
|
||||||
|
Assert.True(flippedArcs.Count > 0);
|
||||||
|
Assert.True(normalArcs.All(e => e is Arc));
|
||||||
|
Assert.True(flippedArcs.All(e => e is Arc));
|
||||||
|
|
||||||
|
var normalFirst = (Arc)normalArcs.First();
|
||||||
|
var flippedFirst = (Arc)flippedArcs.First();
|
||||||
|
var normalStart = GetArcStart(normalFirst);
|
||||||
|
var flippedStart = GetArcStart(flippedFirst);
|
||||||
|
|
||||||
|
Assert.True(normalStart.X < 0, $"Normal ellipse start X should be negative, got {normalStart.X}");
|
||||||
|
Assert.True(flippedStart.X > 0, $"Flipped ellipse should bulge right, got {flippedStart.X}");
|
||||||
|
|
||||||
|
var normalBbox = GetBoundingBox(normalArcs.Cast<Arc>());
|
||||||
|
var flippedBbox = GetBoundingBox(flippedArcs.Cast<Arc>());
|
||||||
|
Assert.True(flippedBbox.minX > 0, $"Flipped ellipse should stay on positive X side, minX={flippedBbox.minX}");
|
||||||
|
Assert.True(normalBbox.maxX < 0, $"Normal ellipse should stay on negative X side, maxX={normalBbox.maxX}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (double minX, double maxX) GetBoundingBox(IEnumerable<Arc> arcs)
|
||||||
|
{
|
||||||
|
var minX = double.MaxValue;
|
||||||
|
var maxX = double.MinValue;
|
||||||
|
foreach (var arc in arcs)
|
||||||
|
{
|
||||||
|
var s = GetArcStart(arc);
|
||||||
|
var e = GetArcEnd(arc);
|
||||||
|
minX = System.Math.Min(minX, System.Math.Min(s.X, e.X));
|
||||||
|
maxX = System.Math.Max(maxX, System.Math.Max(s.X, e.X));
|
||||||
|
}
|
||||||
|
return (minX, maxX);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector GetArcStart(Arc arc)
|
||||||
|
{
|
||||||
|
var angle = arc.IsReversed ? arc.EndAngle : arc.StartAngle;
|
||||||
|
return new Vector(
|
||||||
|
arc.Center.X + arc.Radius * System.Math.Cos(angle),
|
||||||
|
arc.Center.Y + arc.Radius * System.Math.Sin(angle));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector GetArcEnd(Arc arc)
|
||||||
|
{
|
||||||
|
var angle = arc.IsReversed ? arc.StartAngle : arc.EndAngle;
|
||||||
|
return new Vector(
|
||||||
|
arc.Center.X + arc.Radius * System.Math.Cos(angle),
|
||||||
|
arc.Center.Y + arc.Radius * System.Math.Sin(angle));
|
||||||
|
}
|
||||||
|
|
||||||
private static double MaxDeviationFromEllipse(Arc arc, Vector ellipseCenter,
|
private static double MaxDeviationFromEllipse(Arc arc, Vector ellipseCenter,
|
||||||
double semiMajor, double semiMinor, double rotation, int samples)
|
double semiMajor, double semiMinor, double rotation, int samples)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Geometry;
|
||||||
|
|
||||||
|
public class WeldEndpointsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void WeldEndpoints_SnapsNearbyLineEndpoints()
|
||||||
|
{
|
||||||
|
var line1 = new Line(0, 0, 10, 0);
|
||||||
|
var line2 = new Line(10.0000005, 0, 20, 0);
|
||||||
|
var entities = new List<Entity> { line1, line2 };
|
||||||
|
|
||||||
|
ShapeBuilder.WeldEndpoints(entities, 0.000001);
|
||||||
|
|
||||||
|
Assert.True(line1.EndPoint.DistanceTo(line2.StartPoint) <= Tolerance.Epsilon);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WeldEndpoints_SnapsArcEndpointByAdjustingAngle()
|
||||||
|
{
|
||||||
|
var line = new Line(0, 0, 10, 0);
|
||||||
|
var arc = new Arc(15, 0, 5, Angle.ToRadians(180.001), Angle.ToRadians(90));
|
||||||
|
var entities = new List<Entity> { line, arc };
|
||||||
|
|
||||||
|
ShapeBuilder.WeldEndpoints(entities, 0.01);
|
||||||
|
|
||||||
|
var arcStart = arc.StartPoint();
|
||||||
|
Assert.True(line.EndPoint.DistanceTo(arcStart) <= 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WeldEndpoints_DoesNotWeldDistantEndpoints()
|
||||||
|
{
|
||||||
|
var line1 = new Line(0, 0, 10, 0);
|
||||||
|
var line2 = new Line(10.1, 0, 20, 0);
|
||||||
|
var entities = new List<Entity> { line1, line2 };
|
||||||
|
|
||||||
|
ShapeBuilder.WeldEndpoints(entities, 0.000001);
|
||||||
|
|
||||||
|
Assert.True(line1.EndPoint.DistanceTo(line2.StartPoint) > 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetShapes_WithWeldTolerance_WeldsBeforeChaining()
|
||||||
|
{
|
||||||
|
var line1 = new Line(0, 0, 10, 0);
|
||||||
|
var line2 = new Line(10.0000005, 0, 10.0000005, 10);
|
||||||
|
var entities = new List<Entity> { line1, line2 };
|
||||||
|
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities, weldTolerance: 0.000001);
|
||||||
|
|
||||||
|
Assert.Single(shapes);
|
||||||
|
Assert.Equal(2, shapes[0].Entities.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetShapes_WithoutWeldTolerance_DefaultBehavior()
|
||||||
|
{
|
||||||
|
var line1 = new Line(0, 0, 10, 0);
|
||||||
|
var line2 = new Line(10, 0, 10, 10);
|
||||||
|
var entities = new List<Entity> { line1, line2 };
|
||||||
|
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
|
||||||
|
Assert.Single(shapes);
|
||||||
|
Assert.Equal(2, shapes[0].Entities.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.IO;
|
||||||
|
|
||||||
|
public class ChrFontTests
|
||||||
|
{
|
||||||
|
private ChrFont LoadFont()
|
||||||
|
{
|
||||||
|
var path = TestConfig.GetExistingPath("ChrFontPath");
|
||||||
|
Skip.If(path == null, "ChrFontPath not configured in test-config.json or file not found");
|
||||||
|
return ChrFont.Read(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void Read_ParsesFontName()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
Assert.Equal("US BLOCK 1L", font.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void Read_ParsesVersion()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
Assert.StartsWith("C1.", font.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void Read_HasAsciiGlyphs()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
Assert.True(font.HasGlyph('A'));
|
||||||
|
Assert.True(font.HasGlyph('Z'));
|
||||||
|
Assert.True(font.HasGlyph('0'));
|
||||||
|
Assert.True(font.HasGlyph(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void Read_HasExtendedGlyphs()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
Assert.True(font.HasGlyph(0xC7)); // C-cedilla
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void Glyph_L_ProducesLines()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
var glyph = font.GetGlyph('L');
|
||||||
|
Assert.NotNull(glyph);
|
||||||
|
|
||||||
|
var entities = glyph.ToEntities(1.0, 0, 0);
|
||||||
|
Assert.True(entities.Count >= 2, $"Expected at least 2 entities for 'L', got {entities.Count}");
|
||||||
|
Assert.All(entities, e => Assert.Equal(EntityType.Line, e.Type));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void Glyph_O_ProducesEntities()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
var glyph = font.GetGlyph('O');
|
||||||
|
Assert.NotNull(glyph);
|
||||||
|
|
||||||
|
var entities = glyph.ToEntities(1.0, 0, 0);
|
||||||
|
Assert.True(entities.Count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void RenderText_ProducesEntities()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
var entities = font.RenderText("HELLO", 1.0, new Vector(0, 0));
|
||||||
|
Assert.True(entities.Count > 0, "RenderText should produce entities");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void RenderText_ScalesCorrectly()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
|
||||||
|
var small = font.RenderText("A", 0.5, Vector.Zero);
|
||||||
|
var large = font.RenderText("A", 2.0, Vector.Zero);
|
||||||
|
|
||||||
|
var smallBox = small.GetBoundingBox();
|
||||||
|
var largeBox = large.GetBoundingBox();
|
||||||
|
|
||||||
|
Assert.True(largeBox.Width > smallBox.Width);
|
||||||
|
Assert.True(largeBox.Length > smallBox.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void RenderText_AdvancesCursor()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
var abEntities = font.RenderText("AB", 1.0, Vector.Zero);
|
||||||
|
var aEntities = font.RenderText("A", 1.0, Vector.Zero);
|
||||||
|
|
||||||
|
var abBox = abEntities.GetBoundingBox();
|
||||||
|
var aBox = aEntities.GetBoundingBox();
|
||||||
|
|
||||||
|
Assert.True(abBox.Length > aBox.Length * 1.5,
|
||||||
|
$"AB width ({abBox.Length:F1}) should be significantly wider than A width ({aBox.Length:F1})");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void RenderText_MatchesGravographReference()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
|
||||||
|
var height = 5.08;
|
||||||
|
var centerX = 50.8;
|
||||||
|
var centerY = 34.925;
|
||||||
|
|
||||||
|
var entities = font.RenderText("Text", height, Vector.Zero);
|
||||||
|
var rawBox = entities.GetBoundingBox();
|
||||||
|
var shiftX = centerX - (rawBox.Left + rawBox.Right) / 2;
|
||||||
|
var shiftY = centerY - (rawBox.Top + rawBox.Bottom) / 2;
|
||||||
|
foreach (var e in entities)
|
||||||
|
e.Offset(new Vector(shiftX, shiftY));
|
||||||
|
Assert.True(entities.Count > 0, "Should produce entities for 'Text'");
|
||||||
|
|
||||||
|
var box = entities.GetBoundingBox();
|
||||||
|
|
||||||
|
var refLeft = 43.53;
|
||||||
|
var refRight = 58.07;
|
||||||
|
var refBottom = 32.39;
|
||||||
|
var refTop = 37.47;
|
||||||
|
|
||||||
|
var tolerance = 0.5;
|
||||||
|
|
||||||
|
Assert.True(System.Math.Abs(box.Left - refLeft) < tolerance,
|
||||||
|
$"Left: ours={box.Left:F2}, ref={refLeft:F2}, diff={System.Math.Abs(box.Left - refLeft):F2}");
|
||||||
|
Assert.True(System.Math.Abs(box.Right - refRight) < tolerance,
|
||||||
|
$"Right: ours={box.Right:F2}, ref={refRight:F2}, diff={System.Math.Abs(box.Right - refRight):F2}");
|
||||||
|
Assert.True(System.Math.Abs(box.Bottom - refBottom) < tolerance,
|
||||||
|
$"Bottom: ours={box.Bottom:F2}, ref={refBottom:F2}, diff={System.Math.Abs(box.Bottom - refBottom):F2}");
|
||||||
|
Assert.True(System.Math.Abs(box.Top - refTop) < tolerance,
|
||||||
|
$"Top: ours={box.Top:F2}, ref={refTop:F2}, diff={System.Math.Abs(box.Top - refTop):F2}");
|
||||||
|
|
||||||
|
var actualCapHeight = box.Top - box.Bottom;
|
||||||
|
Assert.True(System.Math.Abs(actualCapHeight - height) < 0.5,
|
||||||
|
$"Cap height: ours={actualCapHeight:F2}, expected={height:F2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void MeasureTextWidth_IsConsistent()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
var height = 5.08;
|
||||||
|
var measuredWidth = font.MeasureTextWidth("Text", height);
|
||||||
|
var entities = font.RenderText("Text", height, Vector.Zero);
|
||||||
|
var box = entities.GetBoundingBox();
|
||||||
|
|
||||||
|
Assert.True(measuredWidth >= box.Length,
|
||||||
|
$"Measured={measuredWidth:F2} should be >= rendered={box.Length:F2}");
|
||||||
|
Assert.True(measuredWidth - box.Length < 2.0,
|
||||||
|
$"Measured={measuredWidth:F2}, rendered={box.Length:F2}, diff={measuredWidth - box.Length:F2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public void Glyph_t_HasCurveAtBottom()
|
||||||
|
{
|
||||||
|
var font = LoadFont();
|
||||||
|
var glyph = font.GetGlyph('t');
|
||||||
|
Assert.NotNull(glyph);
|
||||||
|
|
||||||
|
var entities = glyph.ToEntities(1.0, 0, 0);
|
||||||
|
var lines = entities.Cast<Line>().ToList();
|
||||||
|
|
||||||
|
Assert.True(lines.Count >= 10, $"Expected at least 10 entities for 't', got {lines.Count}");
|
||||||
|
|
||||||
|
var curveLines = lines.Skip(1).Take(lines.Count - 3).ToList();
|
||||||
|
Assert.True(curveLines.Count >= 14, $"Expected at least 14 curve segments, got {curveLines.Count}");
|
||||||
|
|
||||||
|
var lastCurve = curveLines[^1];
|
||||||
|
Assert.True(lastCurve.EndPoint.X > curveLines[0].StartPoint.X,
|
||||||
|
$"Curve should end to the right of where it starts: start X={curveLines[0].StartPoint.X:F1}, end X={lastCurve.EndPoint.X:F1}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.IO;
|
||||||
|
|
||||||
|
public class RemoveDuplicateArcsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RemoveDuplicateArcs_RemovesArcMatchingCircle_SameLayer()
|
||||||
|
{
|
||||||
|
var layer = new Layer("0");
|
||||||
|
var circle = new Circle(10, 10, 5) { Layer = layer };
|
||||||
|
var arc = new Arc(10, 10, 5, 0, Angle.ToRadians(90)) { Layer = layer };
|
||||||
|
var line = new Line(0, 0, 10, 0) { Layer = layer };
|
||||||
|
var entities = new List<Entity> { circle, arc, line };
|
||||||
|
|
||||||
|
CadImporter.RemoveDuplicateArcs(entities);
|
||||||
|
|
||||||
|
Assert.Equal(2, entities.Count);
|
||||||
|
Assert.Contains(circle, entities);
|
||||||
|
Assert.Contains(line, entities);
|
||||||
|
Assert.DoesNotContain(arc, entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveDuplicateArcs_KeepsArcOnDifferentLayer()
|
||||||
|
{
|
||||||
|
var layer1 = new Layer("cut");
|
||||||
|
var layer2 = new Layer("etch");
|
||||||
|
var circle = new Circle(10, 10, 5) { Layer = layer1 };
|
||||||
|
var arc = new Arc(10, 10, 5, 0, Angle.ToRadians(90)) { Layer = layer2 };
|
||||||
|
var entities = new List<Entity> { circle, arc };
|
||||||
|
|
||||||
|
CadImporter.RemoveDuplicateArcs(entities);
|
||||||
|
|
||||||
|
Assert.Equal(2, entities.Count);
|
||||||
|
Assert.Contains(arc, entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveDuplicateArcs_KeepsArcWithDifferentRadius()
|
||||||
|
{
|
||||||
|
var layer = new Layer("0");
|
||||||
|
var circle = new Circle(10, 10, 5) { Layer = layer };
|
||||||
|
var arc = new Arc(10, 10, 3, 0, Angle.ToRadians(90)) { Layer = layer };
|
||||||
|
var entities = new List<Entity> { circle, arc };
|
||||||
|
|
||||||
|
CadImporter.RemoveDuplicateArcs(entities);
|
||||||
|
|
||||||
|
Assert.Equal(2, entities.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveDuplicateArcs_KeepsArcWithDifferentCenter()
|
||||||
|
{
|
||||||
|
var layer = new Layer("0");
|
||||||
|
var circle = new Circle(10, 10, 5) { Layer = layer };
|
||||||
|
var arc = new Arc(20, 20, 5, 0, Angle.ToRadians(90)) { Layer = layer };
|
||||||
|
var entities = new List<Entity> { circle, arc };
|
||||||
|
|
||||||
|
CadImporter.RemoveDuplicateArcs(entities);
|
||||||
|
|
||||||
|
Assert.Equal(2, entities.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveDuplicateArcs_NoCircles_NoChange()
|
||||||
|
{
|
||||||
|
var arc = new Arc(10, 10, 5, 0, Angle.ToRadians(90));
|
||||||
|
var line = new Line(0, 0, 10, 0);
|
||||||
|
var entities = new List<Entity> { arc, line };
|
||||||
|
|
||||||
|
CadImporter.RemoveDuplicateArcs(entities);
|
||||||
|
|
||||||
|
Assert.Equal(2, entities.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveDuplicateArcs_MultipleArcsMatchOneCircle_RemovesAll()
|
||||||
|
{
|
||||||
|
var layer = new Layer("0");
|
||||||
|
var circle = new Circle(10, 10, 5) { Layer = layer };
|
||||||
|
var arc1 = new Arc(10, 10, 5, 0, Angle.ToRadians(90)) { Layer = layer };
|
||||||
|
var arc2 = new Arc(10, 10, 5, Angle.ToRadians(90), Angle.ToRadians(180)) { Layer = layer };
|
||||||
|
var entities = new List<Entity> { circle, arc1, arc2 };
|
||||||
|
|
||||||
|
CadImporter.RemoveDuplicateArcs(entities);
|
||||||
|
|
||||||
|
Assert.Single(entities);
|
||||||
|
Assert.Contains(circle, entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using OpenNest.Math;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Math;
|
||||||
|
|
||||||
|
public class FractionTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("3/8", 0.375)]
|
||||||
|
[InlineData("1 3/4", 1.75)]
|
||||||
|
[InlineData("1-3/4", 1.75)]
|
||||||
|
[InlineData("1/2", 0.5)]
|
||||||
|
public void Parse_ValidFraction_ReturnsDouble(string input, double expected)
|
||||||
|
{
|
||||||
|
var result = Fraction.Parse(input);
|
||||||
|
|
||||||
|
Assert.Equal(expected, result, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("3/8", true)]
|
||||||
|
[InlineData("abc", false)]
|
||||||
|
[InlineData("1 3/4", true)]
|
||||||
|
public void IsValid_ReturnsExpected(string input, bool expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, Fraction.IsValid(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryParse_InvalidInput_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var result = Fraction.TryParse("abc", out var value);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Equal(0, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReplaceFractionsWithDecimals_ReplacesFractionInString()
|
||||||
|
{
|
||||||
|
var result = Fraction.ReplaceFractionsWithDecimals("length is 1 3/4 inches");
|
||||||
|
|
||||||
|
Assert.Contains("1.75", result);
|
||||||
|
Assert.DoesNotContain("3/4", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
<PackageReference Include="xunit" Version="2.5.3" />
|
<PackageReference Include="xunit" Version="2.5.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||||
|
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" />
|
<ProjectReference Include="..\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Posts.GravographIS\OpenNest.Posts.GravographIS.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest\OpenNest.csproj" />
|
<ProjectReference Include="..\OpenNest\OpenNest.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -37,6 +39,9 @@
|
|||||||
<Content Include="Splitting\TestData\**\*">
|
<Content Include="Splitting\TestData\**\*">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="test-config.json" Condition="Exists('test-config.json')">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using OpenNest.Shapes;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Shapes;
|
||||||
|
|
||||||
|
public class NgonShapeTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetDrawing_Octagon_BoundingBoxFitsWithinExpectedSize()
|
||||||
|
{
|
||||||
|
var shape = new NgonShape { Sides = 8, Width = 20 };
|
||||||
|
var drawing = shape.GetDrawing();
|
||||||
|
|
||||||
|
var bbox = drawing.Program.BoundingBox();
|
||||||
|
// Corner-to-corner is larger than flat-to-flat
|
||||||
|
Assert.True(bbox.Width >= 20 - 0.01);
|
||||||
|
Assert.True(bbox.Length >= 20 - 0.01);
|
||||||
|
// But should not be wildly larger (corner-to-corner ~ width / cos(22.5deg) ~ width * 1.0824)
|
||||||
|
Assert.True(bbox.Width < 22);
|
||||||
|
Assert.True(bbox.Length < 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(3)]
|
||||||
|
[InlineData(4)]
|
||||||
|
[InlineData(5)]
|
||||||
|
[InlineData(6)]
|
||||||
|
[InlineData(8)]
|
||||||
|
[InlineData(12)]
|
||||||
|
public void GetDrawing_HasOneLinearMovePerSide(int sides)
|
||||||
|
{
|
||||||
|
var shape = new NgonShape { Sides = sides, Width = 20 };
|
||||||
|
var drawing = shape.GetDrawing();
|
||||||
|
|
||||||
|
var moves = drawing.Program.Codes
|
||||||
|
.OfType<OpenNest.CNC.LinearMove>()
|
||||||
|
.Count();
|
||||||
|
Assert.Equal(sides, moves);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDrawing_ClampsSidesBelowThreeToTriangle()
|
||||||
|
{
|
||||||
|
var shape = new NgonShape { Sides = 2, Width = 20 };
|
||||||
|
var drawing = shape.GetDrawing();
|
||||||
|
|
||||||
|
var moves = drawing.Program.Codes
|
||||||
|
.OfType<OpenNest.CNC.LinearMove>()
|
||||||
|
.Count();
|
||||||
|
Assert.Equal(3, moves);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using OpenNest.Shapes;
|
|
||||||
|
|
||||||
namespace OpenNest.Tests.Shapes;
|
|
||||||
|
|
||||||
public class OctagonShapeTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void GetDrawing_BoundingBoxFitsWithinExpectedSize()
|
|
||||||
{
|
|
||||||
var shape = new OctagonShape { Width = 20 };
|
|
||||||
var drawing = shape.GetDrawing();
|
|
||||||
|
|
||||||
var bbox = drawing.Program.BoundingBox();
|
|
||||||
// Corner-to-corner is larger than flat-to-flat
|
|
||||||
Assert.True(bbox.Width >= 20 - 0.01);
|
|
||||||
Assert.True(bbox.Length >= 20 - 0.01);
|
|
||||||
// But should not be wildly larger (corner-to-corner ~ width / cos(22.5deg) ~ width * 1.0824)
|
|
||||||
Assert.True(bbox.Width < 22);
|
|
||||||
Assert.True(bbox.Length < 22);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetDrawing_HasEightEdges()
|
|
||||||
{
|
|
||||||
var shape = new OctagonShape { Width = 20 };
|
|
||||||
var drawing = shape.GetDrawing();
|
|
||||||
|
|
||||||
// An octagon program should have 8 linear moves (one per edge)
|
|
||||||
var moves = drawing.Program.Codes
|
|
||||||
.OfType<OpenNest.CNC.LinearMove>()
|
|
||||||
.Count();
|
|
||||||
Assert.Equal(8, moves);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,37 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.Tests;
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
internal static class TestConfig
|
||||||
|
{
|
||||||
|
private static readonly Lazy<Dictionary<string, string>> Config = new(() =>
|
||||||
|
{
|
||||||
|
var dir = AppContext.BaseDirectory;
|
||||||
|
for (var i = 0; i < 6; i++)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(dir, "test-config.json");
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new();
|
||||||
|
}
|
||||||
|
dir = Path.GetDirectoryName(dir)!;
|
||||||
|
}
|
||||||
|
return new();
|
||||||
|
});
|
||||||
|
|
||||||
|
public static string? Get(string key) =>
|
||||||
|
Config.Value.TryGetValue(key, out var val) ? val : null;
|
||||||
|
|
||||||
|
public static string? GetExistingPath(string key)
|
||||||
|
{
|
||||||
|
var path = Get(key);
|
||||||
|
return path != null && File.Exists(path) ? path : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static class TestHelpers
|
internal static class TestHelpers
|
||||||
{
|
{
|
||||||
public static Part MakePartAt(double x, double y, double size = 1)
|
public static Part MakePartAt(double x, double y, double size = 1)
|
||||||
|
|||||||
@@ -89,8 +89,10 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin
|
|||||||
new Size(48, 24), new Size(120, 10)
|
new Size(48, 24), new Size(120, 10)
|
||||||
};
|
};
|
||||||
|
|
||||||
var dxfFiles = Directory.GetFiles(dir, "*.dxf", SearchOption.AllDirectories);
|
var dxfFiles = Directory.GetFiles(dir, "*.dxf", SearchOption.AllDirectories)
|
||||||
Console.WriteLine($"Found {dxfFiles.Length} DXF files");
|
.Concat(Directory.GetFiles(dir, "*.dwg", SearchOption.AllDirectories))
|
||||||
|
.ToArray();
|
||||||
|
Console.WriteLine($"Found {dxfFiles.Length} CAD files");
|
||||||
var resolvedDb = dbPath.EndsWith(".db", StringComparison.OrdinalIgnoreCase) ? dbPath : dbPath + ".db";
|
var resolvedDb = dbPath.EndsWith(".db", StringComparison.OrdinalIgnoreCase) ? dbPath : dbPath + ".db";
|
||||||
Console.WriteLine($"Database: {Path.GetFullPath(resolvedDb)}");
|
Console.WriteLine($"Database: {Path.GetFullPath(resolvedDb)}");
|
||||||
Console.WriteLine($"Sheet sizes: {sheetSuite.Length} configurations");
|
Console.WriteLine($"Sheet sizes: {sheetSuite.Length} configurations");
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PostProcessors", "PostProce
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Posts.Cincinnati", "OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj", "{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Posts.Cincinnati", "OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj", "{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Posts.GravographIS", "OpenNest.Posts.GravographIS\OpenNest.Posts.GravographIS.csproj", "{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}"
|
||||||
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Data", "OpenNest.Data\OpenNest.Data.csproj", "{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Data", "OpenNest.Data\OpenNest.Data.csproj", "{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
@@ -186,12 +188,25 @@ Global
|
|||||||
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x64.Build.0 = Release|Any CPU
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x86.ActiveCfg = Release|Any CPU
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x86.Build.0 = Release|Any CPU
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532} = {4052CFAC-1F12-48BE-872D-F503C3B65D7E}
|
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532} = {4052CFAC-1F12-48BE-872D-F503C3B65D7E}
|
||||||
|
{3A6B8E7E-9B5F-4D2C-8AE3-2C9F5E3D1A40} = {4052CFAC-1F12-48BE-872D-F503C3B65D7E}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {86FE17B3-F764-40AE-BCAA-F26B470CA05C}
|
SolutionGuid = {86FE17B3-F764-40AE-BCAA-F26B470CA05C}
|
||||||
|
|||||||
@@ -16,15 +16,11 @@ namespace OpenNest.Actions
|
|||||||
private CutOffSettings settings;
|
private CutOffSettings settings;
|
||||||
private CutOffAxis lockedAxis = CutOffAxis.Vertical;
|
private CutOffAxis lockedAxis = CutOffAxis.Vertical;
|
||||||
private Dictionary<Part, Entity> perimeterCache;
|
private Dictionary<Part, Entity> perimeterCache;
|
||||||
private readonly Timer debounceTimer;
|
|
||||||
private bool regeneratePending;
|
|
||||||
|
|
||||||
public ActionCutOff(PlateView plateView)
|
public ActionCutOff(PlateView plateView)
|
||||||
: base(plateView)
|
: base(plateView)
|
||||||
{
|
{
|
||||||
settings = plateView.CutOffSettings;
|
settings = plateView.CutOffSettings;
|
||||||
debounceTimer = new Timer { Interval = 16 };
|
|
||||||
debounceTimer.Tick += OnDebounce;
|
|
||||||
ConnectEvents();
|
ConnectEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +36,6 @@ namespace OpenNest.Actions
|
|||||||
|
|
||||||
public override void DisconnectEvents()
|
public override void DisconnectEvents()
|
||||||
{
|
{
|
||||||
debounceTimer.Stop();
|
|
||||||
debounceTimer.Dispose();
|
|
||||||
plateView.MouseMove -= OnMouseMove;
|
plateView.MouseMove -= OnMouseMove;
|
||||||
plateView.MouseDown -= OnMouseDown;
|
plateView.MouseDown -= OnMouseDown;
|
||||||
plateView.KeyDown -= OnKeyDown;
|
plateView.KeyDown -= OnKeyDown;
|
||||||
@@ -58,18 +52,6 @@ namespace OpenNest.Actions
|
|||||||
|
|
||||||
private void OnMouseMove(object sender, MouseEventArgs e)
|
private void OnMouseMove(object sender, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
regeneratePending = true;
|
|
||||||
debounceTimer.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDebounce(object sender, System.EventArgs e)
|
|
||||||
{
|
|
||||||
debounceTimer.Stop();
|
|
||||||
|
|
||||||
if (!regeneratePending)
|
|
||||||
return;
|
|
||||||
|
|
||||||
regeneratePending = false;
|
|
||||||
var pt = plateView.CurrentPoint;
|
var pt = plateView.CurrentPoint;
|
||||||
previewCutOff = new CutOff(pt, lockedAxis);
|
previewCutOff = new CutOff(pt, lockedAxis);
|
||||||
previewCutOff.Regenerate(plateView.Plate, settings, perimeterCache);
|
previewCutOff.Regenerate(plateView.Plate, settings, perimeterCache);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using OpenNest.IO.Bom;
|
using OpenNest.Math;
|
||||||
using System;
|
using System;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ namespace OpenNest
|
|||||||
private Color edgeSpacingColor;
|
private Color edgeSpacingColor;
|
||||||
private Color previewPartColor;
|
private Color previewPartColor;
|
||||||
|
|
||||||
public static Color[] PartColors => Drawing.PartColors;
|
public string Name { get; set; } = "Unnamed";
|
||||||
|
|
||||||
|
public Color[] PartColors { get; set; } = Drawing.PartColors;
|
||||||
|
|
||||||
public static readonly ColorScheme Default = new ColorScheme
|
public static readonly ColorScheme Default = new ColorScheme
|
||||||
{
|
{
|
||||||
|
Name = "Classic",
|
||||||
BackgroundColor = Color.DarkGray,
|
BackgroundColor = Color.DarkGray,
|
||||||
LayoutOutlineColor = Color.Gray,
|
LayoutOutlineColor = Color.Gray,
|
||||||
LayoutFillColor = Color.WhiteSmoke,
|
LayoutFillColor = Color.WhiteSmoke,
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
using OpenNest.Forms;
|
||||||
|
using OpenNest.Properties;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public static class ColorSchemeRegistry
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, ColorScheme> builtIns =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["Classic"] = BuildClassic(),
|
||||||
|
["Pastel"] = BuildPastel(),
|
||||||
|
["Dark"] = BuildDark()
|
||||||
|
};
|
||||||
|
|
||||||
|
private static List<ColorScheme> diskCache;
|
||||||
|
|
||||||
|
public static IEnumerable<ColorScheme> AllSchemes
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
diskCache ??= LoadDiskSchemes().ToList();
|
||||||
|
return builtIns.Values.Concat(diskCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Refresh() => diskCache = null;
|
||||||
|
|
||||||
|
public static ColorScheme Get(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
return builtIns["Classic"];
|
||||||
|
|
||||||
|
var hit = AllSchemes.FirstOrDefault(
|
||||||
|
s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
return hit ?? builtIns["Classic"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApplyActiveFromSettings()
|
||||||
|
{
|
||||||
|
var name = Settings.Default.ActiveColorScheme;
|
||||||
|
var scheme = Get(name);
|
||||||
|
Apply(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Apply(ColorScheme scheme)
|
||||||
|
{
|
||||||
|
var d = ColorScheme.Default;
|
||||||
|
d.Name = scheme.Name;
|
||||||
|
d.BackgroundColor = scheme.BackgroundColor;
|
||||||
|
d.LayoutOutlineColor = scheme.LayoutOutlineColor;
|
||||||
|
d.LayoutFillColor = scheme.LayoutFillColor;
|
||||||
|
d.BoundingBoxColor = scheme.BoundingBoxColor;
|
||||||
|
d.RapidColor = scheme.RapidColor;
|
||||||
|
d.OriginColor = scheme.OriginColor;
|
||||||
|
d.EdgeSpacingColor = scheme.EdgeSpacingColor;
|
||||||
|
d.PreviewPartColor = scheme.PreviewPartColor;
|
||||||
|
d.PartColors = scheme.PartColors;
|
||||||
|
|
||||||
|
Drawing.PartColors = scheme.PartColors;
|
||||||
|
|
||||||
|
RecolorOpenNests(scheme.PartColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RecolorOpenNests(Color[] palette)
|
||||||
|
{
|
||||||
|
foreach (Form f in Application.OpenForms)
|
||||||
|
{
|
||||||
|
if (f is not EditNestForm enf)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
foreach (var drawing in enf.Nest.Drawings)
|
||||||
|
{
|
||||||
|
if (drawing.IsCutOff)
|
||||||
|
continue;
|
||||||
|
drawing.Color = palette[i % palette.Length];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<ColorScheme> LoadDiskSchemes()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(AppContext.BaseDirectory, "Schemes");
|
||||||
|
if (!Directory.Exists(dir))
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
foreach (var path in Directory.GetFiles(dir, "*.json"))
|
||||||
|
{
|
||||||
|
ColorScheme scheme;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scheme = ColorSchemeSerializer.Deserialize(File.ReadAllText(path));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!builtIns.ContainsKey(scheme.Name))
|
||||||
|
yield return scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ColorScheme BuildClassic() => new ColorScheme
|
||||||
|
{
|
||||||
|
Name = "Classic",
|
||||||
|
BackgroundColor = Color.DarkGray,
|
||||||
|
LayoutOutlineColor = Color.Gray,
|
||||||
|
LayoutFillColor = Color.WhiteSmoke,
|
||||||
|
BoundingBoxColor = Color.FromArgb(128, 128, 255),
|
||||||
|
RapidColor = Color.DodgerBlue,
|
||||||
|
OriginColor = Color.Gray,
|
||||||
|
EdgeSpacingColor = Color.FromArgb(180, 180, 180),
|
||||||
|
PreviewPartColor = Color.FromArgb(255, 140, 0),
|
||||||
|
PartColors = new[]
|
||||||
|
{
|
||||||
|
Color.FromArgb(205, 92, 92),
|
||||||
|
Color.FromArgb(148, 103, 189),
|
||||||
|
Color.FromArgb(75, 180, 175),
|
||||||
|
Color.FromArgb(210, 190, 75),
|
||||||
|
Color.FromArgb(190, 85, 175),
|
||||||
|
Color.FromArgb(185, 115, 85),
|
||||||
|
Color.FromArgb(120, 100, 190),
|
||||||
|
Color.FromArgb(200, 100, 140),
|
||||||
|
Color.FromArgb(80, 175, 155),
|
||||||
|
Color.FromArgb(195, 160, 85),
|
||||||
|
Color.FromArgb(175, 95, 160),
|
||||||
|
Color.FromArgb(215, 130, 130),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static ColorScheme BuildPastel() => new ColorScheme
|
||||||
|
{
|
||||||
|
Name = "Pastel",
|
||||||
|
BackgroundColor = Color.FromArgb(70, 75, 85),
|
||||||
|
LayoutOutlineColor = Color.FromArgb(180, 180, 190),
|
||||||
|
LayoutFillColor = Color.FromArgb(245, 245, 248),
|
||||||
|
BoundingBoxColor = Color.FromArgb(128, 128, 255),
|
||||||
|
RapidColor = Color.DodgerBlue,
|
||||||
|
OriginColor = Color.FromArgb(160, 160, 160),
|
||||||
|
EdgeSpacingColor = Color.FromArgb(200, 200, 210),
|
||||||
|
PreviewPartColor = Color.FromArgb(255, 140, 0),
|
||||||
|
PartColors = new[]
|
||||||
|
{
|
||||||
|
Color.FromArgb(122, 179, 209), Color.FromArgb(254, 229, 174),
|
||||||
|
Color.FromArgb(143, 177, 229), Color.FromArgb(167, 172, 227),
|
||||||
|
Color.FromArgb(216, 249, 195), Color.FromArgb(209, 168, 216),
|
||||||
|
Color.FromArgb(222, 157, 190), Color.FromArgb(176, 255, 240),
|
||||||
|
Color.FromArgb(235, 205, 153), Color.FromArgb(177, 225, 180),
|
||||||
|
Color.FromArgb(125, 202, 241), Color.FromArgb(187, 206, 151),
|
||||||
|
Color.FromArgb(251, 175, 190), Color.FromArgb(129, 226, 227),
|
||||||
|
Color.FromArgb(255, 253, 207), Color.FromArgb(235, 205, 255),
|
||||||
|
Color.FromArgb(255, 197, 168), Color.FromArgb(116, 213, 234),
|
||||||
|
Color.FromArgb(190, 169, 122), Color.FromArgb(213, 159, 135),
|
||||||
|
Color.FromArgb(124, 184, 155), Color.FromArgb(255, 189, 214),
|
||||||
|
Color.FromArgb(146, 222, 255), Color.FromArgb(177, 173, 125),
|
||||||
|
Color.FromArgb(177, 166, 202), Color.FromArgb(197, 208, 255),
|
||||||
|
Color.FromArgb(255, 209, 243), Color.FromArgb(210, 255, 237),
|
||||||
|
Color.FromArgb(255, 237, 204), Color.FromArgb(167, 233, 255),
|
||||||
|
Color.FromArgb(182, 220, 255), Color.FromArgb(159, 177, 142),
|
||||||
|
Color.FromArgb(190, 248, 255), Color.FromArgb(187, 169, 136),
|
||||||
|
Color.FromArgb(199, 162, 168), Color.FromArgb(250, 255, 239),
|
||||||
|
Color.FromArgb(222, 233, 255), Color.FromArgb(255, 234, 225),
|
||||||
|
Color.FromArgb(240, 249, 255), Color.FromArgb(152, 176, 176),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static ColorScheme BuildDark() => new ColorScheme
|
||||||
|
{
|
||||||
|
Name = "Dark",
|
||||||
|
BackgroundColor = Color.FromArgb(30, 30, 34),
|
||||||
|
LayoutOutlineColor = Color.FromArgb(90, 90, 95),
|
||||||
|
LayoutFillColor = Color.FromArgb(50, 50, 55),
|
||||||
|
BoundingBoxColor = Color.FromArgb(100, 160, 220),
|
||||||
|
RapidColor = Color.FromArgb(255, 200, 50),
|
||||||
|
OriginColor = Color.FromArgb(120, 120, 130),
|
||||||
|
EdgeSpacingColor = Color.FromArgb(90, 90, 100),
|
||||||
|
PreviewPartColor = Color.FromArgb(255, 170, 60),
|
||||||
|
PartColors = new[]
|
||||||
|
{
|
||||||
|
Color.FromArgb(255, 85, 85), // Neon Red
|
||||||
|
Color.FromArgb(80, 220, 255), // Electric Cyan
|
||||||
|
Color.FromArgb(255, 200, 50), // Amber
|
||||||
|
Color.FromArgb(130, 255, 130), // Lime Green
|
||||||
|
Color.FromArgb(255, 130, 220), // Hot Pink
|
||||||
|
Color.FromArgb(255, 165, 70), // Tangerine
|
||||||
|
Color.FromArgb(100, 180, 255), // Sky Blue
|
||||||
|
Color.FromArgb(200, 160, 255), // Lavender
|
||||||
|
Color.FromArgb(50, 230, 180), // Mint
|
||||||
|
Color.FromArgb(255, 255, 100), // Lemon
|
||||||
|
Color.FromArgb(255, 120, 120), // Salmon
|
||||||
|
Color.FromArgb(140, 230, 255), // Ice Blue
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public static class ColorSchemeSerializer
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string Serialize(ColorScheme scheme)
|
||||||
|
{
|
||||||
|
var dto = new ColorSchemeDto
|
||||||
|
{
|
||||||
|
Name = scheme.Name,
|
||||||
|
BackgroundColor = ToHex(scheme.BackgroundColor),
|
||||||
|
LayoutOutlineColor = ToHex(scheme.LayoutOutlineColor),
|
||||||
|
LayoutFillColor = ToHex(scheme.LayoutFillColor),
|
||||||
|
BoundingBoxColor = ToHex(scheme.BoundingBoxColor),
|
||||||
|
RapidColor = ToHex(scheme.RapidColor),
|
||||||
|
OriginColor = ToHex(scheme.OriginColor),
|
||||||
|
EdgeSpacingColor = ToHex(scheme.EdgeSpacingColor),
|
||||||
|
PreviewPartColor = ToHex(scheme.PreviewPartColor),
|
||||||
|
PartColors = scheme.PartColors.Select(ToHex).ToArray()
|
||||||
|
};
|
||||||
|
return JsonSerializer.Serialize(dto, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ColorScheme Deserialize(string json)
|
||||||
|
{
|
||||||
|
var dto = JsonSerializer.Deserialize<ColorSchemeDto>(json, JsonOptions)
|
||||||
|
?? throw new JsonException("ColorScheme JSON was null");
|
||||||
|
|
||||||
|
return new ColorScheme
|
||||||
|
{
|
||||||
|
Name = dto.Name ?? "Unnamed",
|
||||||
|
BackgroundColor = FromHex(dto.BackgroundColor),
|
||||||
|
LayoutOutlineColor = FromHex(dto.LayoutOutlineColor),
|
||||||
|
LayoutFillColor = FromHex(dto.LayoutFillColor),
|
||||||
|
BoundingBoxColor = FromHex(dto.BoundingBoxColor),
|
||||||
|
RapidColor = FromHex(dto.RapidColor),
|
||||||
|
OriginColor = FromHex(dto.OriginColor),
|
||||||
|
EdgeSpacingColor = FromHex(dto.EdgeSpacingColor),
|
||||||
|
PreviewPartColor = FromHex(dto.PreviewPartColor),
|
||||||
|
PartColors = (dto.PartColors ?? new string[0]).Select(FromHex).ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToHex(Color c) =>
|
||||||
|
"#" + c.R.ToString("X2") + c.G.ToString("X2") + c.B.ToString("X2");
|
||||||
|
|
||||||
|
private static Color FromHex(string hex)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hex))
|
||||||
|
return Color.Black;
|
||||||
|
var h = hex.TrimStart('#');
|
||||||
|
if (h.Length < 6)
|
||||||
|
return Color.Black;
|
||||||
|
var r = byte.Parse(h.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||||
|
var g = byte.Parse(h.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||||
|
var b = byte.Parse(h.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||||
|
return Color.FromArgb(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ColorSchemeDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string BackgroundColor { get; set; }
|
||||||
|
public string LayoutOutlineColor { get; set; }
|
||||||
|
public string LayoutFillColor { get; set; }
|
||||||
|
public string BoundingBoxColor { get; set; }
|
||||||
|
public string RapidColor { get; set; }
|
||||||
|
public string OriginColor { get; set; }
|
||||||
|
public string EdgeSpacingColor { get; set; }
|
||||||
|
public string PreviewPartColor { get; set; }
|
||||||
|
public string[] PartColors { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Controls
|
||||||
|
{
|
||||||
|
public class CadText
|
||||||
|
{
|
||||||
|
public Vector Position { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
public double Height { get; set; }
|
||||||
|
public double Rotation { get; set; }
|
||||||
|
public string LayerName { get; set; }
|
||||||
|
public StringAlignment HAlign { get; set; }
|
||||||
|
public StringAlignment VAlign { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
private readonly CheckBox chkTabsEnabled;
|
private readonly CheckBox chkTabsEnabled;
|
||||||
private readonly NumericUpDown nudTabWidth;
|
private readonly NumericUpDown nudTabWidth;
|
||||||
|
private readonly RadioButton rbTabAll;
|
||||||
|
private readonly RadioButton rbAutoTab;
|
||||||
private readonly NumericUpDown nudAutoTabMin;
|
private readonly NumericUpDown nudAutoTabMin;
|
||||||
private readonly NumericUpDown nudAutoTabMax;
|
private readonly NumericUpDown nudAutoTabMax;
|
||||||
private readonly NumericUpDown nudPierceClearance;
|
private readonly NumericUpDown nudPierceClearance;
|
||||||
@@ -112,7 +114,7 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
HeaderText = "Tabs",
|
HeaderText = "Tabs",
|
||||||
Dock = DockStyle.Top,
|
Dock = DockStyle.Top,
|
||||||
ExpandedHeight = 120,
|
ExpandedHeight = 160,
|
||||||
IsExpanded = false
|
IsExpanded = false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,44 +124,78 @@ namespace OpenNest.Controls
|
|||||||
Location = new Point(12, 4),
|
Location = new Point(12, 4),
|
||||||
AutoSize = true
|
AutoSize = true
|
||||||
};
|
};
|
||||||
chkTabsEnabled.CheckedChanged += (s, e) =>
|
|
||||||
{
|
|
||||||
nudTabWidth.Enabled = chkTabsEnabled.Checked;
|
|
||||||
OnParametersChanged();
|
|
||||||
};
|
|
||||||
tabsPanel.ContentPanel.Controls.Add(chkTabsEnabled);
|
tabsPanel.ContentPanel.Controls.Add(chkTabsEnabled);
|
||||||
|
|
||||||
tabsPanel.ContentPanel.Controls.Add(new Label
|
tabsPanel.ContentPanel.Controls.Add(new Label
|
||||||
{
|
{
|
||||||
Text = "Width:",
|
Text = "Tab Size:",
|
||||||
Location = new Point(160, 6),
|
Location = new Point(160, 6),
|
||||||
AutoSize = true
|
AutoSize = true
|
||||||
});
|
});
|
||||||
|
|
||||||
nudTabWidth = CreateNumeric(215, 3, 0.25, 0.0625);
|
nudTabWidth = CreateNumeric(225, 3, 0.25, 0.0625);
|
||||||
nudTabWidth.Enabled = false;
|
nudTabWidth.Enabled = false;
|
||||||
tabsPanel.ContentPanel.Controls.Add(nudTabWidth);
|
tabsPanel.ContentPanel.Controls.Add(nudTabWidth);
|
||||||
|
|
||||||
|
rbTabAll = new RadioButton
|
||||||
|
{
|
||||||
|
Text = "Tab all parts",
|
||||||
|
Location = new Point(28, 28),
|
||||||
|
AutoSize = true,
|
||||||
|
Enabled = false,
|
||||||
|
Checked = true
|
||||||
|
};
|
||||||
|
tabsPanel.ContentPanel.Controls.Add(rbTabAll);
|
||||||
|
|
||||||
|
rbAutoTab = new RadioButton
|
||||||
|
{
|
||||||
|
Text = "Auto-tab when smallest part dimension is between:",
|
||||||
|
Location = new Point(28, 50),
|
||||||
|
AutoSize = true,
|
||||||
|
Enabled = false
|
||||||
|
};
|
||||||
|
tabsPanel.ContentPanel.Controls.Add(rbAutoTab);
|
||||||
|
|
||||||
tabsPanel.ContentPanel.Controls.Add(new Label
|
tabsPanel.ContentPanel.Controls.Add(new Label
|
||||||
{
|
{
|
||||||
Text = "Auto-Tab Min Size:",
|
Text = "Min:",
|
||||||
Location = new Point(12, 32),
|
Location = new Point(44, 76),
|
||||||
AutoSize = true
|
AutoSize = true
|
||||||
});
|
});
|
||||||
|
|
||||||
nudAutoTabMin = CreateNumeric(140, 29, 0, 0.0625);
|
nudAutoTabMin = CreateNumeric(77, 73, 0, 0.0625);
|
||||||
|
nudAutoTabMin.Enabled = false;
|
||||||
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMin);
|
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMin);
|
||||||
|
|
||||||
tabsPanel.ContentPanel.Controls.Add(new Label
|
tabsPanel.ContentPanel.Controls.Add(new Label
|
||||||
{
|
{
|
||||||
Text = "Auto-Tab Max Size:",
|
Text = "Max:",
|
||||||
Location = new Point(12, 58),
|
Location = new Point(210, 76),
|
||||||
AutoSize = true
|
AutoSize = true
|
||||||
});
|
});
|
||||||
|
|
||||||
nudAutoTabMax = CreateNumeric(140, 55, 0, 0.0625);
|
nudAutoTabMax = CreateNumeric(245, 73, 0, 0.0625);
|
||||||
|
nudAutoTabMax.Enabled = false;
|
||||||
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMax);
|
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMax);
|
||||||
|
|
||||||
|
chkTabsEnabled.CheckedChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
var enabled = chkTabsEnabled.Checked;
|
||||||
|
nudTabWidth.Enabled = enabled;
|
||||||
|
rbTabAll.Enabled = enabled;
|
||||||
|
rbAutoTab.Enabled = enabled;
|
||||||
|
nudAutoTabMin.Enabled = enabled && rbAutoTab.Checked;
|
||||||
|
nudAutoTabMax.Enabled = enabled && rbAutoTab.Checked;
|
||||||
|
OnParametersChanged();
|
||||||
|
};
|
||||||
|
|
||||||
|
rbTabAll.CheckedChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
nudAutoTabMin.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked;
|
||||||
|
nudAutoTabMax.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked;
|
||||||
|
OnParametersChanged();
|
||||||
|
};
|
||||||
|
|
||||||
// Pierce section
|
// Pierce section
|
||||||
var piercePanel = new CollapsiblePanel
|
var piercePanel = new CollapsiblePanel
|
||||||
{
|
{
|
||||||
@@ -246,13 +282,13 @@ namespace OpenNest.Controls
|
|||||||
InternalLeadOut = BuildLeadOut(cboInternalLeadOut, pnlInternalLeadOut),
|
InternalLeadOut = BuildLeadOut(cboInternalLeadOut, pnlInternalLeadOut),
|
||||||
ArcCircleLeadIn = BuildLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn),
|
ArcCircleLeadIn = BuildLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn),
|
||||||
ArcCircleLeadOut = BuildLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut),
|
ArcCircleLeadOut = BuildLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut),
|
||||||
TabsEnabled = chkTabsEnabled.Checked,
|
TabsEnabled = chkTabsEnabled.Checked && rbTabAll.Checked,
|
||||||
TabConfig = new NormalTab { Size = (double)nudTabWidth.Value },
|
TabConfig = new NormalTab { Size = (double)nudTabWidth.Value },
|
||||||
PierceClearance = (double)nudPierceClearance.Value,
|
PierceClearance = (double)nudPierceClearance.Value,
|
||||||
RoundLeadInAngles = chkRoundLeadInAngles.Checked,
|
RoundLeadInAngles = chkRoundLeadInAngles.Checked,
|
||||||
LeadInAngleIncrement = (double)nudLeadInAngleIncrement.Value,
|
LeadInAngleIncrement = (double)nudLeadInAngleIncrement.Value,
|
||||||
AutoTabMinSize = (double)nudAutoTabMin.Value,
|
AutoTabMinSize = chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMin.Value : 0,
|
||||||
AutoTabMaxSize = (double)nudAutoTabMax.Value
|
AutoTabMaxSize = chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMax.Value : 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +303,10 @@ namespace OpenNest.Controls
|
|||||||
LoadLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn, p.ArcCircleLeadIn);
|
LoadLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn, p.ArcCircleLeadIn);
|
||||||
LoadLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut, p.ArcCircleLeadOut);
|
LoadLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut, p.ArcCircleLeadOut);
|
||||||
|
|
||||||
chkTabsEnabled.Checked = p.TabsEnabled;
|
var hasAutoTab = p.AutoTabMinSize > 0 || p.AutoTabMaxSize > 0;
|
||||||
|
chkTabsEnabled.Checked = p.TabsEnabled || hasAutoTab;
|
||||||
|
rbAutoTab.Checked = hasAutoTab;
|
||||||
|
rbTabAll.Checked = !hasAutoTab;
|
||||||
if (p.TabConfig != null)
|
if (p.TabConfig != null)
|
||||||
nudTabWidth.Value = (decimal)p.TabConfig.Size;
|
nudTabWidth.Value = (decimal)p.TabConfig.Size;
|
||||||
nudPierceClearance.Value = (decimal)p.PierceClearance;
|
nudPierceClearance.Value = (decimal)p.PierceClearance;
|
||||||
|
|||||||
@@ -29,15 +29,18 @@ namespace OpenNest.Controls
|
|||||||
public List<Entity> SimplifierToleranceRight { get; set; }
|
public List<Entity> SimplifierToleranceRight { get; set; }
|
||||||
public List<Entity> OriginalEntities { get; set; }
|
public List<Entity> OriginalEntities { get; set; }
|
||||||
public bool ShowEntityLabels { get; set; }
|
public bool ShowEntityLabels { get; set; }
|
||||||
|
public List<CadText> Texts { get; set; } = new List<CadText>();
|
||||||
|
|
||||||
private readonly Pen gridPen = new Pen(Color.FromArgb(70, 70, 70));
|
private readonly Pen gridPen = new Pen(Color.FromArgb(70, 70, 70));
|
||||||
private readonly Dictionary<int, Pen> penCache = new Dictionary<int, Pen>();
|
private readonly Dictionary<int, Pen> penCache = new Dictionary<int, Pen>();
|
||||||
private readonly Font labelFont = new Font("Segoe UI", 7f);
|
private readonly Font labelFont = new Font("Segoe UI", 7f);
|
||||||
private readonly SolidBrush labelBrush = new SolidBrush(Color.FromArgb(220, 255, 255, 200));
|
private readonly SolidBrush labelBrush = new SolidBrush(Color.FromArgb(220, 255, 255, 200));
|
||||||
private readonly SolidBrush labelBackBrush = new SolidBrush(Color.FromArgb(33, 40, 48));
|
private readonly SolidBrush labelBackBrush = new SolidBrush(Color.FromArgb(33, 40, 48));
|
||||||
|
private readonly SolidBrush textBrush = new SolidBrush(Color.FromArgb(180, 200, 200, 200));
|
||||||
|
|
||||||
public event EventHandler<Line> LinePicked;
|
public event EventHandler<Line> LinePicked;
|
||||||
public event EventHandler PickCancelled;
|
public event EventHandler PickCancelled;
|
||||||
|
public event EventHandler<CadText> TextConvertRequested;
|
||||||
|
|
||||||
private bool isPickingBendLine;
|
private bool isPickingBendLine;
|
||||||
public bool IsPickingBendLine
|
public bool IsPickingBendLine
|
||||||
@@ -74,6 +77,13 @@ namespace OpenNest.Controls
|
|||||||
if (line != null)
|
if (line != null)
|
||||||
LinePicked?.Invoke(this, line);
|
LinePicked?.Invoke(this, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.Button == MouseButtons.Right)
|
||||||
|
{
|
||||||
|
var text = HitTestText(e.Location);
|
||||||
|
if (text != null)
|
||||||
|
ShowTextContextMenu(text, e.Location);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnPaint(PaintEventArgs e)
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
@@ -116,6 +126,8 @@ namespace OpenNest.Controls
|
|||||||
DrawEntity(e.Graphics, entity, pen);
|
DrawEntity(e.Graphics, entity, pen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DrawTexts(e.Graphics);
|
||||||
|
|
||||||
if (ShowEntityLabels)
|
if (ShowEntityLabels)
|
||||||
DrawEntityLabels(e.Graphics);
|
DrawEntityLabels(e.Graphics);
|
||||||
|
|
||||||
@@ -324,6 +336,41 @@ namespace OpenNest.Controls
|
|||||||
return bestLine;
|
return bestLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CadText HitTestText(Point controlPoint)
|
||||||
|
{
|
||||||
|
if (Texts == null || Texts.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var worldPoint = PointControlToWorld(controlPoint);
|
||||||
|
var tolerance = LengthGuiToWorld(8);
|
||||||
|
|
||||||
|
foreach (var text in Texts)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text.Value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var estimatedWidth = text.Height * text.Value.Length * 0.6;
|
||||||
|
var minX = text.Position.X - tolerance;
|
||||||
|
var maxX = text.Position.X + estimatedWidth + tolerance;
|
||||||
|
var minY = text.Position.Y - tolerance;
|
||||||
|
var maxY = text.Position.Y + text.Height + tolerance;
|
||||||
|
|
||||||
|
if (worldPoint.X >= minX && worldPoint.X <= maxX &&
|
||||||
|
worldPoint.Y >= minY && worldPoint.Y <= maxY)
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowTextContextMenu(CadText text, Point location)
|
||||||
|
{
|
||||||
|
var menu = new ContextMenuStrip();
|
||||||
|
var item = menu.Items.Add($"Convert \"{text.Value}\" to Geometry");
|
||||||
|
item.Click += (s, e) => TextConvertRequested?.Invoke(this, text);
|
||||||
|
menu.Show(this, location);
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawEntityLabels(Graphics g)
|
private void DrawEntityLabels(Graphics g)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < Entities.Count; i++)
|
for (var i = 0; i < Entities.Count; i++)
|
||||||
@@ -408,6 +455,7 @@ namespace OpenNest.Controls
|
|||||||
labelFont.Dispose();
|
labelFont.Dispose();
|
||||||
labelBrush.Dispose();
|
labelBrush.Dispose();
|
||||||
labelBackBrush.Dispose();
|
labelBackBrush.Dispose();
|
||||||
|
textBrush.Dispose();
|
||||||
}
|
}
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
@@ -474,6 +522,34 @@ namespace OpenNest.Controls
|
|||||||
diameter);
|
diameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawTexts(Graphics g)
|
||||||
|
{
|
||||||
|
if (Texts == null || Texts.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var sf = new StringFormat();
|
||||||
|
|
||||||
|
foreach (var text in Texts)
|
||||||
|
{
|
||||||
|
var pos = PointWorldToGraph(text.Position);
|
||||||
|
var fontSize = LengthWorldToGui(text.Height);
|
||||||
|
if (fontSize < 2f) continue;
|
||||||
|
|
||||||
|
var state = g.Save();
|
||||||
|
g.TranslateTransform(pos.X, pos.Y);
|
||||||
|
|
||||||
|
if (text.Rotation != 0)
|
||||||
|
g.RotateTransform((float)OpenNest.Math.Angle.ToDegrees(text.Rotation));
|
||||||
|
|
||||||
|
sf.Alignment = text.HAlign;
|
||||||
|
sf.LineAlignment = text.VAlign;
|
||||||
|
|
||||||
|
using var font = new Font("Segoe UI", fontSize, GraphicsUnit.Pixel);
|
||||||
|
g.DrawString(text.Value, font, textBrush, 0, 0, sf);
|
||||||
|
g.Restore(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawPoint(Graphics g, Vector pt, Pen pen)
|
private void DrawPoint(Graphics g, Vector pt, Pen pen)
|
||||||
{
|
{
|
||||||
var pt1 = PointWorldToGraph(pt);
|
var pt1 = PointWorldToGraph(pt);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ namespace OpenNest.Controls
|
|||||||
public HashSet<Guid> SuppressedEntityIds { get; set; }
|
public HashSet<Guid> SuppressedEntityIds { get; set; }
|
||||||
public Box Bounds { get; set; }
|
public Box Bounds { get; set; }
|
||||||
public int EntityCount { get; set; }
|
public int EntityCount { get; set; }
|
||||||
|
public List<CadText> Texts { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FileListControl : Control
|
public class FileListControl : Control
|
||||||
|
|||||||
@@ -385,85 +385,20 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
private void DrawRapids(Graphics g)
|
private void DrawRapids(Graphics g)
|
||||||
{
|
{
|
||||||
|
var pen = view.ColorScheme.RapidPen;
|
||||||
var pos = new Vector(0, 0);
|
var pos = new Vector(0, 0);
|
||||||
|
|
||||||
for (var i = 0; i < view.Plate.Parts.Count; ++i)
|
for (var i = 0; i < view.Plate.Parts.Count; ++i)
|
||||||
{
|
{
|
||||||
var part = view.Plate.Parts[i];
|
var part = view.Plate.Parts[i];
|
||||||
var pgm = part.Program;
|
var segments = RapidEnumerator.Enumerate(part.Program, part.Location, pos);
|
||||||
|
|
||||||
var piercePoint = GetFirstPiercePoint(pgm, part.Location);
|
foreach (var seg in segments)
|
||||||
DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen);
|
|
||||||
|
|
||||||
pos = piercePoint;
|
|
||||||
DrawRapids(g, pgm, part.Location, ref pos, skipFirstRapid: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector GetFirstPiercePoint(Program pgm, Vector partLocation)
|
|
||||||
{
|
{
|
||||||
for (var i = 0; i < pgm.Length; i++)
|
DrawLine(g, seg.From, seg.To, pen);
|
||||||
{
|
pos = seg.To;
|
||||||
if (pgm[i] is SubProgramCall call && call.Program != null)
|
|
||||||
return GetFirstPiercePoint(call.Program, partLocation + call.Offset);
|
|
||||||
|
|
||||||
if (pgm[i] is Motion motion)
|
|
||||||
{
|
|
||||||
return motion.EndPoint + partLocation;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return partLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawRapids(Graphics g, Program pgm, Vector basePos, ref Vector pos, bool skipFirstRapid = false)
|
|
||||||
{
|
|
||||||
var firstRapidSkipped = false;
|
|
||||||
|
|
||||||
for (var i = 0; i < pgm.Length; ++i)
|
|
||||||
{
|
|
||||||
var code = pgm[i];
|
|
||||||
|
|
||||||
if (code is SubProgramCall { Program: { } program } call)
|
|
||||||
{
|
|
||||||
// A SubProgramCall is a coordinate-frame shift, not a physical
|
|
||||||
// rapid to the hole center. The Cincinnati post emits it as a
|
|
||||||
// G52 bracket, so the physical rapid is the sub-program's first
|
|
||||||
// motion, which goes straight from here to the lead-in pierce.
|
|
||||||
// Look ahead for that pierce point and draw the direct rapid,
|
|
||||||
// then recurse with skipFirstRapid so the sub doesn't also draw
|
|
||||||
// its first rapid on top. See docs/cincinnati-post-output.md.
|
|
||||||
var holeBase = basePos + call.Offset;
|
|
||||||
var firstPierce = GetFirstPiercePoint(program, holeBase);
|
|
||||||
|
|
||||||
if (ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
|
|
||||||
DrawLine(g, pos, firstPierce, view.ColorScheme.RapidPen);
|
|
||||||
|
|
||||||
var subPos = holeBase;
|
|
||||||
DrawRapids(g, program, holeBase, ref subPos, skipFirstRapid: true);
|
|
||||||
pos = subPos;
|
|
||||||
}
|
|
||||||
else if (code is Motion motion)
|
|
||||||
{
|
|
||||||
var endpt = pgm.Mode == Mode.Incremental
|
|
||||||
? motion.EndPoint + pos
|
|
||||||
: motion.EndPoint;
|
|
||||||
|
|
||||||
if (code.Type == CodeType.RapidMove && ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
|
|
||||||
DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
|
|
||||||
|
|
||||||
pos = endpt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ShouldDrawRapid(bool skipFirstRapid, ref bool firstRapidSkipped)
|
|
||||||
{
|
|
||||||
if (skipFirstRapid && !firstRapidSkipped)
|
|
||||||
{
|
|
||||||
firstRapidSkipped = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawAllPiercePoints(Graphics g)
|
private void DrawAllPiercePoints(Graphics g)
|
||||||
|
|||||||
@@ -464,6 +464,9 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
protected override void OnPaint(PaintEventArgs e)
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (BackColor != ColorScheme.BackgroundColor)
|
||||||
|
BackColor = ColorScheme.BackgroundColor;
|
||||||
|
|
||||||
e.Graphics.SmoothingMode = SmoothingMode.HighSpeed;
|
e.Graphics.SmoothingMode = SmoothingMode.HighSpeed;
|
||||||
|
|
||||||
if (DrawOrigin)
|
if (DrawOrigin)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
public BestFitResult SelectedResult { get; private set; }
|
public BestFitResult SelectedResult { get; private set; }
|
||||||
public Drawing SelectedDrawing => activeDrawing;
|
public Drawing SelectedDrawing => activeDrawing;
|
||||||
|
public List<Part> SelectedParts { get; private set; }
|
||||||
|
|
||||||
public BestFitViewerForm(DrawingCollection drawings, Plate plate, Units units = Units.Inches)
|
public BestFitViewerForm(DrawingCollection drawings, Plate plate, Units units = Units.Inches)
|
||||||
{
|
{
|
||||||
@@ -318,12 +319,12 @@ namespace OpenNest.Forms
|
|||||||
var cell = new BestFitCell(colorScheme);
|
var cell = new BestFitCell(colorScheme);
|
||||||
cell.PartColor = partColor;
|
cell.PartColor = partColor;
|
||||||
cell.Dock = DockStyle.Fill;
|
cell.Dock = DockStyle.Fill;
|
||||||
|
|
||||||
|
var parts = result.BuildCanonicalParts();
|
||||||
cell.Plate.Size = new Geometry.Size(
|
cell.Plate.Size = new Geometry.Size(
|
||||||
result.BoundingHeight,
|
result.BoundingHeight,
|
||||||
result.BoundingWidth);
|
result.BoundingWidth);
|
||||||
|
|
||||||
var parts = result.BuildParts(drawing);
|
|
||||||
|
|
||||||
foreach (var part in parts)
|
foreach (var part in parts)
|
||||||
cell.Plate.Parts.Add(part);
|
cell.Plate.Parts.Add(part);
|
||||||
|
|
||||||
@@ -332,6 +333,7 @@ namespace OpenNest.Forms
|
|||||||
cell.DoubleClick += (sender, e) =>
|
cell.DoubleClick += (sender, e) =>
|
||||||
{
|
{
|
||||||
SelectedResult = result;
|
SelectedResult = result;
|
||||||
|
SelectedParts = result.BuildSourceParts(drawing);
|
||||||
DialogResult = DialogResult.OK;
|
DialogResult = DialogResult.OK;
|
||||||
Close();
|
Close();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ namespace OpenNest.Forms
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var lookupName = item.FileName;
|
var lookupName = item.FileName;
|
||||||
if (lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase))
|
if (lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| lookupName.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase))
|
||||||
lookupName = Path.GetFileNameWithoutExtension(lookupName);
|
lookupName = Path.GetFileNameWithoutExtension(lookupName);
|
||||||
|
|
||||||
if (matchedPaths.TryGetValue(lookupName, out var dxfPath))
|
if (matchedPaths.TryGetValue(lookupName, out var dxfPath))
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ namespace OpenNest.Forms
|
|||||||
filterPanel.AddBendLineClicked += OnAddBendLineClicked;
|
filterPanel.AddBendLineClicked += OnAddBendLineClicked;
|
||||||
entityView1.LinePicked += OnLinePicked;
|
entityView1.LinePicked += OnLinePicked;
|
||||||
entityView1.PickCancelled += OnPickCancelled;
|
entityView1.PickCancelled += OnPickCancelled;
|
||||||
|
entityView1.TextConvertRequested += OnTextConvertRequested;
|
||||||
btnSplit.Click += OnSplitClicked;
|
btnSplit.Click += OnSplitClicked;
|
||||||
numQuantity.ValueChanged += OnQuantityChanged;
|
numQuantity.ValueChanged += OnQuantityChanged;
|
||||||
txtCustomer.TextChanged += OnCustomerChanged;
|
txtCustomer.TextChanged += OnCustomerChanged;
|
||||||
@@ -92,7 +93,8 @@ namespace OpenNest.Forms
|
|||||||
Customer = string.Empty,
|
Customer = string.Empty,
|
||||||
Bends = result.Bends,
|
Bends = result.Bends,
|
||||||
Bounds = result.Bounds,
|
Bounds = result.Bounds,
|
||||||
EntityCount = result.Entities.Count
|
EntityCount = result.Entities.Count,
|
||||||
|
Texts = ExtractTexts(result.Document),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (InvokeRequired)
|
if (InvokeRequired)
|
||||||
@@ -152,6 +154,7 @@ namespace OpenNest.Forms
|
|||||||
entityView1.Entities.Clear();
|
entityView1.Entities.Clear();
|
||||||
entityView1.Entities.AddRange(item.Entities);
|
entityView1.Entities.AddRange(item.Entities);
|
||||||
entityView1.Bends = item.Bends ?? new List<Bend>();
|
entityView1.Bends = item.Bends ?? new List<Bend>();
|
||||||
|
entityView1.Texts = item.Texts ?? new List<CadText>();
|
||||||
|
|
||||||
item.Entities.ForEach(e => e.IsVisible = true);
|
item.Entities.ForEach(e => e.IsVisible = true);
|
||||||
if (item.Entities.Any(e => e.Layer != null))
|
if (item.Entities.Any(e => e.Layer != null))
|
||||||
@@ -461,6 +464,115 @@ namespace OpenNest.Forms
|
|||||||
filterPanel.SetPickMode(false);
|
filterPanel.SetPickMode(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnTextConvertRequested(object sender, Controls.CadText text)
|
||||||
|
{
|
||||||
|
var item = CurrentItem;
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
|
var font = LoadChrFont();
|
||||||
|
if (font == null) return;
|
||||||
|
|
||||||
|
var layer = new Geometry.Layer("ENGRAVE")
|
||||||
|
{
|
||||||
|
Color = System.Drawing.Color.Cyan,
|
||||||
|
IsVisible = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var entities = font.RenderText(text.Value, text.Height, Geometry.Vector.Zero, layer);
|
||||||
|
if (entities.Count > 0)
|
||||||
|
{
|
||||||
|
var box = entities.GetBoundingBox();
|
||||||
|
var shiftX = text.HAlign switch
|
||||||
|
{
|
||||||
|
System.Drawing.StringAlignment.Center => text.Position.X - (box.Left + box.Right) / 2,
|
||||||
|
System.Drawing.StringAlignment.Far => text.Position.X - box.Right,
|
||||||
|
_ => text.Position.X - box.Left,
|
||||||
|
};
|
||||||
|
var shiftY = text.VAlign switch
|
||||||
|
{
|
||||||
|
System.Drawing.StringAlignment.Center => text.Position.Y - (box.Top + box.Bottom) / 2,
|
||||||
|
System.Drawing.StringAlignment.Near => text.Position.Y - box.Top,
|
||||||
|
_ => text.Position.Y - box.Bottom,
|
||||||
|
};
|
||||||
|
var shift = new Geometry.Vector(shiftX, shiftY);
|
||||||
|
foreach (var e in entities)
|
||||||
|
e.Offset(shift);
|
||||||
|
}
|
||||||
|
if (entities.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"No geometry produced for \"{text.Value}\".", "Convert Text",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Entities.AddRange(entities);
|
||||||
|
item.Texts.Remove(text);
|
||||||
|
item.EntityCount = item.Entities.Count;
|
||||||
|
item.Bounds = item.Entities.GetBoundingBox();
|
||||||
|
|
||||||
|
entityView1.Entities.Clear();
|
||||||
|
entityView1.Entities.AddRange(item.Entities);
|
||||||
|
entityView1.Texts = item.Texts;
|
||||||
|
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||||
|
entityView1.Invalidate();
|
||||||
|
staleProgram = true;
|
||||||
|
lblEntityCount.Text = $"{item.EntityCount} entities";
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChrFont cachedChrFont;
|
||||||
|
private string cachedChrFontPath;
|
||||||
|
|
||||||
|
private ChrFont LoadChrFont()
|
||||||
|
{
|
||||||
|
if (cachedChrFont != null)
|
||||||
|
return cachedChrFont;
|
||||||
|
|
||||||
|
// Look for .CHR files next to the app, then prompt
|
||||||
|
var appDir = System.IO.Path.GetDirectoryName(Application.ExecutablePath);
|
||||||
|
var candidates = Directory.GetFiles(appDir, "*.CHR", SearchOption.TopDirectoryOnly);
|
||||||
|
|
||||||
|
string fontPath;
|
||||||
|
if (candidates.Length == 1)
|
||||||
|
{
|
||||||
|
fontPath = candidates[0];
|
||||||
|
}
|
||||||
|
else if (candidates.Length > 1)
|
||||||
|
{
|
||||||
|
fontPath = PromptForChrFile(appDir);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fontPath = PromptForChrFile(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fontPath == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cachedChrFont = ChrFont.Read(fontPath);
|
||||||
|
cachedChrFontPath = fontPath;
|
||||||
|
return cachedChrFont;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Error loading font: {ex.Message}", "Font Error",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PromptForChrFile(string initialDir)
|
||||||
|
{
|
||||||
|
using var dlg = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Title = "Select Engraving Font (.CHR)",
|
||||||
|
Filter = "Gravograph Font (*.CHR)|*.CHR",
|
||||||
|
InitialDirectory = initialDir ?? "",
|
||||||
|
};
|
||||||
|
return dlg.ShowDialog() == DialogResult.OK ? dlg.FileName : null;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDragEnter(object sender, DragEventArgs e)
|
private void OnDragEnter(object sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||||
@@ -473,7 +585,8 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
|
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
|
||||||
var dxfFiles = files.Where(f =>
|
var dxfFiles = files.Where(f =>
|
||||||
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)).ToArray();
|
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
f.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||||
if (dxfFiles.Length > 0)
|
if (dxfFiles.Length > 0)
|
||||||
AddFiles(dxfFiles);
|
AddFiles(dxfFiles);
|
||||||
}
|
}
|
||||||
@@ -803,6 +916,110 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
private static List<CadText> ExtractTexts(ACadSharp.CadDocument doc)
|
||||||
|
{
|
||||||
|
var texts = new List<CadText>();
|
||||||
|
if (doc == null) return texts;
|
||||||
|
|
||||||
|
foreach (var entity in doc.Entities)
|
||||||
|
{
|
||||||
|
switch (entity)
|
||||||
|
{
|
||||||
|
case ACadSharp.Entities.MText mtext:
|
||||||
|
var (mh, mv) = MapAttachmentPoint(mtext.AttachmentPoint);
|
||||||
|
texts.Add(new CadText
|
||||||
|
{
|
||||||
|
Position = new Vector(mtext.InsertPoint.X, mtext.InsertPoint.Y),
|
||||||
|
Value = ReplaceControlCodes(StripMTextFormatting(mtext.Value)),
|
||||||
|
Height = mtext.Height,
|
||||||
|
Rotation = mtext.Rotation,
|
||||||
|
LayerName = mtext.Layer?.Name,
|
||||||
|
HAlign = mh,
|
||||||
|
VAlign = mv,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ACadSharp.Entities.TextEntity text:
|
||||||
|
var useAlignment = text.HorizontalAlignment != 0
|
||||||
|
|| text.VerticalAlignment != 0;
|
||||||
|
var pt = useAlignment ? text.AlignmentPoint : text.InsertPoint;
|
||||||
|
var ha = text.HorizontalAlignment switch
|
||||||
|
{
|
||||||
|
ACadSharp.Entities.TextHorizontalAlignment.Center => System.Drawing.StringAlignment.Center,
|
||||||
|
ACadSharp.Entities.TextHorizontalAlignment.Right => System.Drawing.StringAlignment.Far,
|
||||||
|
_ => System.Drawing.StringAlignment.Near,
|
||||||
|
};
|
||||||
|
var va = text.VerticalAlignment switch
|
||||||
|
{
|
||||||
|
ACadSharp.Entities.TextVerticalAlignmentType.Middle => System.Drawing.StringAlignment.Center,
|
||||||
|
ACadSharp.Entities.TextVerticalAlignmentType.Top => System.Drawing.StringAlignment.Near,
|
||||||
|
ACadSharp.Entities.TextVerticalAlignmentType.Bottom => System.Drawing.StringAlignment.Far,
|
||||||
|
_ => System.Drawing.StringAlignment.Far,
|
||||||
|
};
|
||||||
|
texts.Add(new CadText
|
||||||
|
{
|
||||||
|
Position = new Vector(pt.X, pt.Y),
|
||||||
|
Value = ReplaceControlCodes(text.Value),
|
||||||
|
Height = text.Height,
|
||||||
|
Rotation = text.Rotation,
|
||||||
|
LayerName = text.Layer?.Name,
|
||||||
|
HAlign = ha,
|
||||||
|
VAlign = va,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return texts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (System.Drawing.StringAlignment h, System.Drawing.StringAlignment v) MapAttachmentPoint(
|
||||||
|
ACadSharp.Entities.AttachmentPointType apt)
|
||||||
|
{
|
||||||
|
var h = apt switch
|
||||||
|
{
|
||||||
|
ACadSharp.Entities.AttachmentPointType.TopCenter
|
||||||
|
or ACadSharp.Entities.AttachmentPointType.MiddleCenter
|
||||||
|
or ACadSharp.Entities.AttachmentPointType.BottomCenter => System.Drawing.StringAlignment.Center,
|
||||||
|
ACadSharp.Entities.AttachmentPointType.TopRight
|
||||||
|
or ACadSharp.Entities.AttachmentPointType.MiddleRight
|
||||||
|
or ACadSharp.Entities.AttachmentPointType.BottomRight => System.Drawing.StringAlignment.Far,
|
||||||
|
_ => System.Drawing.StringAlignment.Near,
|
||||||
|
};
|
||||||
|
var v = apt switch
|
||||||
|
{
|
||||||
|
ACadSharp.Entities.AttachmentPointType.MiddleLeft
|
||||||
|
or ACadSharp.Entities.AttachmentPointType.MiddleCenter
|
||||||
|
or ACadSharp.Entities.AttachmentPointType.MiddleRight => System.Drawing.StringAlignment.Center,
|
||||||
|
ACadSharp.Entities.AttachmentPointType.BottomLeft
|
||||||
|
or ACadSharp.Entities.AttachmentPointType.BottomCenter
|
||||||
|
or ACadSharp.Entities.AttachmentPointType.BottomRight => System.Drawing.StringAlignment.Far,
|
||||||
|
_ => System.Drawing.StringAlignment.Near,
|
||||||
|
};
|
||||||
|
return (h, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripMTextFormatting(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return text;
|
||||||
|
var result = System.Text.RegularExpressions.Regex.Replace(text, @"\\[A-Za-z][^;]*;", "");
|
||||||
|
result = result.Replace("{", "").Replace("}", "");
|
||||||
|
return result.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReplaceControlCodes(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return text;
|
||||||
|
return text
|
||||||
|
.Replace("%%p", "±")
|
||||||
|
.Replace("%%P", "±")
|
||||||
|
.Replace("%%d", "°")
|
||||||
|
.Replace("%%D", "°")
|
||||||
|
.Replace("%%c", "⌀")
|
||||||
|
.Replace("%%C", "⌀")
|
||||||
|
.Replace("%%%", "%");
|
||||||
|
}
|
||||||
|
|
||||||
private void filterPanel_Paint(object sender, PaintEventArgs e)
|
private void filterPanel_Paint(object sender, PaintEventArgs e)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
Generated
+12
-21
@@ -47,11 +47,9 @@
|
|||||||
drawingListBox1 = new OpenNest.Controls.DrawingListBox();
|
drawingListBox1 = new OpenNest.Controls.DrawingListBox();
|
||||||
toolStrip2 = new System.Windows.Forms.ToolStrip();
|
toolStrip2 = new System.Windows.Forms.ToolStrip();
|
||||||
toolStripButton2 = new System.Windows.Forms.ToolStripButton();
|
toolStripButton2 = new System.Windows.Forms.ToolStripButton();
|
||||||
toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
|
shapeLibraryButton = new System.Windows.Forms.ToolStripButton();
|
||||||
editDrawingsButton = new System.Windows.Forms.ToolStripButton();
|
editDrawingsButton = new System.Windows.Forms.ToolStripButton();
|
||||||
toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
|
|
||||||
toolStripButton3 = new System.Windows.Forms.ToolStripButton();
|
toolStripButton3 = new System.Windows.Forms.ToolStripButton();
|
||||||
toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
|
||||||
hideNestedButton = new System.Windows.Forms.ToolStripButton();
|
hideNestedButton = new System.Windows.Forms.ToolStripButton();
|
||||||
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
|
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
|
||||||
splitContainer.Panel1.SuspendLayout();
|
splitContainer.Panel1.SuspendLayout();
|
||||||
@@ -81,8 +79,8 @@
|
|||||||
//
|
//
|
||||||
// tabControl1
|
// tabControl1
|
||||||
//
|
//
|
||||||
tabControl1.Controls.Add(tabPage1);
|
|
||||||
tabControl1.Controls.Add(tabPage2);
|
tabControl1.Controls.Add(tabPage2);
|
||||||
|
tabControl1.Controls.Add(tabPage1);
|
||||||
tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
|
tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
tabControl1.ItemSize = new System.Drawing.Size(100, 22);
|
tabControl1.ItemSize = new System.Drawing.Size(100, 22);
|
||||||
tabControl1.Location = new System.Drawing.Point(0, 0);
|
tabControl1.Location = new System.Drawing.Point(0, 0);
|
||||||
@@ -219,7 +217,7 @@
|
|||||||
//
|
//
|
||||||
toolStrip2.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
|
toolStrip2.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
|
||||||
toolStrip2.ImageScalingSize = new System.Drawing.Size(20, 20);
|
toolStrip2.ImageScalingSize = new System.Drawing.Size(20, 20);
|
||||||
toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripButton2, toolStripSeparator4, editDrawingsButton, toolStripSeparator1, toolStripButton3, toolStripSeparator2, hideNestedButton });
|
toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripButton2, shapeLibraryButton, editDrawingsButton, toolStripButton3, hideNestedButton });
|
||||||
toolStrip2.Location = new System.Drawing.Point(4, 3);
|
toolStrip2.Location = new System.Drawing.Point(4, 3);
|
||||||
toolStrip2.Name = "toolStrip2";
|
toolStrip2.Name = "toolStrip2";
|
||||||
toolStrip2.Size = new System.Drawing.Size(265, 27);
|
toolStrip2.Size = new System.Drawing.Size(265, 27);
|
||||||
@@ -238,10 +236,15 @@
|
|||||||
toolStripButton2.Text = "Import Drawings";
|
toolStripButton2.Text = "Import Drawings";
|
||||||
toolStripButton2.Click += ImportDrawings_Click;
|
toolStripButton2.Click += ImportDrawings_Click;
|
||||||
//
|
//
|
||||||
// toolStripSeparator4
|
// shapeLibraryButton
|
||||||
//
|
//
|
||||||
toolStripSeparator4.Name = "toolStripSeparator4";
|
shapeLibraryButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
|
||||||
toolStripSeparator4.Size = new System.Drawing.Size(6, 27);
|
shapeLibraryButton.Image = Properties.Resources.shapes;
|
||||||
|
shapeLibraryButton.Name = "shapeLibraryButton";
|
||||||
|
shapeLibraryButton.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
|
||||||
|
shapeLibraryButton.Size = new System.Drawing.Size(34, 24);
|
||||||
|
shapeLibraryButton.Text = "Shape Library";
|
||||||
|
shapeLibraryButton.Click += ShapeLibrary_Click;
|
||||||
//
|
//
|
||||||
// editDrawingsButton
|
// editDrawingsButton
|
||||||
//
|
//
|
||||||
@@ -253,11 +256,6 @@
|
|||||||
editDrawingsButton.Text = "Edit Drawings in Converter";
|
editDrawingsButton.Text = "Edit Drawings in Converter";
|
||||||
editDrawingsButton.Click += EditDrawingsInConverter_Click;
|
editDrawingsButton.Click += EditDrawingsInConverter_Click;
|
||||||
//
|
//
|
||||||
// toolStripSeparator1
|
|
||||||
//
|
|
||||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
|
||||||
toolStripSeparator1.Size = new System.Drawing.Size(6, 27);
|
|
||||||
//
|
|
||||||
// toolStripButton3
|
// toolStripButton3
|
||||||
//
|
//
|
||||||
toolStripButton3.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
|
toolStripButton3.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
|
||||||
@@ -269,11 +267,6 @@
|
|||||||
toolStripButton3.Text = "Cleanup unused Drawings";
|
toolStripButton3.Text = "Cleanup unused Drawings";
|
||||||
toolStripButton3.Click += CleanUnusedDrawings_Click;
|
toolStripButton3.Click += CleanUnusedDrawings_Click;
|
||||||
//
|
//
|
||||||
// toolStripSeparator2
|
|
||||||
//
|
|
||||||
toolStripSeparator2.Name = "toolStripSeparator2";
|
|
||||||
toolStripSeparator2.Size = new System.Drawing.Size(6, 27);
|
|
||||||
//
|
|
||||||
// hideNestedButton
|
// hideNestedButton
|
||||||
//
|
//
|
||||||
hideNestedButton.CheckOnClick = true;
|
hideNestedButton.CheckOnClick = true;
|
||||||
@@ -329,11 +322,9 @@
|
|||||||
private System.Windows.Forms.ColumnHeader utilColumn;
|
private System.Windows.Forms.ColumnHeader utilColumn;
|
||||||
private System.Windows.Forms.ToolStrip toolStrip2;
|
private System.Windows.Forms.ToolStrip toolStrip2;
|
||||||
private System.Windows.Forms.ToolStripButton toolStripButton2;
|
private System.Windows.Forms.ToolStripButton toolStripButton2;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
|
private System.Windows.Forms.ToolStripButton shapeLibraryButton;
|
||||||
private System.Windows.Forms.ToolStripButton editDrawingsButton;
|
private System.Windows.Forms.ToolStripButton editDrawingsButton;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
|
||||||
private System.Windows.Forms.ToolStripButton toolStripButton3;
|
private System.Windows.Forms.ToolStripButton toolStripButton3;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
|
|
||||||
private System.Windows.Forms.ToolStripButton hideNestedButton;
|
private System.Windows.Forms.ToolStripButton hideNestedButton;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
|
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
|
||||||
private System.Windows.Forms.ToolStripButton toolStripLabel1;
|
private System.Windows.Forms.ToolStripButton toolStripLabel1;
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
var dlg = new OpenFileDialog();
|
var dlg = new OpenFileDialog();
|
||||||
dlg.Multiselect = true;
|
dlg.Multiselect = true;
|
||||||
dlg.Filter = "DXF Files (*.dxf) | *.dxf";
|
dlg.Filter = "CAD Files (*.dxf;*.dwg)|*.dxf;*.dwg|DXF Files (*.dxf)|*.dxf|DWG Files (*.dwg)|*.dwg";
|
||||||
|
|
||||||
if (dlg.ShowDialog() != DialogResult.OK)
|
if (dlg.ShowDialog() != DialogResult.OK)
|
||||||
return;
|
return;
|
||||||
@@ -346,7 +346,6 @@ namespace OpenNest.Forms
|
|||||||
drawings.ForEach(d => Nest.Drawings.Add(d));
|
drawings.ForEach(d => Nest.Drawings.Add(d));
|
||||||
|
|
||||||
UpdateDrawingList();
|
UpdateDrawingList();
|
||||||
tabControl1.SelectedIndex = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Export()
|
public bool Export()
|
||||||
@@ -875,6 +874,18 @@ namespace OpenNest.Forms
|
|||||||
Import();
|
Import();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShapeLibrary_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var form = new ShapeLibraryForm(Nest.Drawings.Select(d => d.Name));
|
||||||
|
form.ShowDialog();
|
||||||
|
|
||||||
|
var drawings = form.GetDrawings();
|
||||||
|
if (drawings.Count == 0) return;
|
||||||
|
|
||||||
|
drawings.ForEach(d => Nest.Drawings.Add(d));
|
||||||
|
UpdateDrawingList();
|
||||||
|
}
|
||||||
|
|
||||||
private void EditDrawingsInConverter_Click(object sender, EventArgs e)
|
private void EditDrawingsInConverter_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (Nest.Drawings.Count == 0)
|
if (Nest.Drawings.Count == 0)
|
||||||
|
|||||||
+4
-3
@@ -63,7 +63,7 @@
|
|||||||
this.textBox2 = new System.Windows.Forms.TextBox();
|
this.textBox2 = new System.Windows.Forms.TextBox();
|
||||||
this.label5 = new System.Windows.Forms.Label();
|
this.label5 = new System.Windows.Forms.Label();
|
||||||
this.labelMaterial = new System.Windows.Forms.Label();
|
this.labelMaterial = new System.Windows.Forms.Label();
|
||||||
this.materialBox = new System.Windows.Forms.TextBox();
|
this.materialBox = new System.Windows.Forms.ComboBox();
|
||||||
this.tabPage2 = new System.Windows.Forms.TabPage();
|
this.tabPage2 = new System.Windows.Forms.TabPage();
|
||||||
this.tabPage3 = new System.Windows.Forms.TabPage();
|
this.tabPage3 = new System.Windows.Forms.TabPage();
|
||||||
this.notesBox = new System.Windows.Forms.TextBox();
|
this.notesBox = new System.Windows.Forms.TextBox();
|
||||||
@@ -516,9 +516,10 @@
|
|||||||
// materialBox
|
// materialBox
|
||||||
//
|
//
|
||||||
this.materialBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
|
this.materialBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.materialBox.FormattingEnabled = true;
|
||||||
this.materialBox.Location = new System.Drawing.Point(135, 159);
|
this.materialBox.Location = new System.Drawing.Point(135, 159);
|
||||||
this.materialBox.Name = "materialBox";
|
this.materialBox.Name = "materialBox";
|
||||||
this.materialBox.Size = new System.Drawing.Size(224, 22);
|
this.materialBox.Size = new System.Drawing.Size(224, 24);
|
||||||
this.materialBox.TabIndex = 11;
|
this.materialBox.TabIndex = 11;
|
||||||
//
|
//
|
||||||
// label3
|
// label3
|
||||||
@@ -729,6 +730,6 @@
|
|||||||
private System.Windows.Forms.RadioButton radioButton2;
|
private System.Windows.Forms.RadioButton radioButton2;
|
||||||
private System.Windows.Forms.Label label5;
|
private System.Windows.Forms.Label label5;
|
||||||
private System.Windows.Forms.Label labelMaterial;
|
private System.Windows.Forms.Label labelMaterial;
|
||||||
private System.Windows.Forms.TextBox materialBox;
|
private System.Windows.Forms.ComboBox materialBox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,9 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
foreach (var name in PostProcessorMaterials.Names)
|
||||||
|
materialBox.Items.Add(name);
|
||||||
|
|
||||||
timer = new Timer
|
timer = new Timer
|
||||||
{
|
{
|
||||||
SynchronizingObject = this,
|
SynchronizingObject = this,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user