Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1945270fa7 | |||
| a18b5398de | |||
| 9d1a39aa8f | |||
| cc38934d10 | |||
| 4f849f1c06 | |||
| 4f2a8d29d5 | |||
| 09a5339b51 | |||
| 77ed1a1522 | |||
| 8ac3f5622c | |||
| c3494681a8 | |||
| c25b6bc23a | |||
| 1c994718fb | |||
| 9d58e6fba8 | |||
| 2bae5340f0 | |||
| 0b322817d7 | |||
| e41f335c63 | |||
| 0ab33af5d3 | |||
| e04c9381f3 | |||
| ceb9cc0b44 | |||
| 4cecaba83a | |||
| 4053f1f989 | |||
| ca67b1bd29 | |||
| 199095ee43 | |||
| eb493d501a | |||
| 6c98732117 | |||
| a2e9fd4d14 | |||
| d228b6b812 | |||
| c634aecd4b | |||
| 14b7c1cf32 | |||
| 402af91af5 | |||
| 9a6b656e3c | |||
| d2f9597b0c | |||
| c40dcf0e25 |
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
pgm.ArcTo(endpt, arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ namespace OpenNest.IO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool DetectBends { get; set; } = true;
|
public bool DetectBends { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, detects and identifies title block entities during import. Default true.
|
||||||
|
/// </summary>
|
||||||
|
public bool DetectTitleBlock { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Override the drawing name. Null = filename without extension.
|
/// Override the drawing name. Null = filename without extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -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,16 @@ 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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GUIDs of entities identified as part of the title block during import.
|
||||||
|
/// </summary>
|
||||||
|
public HashSet<System.Guid> TitleBlockEntityIds { 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)
|
||||||
{
|
{
|
||||||
@@ -37,6 +41,10 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
Bend.UpdateEtchEntities(dxf.Entities, bends);
|
Bend.UpdateEtchEntities(dxf.Entities, bends);
|
||||||
|
|
||||||
|
HashSet<System.Guid> titleBlockIds = null;
|
||||||
|
if (options.DetectTitleBlock)
|
||||||
|
titleBlockIds = TitleBlockDetector.Detect(dxf.Entities, dxf.Document);
|
||||||
|
|
||||||
return new CadImportResult
|
return new CadImportResult
|
||||||
{
|
{
|
||||||
Entities = dxf.Entities,
|
Entities = dxf.Entities,
|
||||||
@@ -44,6 +52,8 @@ 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,
|
||||||
|
TitleBlockEntityIds = titleBlockIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +144,51 @@ namespace OpenNest.IO
|
|||||||
.Where(e => !(e.Layer != null && e.Layer.IsVisible && e.IsVisible))
|
.Where(e => !(e.Layer != null && e.Layer.IsVisible && e.IsVisible))
|
||||||
.Select(e => e.Id));
|
.Select(e => e.Id));
|
||||||
|
|
||||||
|
if (result.TitleBlockEntityIds != null)
|
||||||
|
{
|
||||||
|
var sourceIds = new HashSet<System.Guid>(drawing.SourceEntities.Select(e => e.Id));
|
||||||
|
foreach (var id in result.TitleBlockEntityIds)
|
||||||
|
{
|
||||||
|
if (sourceIds.Contains(id))
|
||||||
|
drawing.SuppressedEntityIds.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-5
@@ -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)
|
||||||
@@ -113,11 +111,29 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
#region Private
|
#region Private
|
||||||
|
|
||||||
|
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)
|
private static List<Entity> ConvertEntities(CadDocument doc)
|
||||||
{
|
{
|
||||||
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>();
|
||||||
|
|
||||||
foreach (var entity in doc.Entities)
|
foreach (var entity in doc.Entities)
|
||||||
{
|
{
|
||||||
@@ -135,7 +151,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 +182,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);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using ACadSharp;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public static class TitleBlockDetector
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> TitleBlockLayerNames = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"TITLE", "TITLEBLOCK", "TITLE_BLOCK", "BORDER", "FRAME",
|
||||||
|
"TB", "INFO", "SHEET", "ANNOTATION"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static HashSet<Guid> Detect(List<Entity> entities, CadDocument document)
|
||||||
|
{
|
||||||
|
var flagged = new HashSet<Guid>();
|
||||||
|
DetectByLayerName(entities, flagged);
|
||||||
|
DetectBorder(entities, flagged);
|
||||||
|
if (document != null)
|
||||||
|
DetectTitleBlockRegion(entities, document, flagged);
|
||||||
|
return flagged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DetectByLayerName(List<Entity> entities, HashSet<Guid> flagged)
|
||||||
|
{
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
if (entity.Layer?.Name != null && TitleBlockLayerNames.Contains(entity.Layer.Name))
|
||||||
|
flagged.Add(entity.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DetectBorder(List<Entity> entities, HashSet<Guid> flagged)
|
||||||
|
{
|
||||||
|
var lines = entities.OfType<Line>().Where(l => !flagged.Contains(l.Id)).ToList();
|
||||||
|
if (lines.Count < 4) return;
|
||||||
|
|
||||||
|
var bounds = entities.GetBoundingBox();
|
||||||
|
if (bounds == null || bounds.Area() < OpenNest.Math.Tolerance.Epsilon) return;
|
||||||
|
|
||||||
|
var borderCount = 0;
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (IsBorderLine(line, bounds))
|
||||||
|
{
|
||||||
|
flagged.Add(line.Id);
|
||||||
|
borderCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (borderCount >= 2)
|
||||||
|
DetectZoneMarkers(lines, bounds, flagged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsBorderLine(Line line, Box bounds)
|
||||||
|
{
|
||||||
|
var dx = line.EndPoint.X - line.StartPoint.X;
|
||||||
|
var dy = line.EndPoint.Y - line.StartPoint.Y;
|
||||||
|
var length = System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
var angleRad = System.Math.Atan2(System.Math.Abs(dy), System.Math.Abs(dx));
|
||||||
|
var angularTolerance = OpenNest.Math.Angle.ToRadians(2.0);
|
||||||
|
var positionTolerance = System.Math.Max(bounds.Length, bounds.Width) * 0.01;
|
||||||
|
|
||||||
|
var isHorizontal = angleRad < angularTolerance;
|
||||||
|
var isVertical = System.Math.Abs(angleRad - System.Math.PI / 2) < angularTolerance;
|
||||||
|
|
||||||
|
if (!isHorizontal && !isVertical) return false;
|
||||||
|
|
||||||
|
var minSpan = isHorizontal ? bounds.Length * 0.8 : bounds.Width * 0.8;
|
||||||
|
if (length < minSpan) return false;
|
||||||
|
|
||||||
|
if (isHorizontal)
|
||||||
|
{
|
||||||
|
var midY = (line.StartPoint.Y + line.EndPoint.Y) / 2;
|
||||||
|
return System.Math.Abs(midY - bounds.Bottom) < positionTolerance
|
||||||
|
|| System.Math.Abs(midY - bounds.Top) < positionTolerance;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var midX = (line.StartPoint.X + line.EndPoint.X) / 2;
|
||||||
|
return System.Math.Abs(midX - bounds.Left) < positionTolerance
|
||||||
|
|| System.Math.Abs(midX - bounds.Right) < positionTolerance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DetectZoneMarkers(List<Line> lines, Box bounds, HashSet<Guid> flagged)
|
||||||
|
{
|
||||||
|
var positionTolerance = System.Math.Max(bounds.Length, bounds.Width) * 0.01;
|
||||||
|
var maxTickLength = System.Math.Max(bounds.Length, bounds.Width) * 0.05;
|
||||||
|
var angularTolerance = OpenNest.Math.Angle.ToRadians(2.0);
|
||||||
|
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (flagged.Contains(line.Id)) continue;
|
||||||
|
|
||||||
|
var dx = line.EndPoint.X - line.StartPoint.X;
|
||||||
|
var dy = line.EndPoint.Y - line.StartPoint.Y;
|
||||||
|
var length = System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (length > maxTickLength || length < OpenNest.Math.Tolerance.Epsilon) continue;
|
||||||
|
|
||||||
|
var angleRad = System.Math.Atan2(System.Math.Abs(dy), System.Math.Abs(dx));
|
||||||
|
var isVertical = System.Math.Abs(angleRad - System.Math.PI / 2) < angularTolerance;
|
||||||
|
var isHorizontal = angleRad < angularTolerance;
|
||||||
|
|
||||||
|
if (!isVertical && !isHorizontal) continue;
|
||||||
|
|
||||||
|
var touchesEdge = false;
|
||||||
|
if (isVertical)
|
||||||
|
{
|
||||||
|
var minY = System.Math.Min(line.StartPoint.Y, line.EndPoint.Y);
|
||||||
|
var maxY = System.Math.Max(line.StartPoint.Y, line.EndPoint.Y);
|
||||||
|
touchesEdge = System.Math.Abs(minY - bounds.Bottom) < positionTolerance
|
||||||
|
|| System.Math.Abs(maxY - bounds.Top) < positionTolerance;
|
||||||
|
}
|
||||||
|
else if (isHorizontal)
|
||||||
|
{
|
||||||
|
var minX = System.Math.Min(line.StartPoint.X, line.EndPoint.X);
|
||||||
|
var maxX = System.Math.Max(line.StartPoint.X, line.EndPoint.X);
|
||||||
|
touchesEdge = System.Math.Abs(minX - bounds.Left) < positionTolerance
|
||||||
|
|| System.Math.Abs(maxX - bounds.Right) < positionTolerance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touchesEdge)
|
||||||
|
flagged.Add(line.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DetectTitleBlockRegion(List<Entity> entities, CadDocument document, HashSet<Guid> flagged)
|
||||||
|
{
|
||||||
|
var textPositions = ExtractTextPositions(document);
|
||||||
|
if (textPositions.Count < 3) return;
|
||||||
|
|
||||||
|
var unflagged = entities.Where(e => !flagged.Contains(e.Id)).ToList();
|
||||||
|
if (unflagged.Count == 0) return;
|
||||||
|
|
||||||
|
var bounds = entities.GetBoundingBox();
|
||||||
|
if (bounds == null || bounds.Area() < OpenNest.Math.Tolerance.Epsilon) return;
|
||||||
|
|
||||||
|
var bestRegion = FindBestTitleBlockRegion(bounds, textPositions, unflagged);
|
||||||
|
if (bestRegion == null) return;
|
||||||
|
|
||||||
|
var initiallyInside = unflagged.Where(e => {
|
||||||
|
var c = EntityCenter(e);
|
||||||
|
return c.HasValue && RegionContains(bestRegion, c.Value);
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var expandedBounds = initiallyInside.Count > 0 ? initiallyInside.GetBoundingBox() : null;
|
||||||
|
|
||||||
|
foreach (var entity in unflagged)
|
||||||
|
{
|
||||||
|
var center = EntityCenter(entity);
|
||||||
|
if (!center.HasValue) continue;
|
||||||
|
if (RegionContains(bestRegion, center.Value)
|
||||||
|
|| (expandedBounds != null && RegionContains(expandedBounds, center.Value)))
|
||||||
|
flagged.Add(entity.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Vector> ExtractTextPositions(CadDocument document)
|
||||||
|
{
|
||||||
|
var positions = new List<Vector>();
|
||||||
|
foreach (var entity in document.Entities)
|
||||||
|
{
|
||||||
|
switch (entity)
|
||||||
|
{
|
||||||
|
case ACadSharp.Entities.MText mtext:
|
||||||
|
positions.Add(new Vector(mtext.InsertPoint.X, mtext.InsertPoint.Y));
|
||||||
|
break;
|
||||||
|
case ACadSharp.Entities.TextEntity text:
|
||||||
|
var pt = text.HorizontalAlignment != 0 || text.VerticalAlignment != 0
|
||||||
|
? text.AlignmentPoint : text.InsertPoint;
|
||||||
|
positions.Add(new Vector(pt.X, pt.Y));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Box FindBestTitleBlockRegion(Box bounds, List<Vector> textPositions, List<Entity> entities)
|
||||||
|
{
|
||||||
|
var candidates = GenerateCandidateRegions(bounds);
|
||||||
|
Box bestRegion = null;
|
||||||
|
var bestScore = 0.0;
|
||||||
|
|
||||||
|
var openLines = FindOpenLines(entities);
|
||||||
|
|
||||||
|
foreach (var region in candidates)
|
||||||
|
{
|
||||||
|
var textCount = textPositions.Count(p => RegionContains(region, p));
|
||||||
|
if (textCount < 3) continue;
|
||||||
|
|
||||||
|
var openLineCount = openLines.Count(l => RegionContains(region, l.MidPoint));
|
||||||
|
|
||||||
|
var area = region.Area();
|
||||||
|
if (area < OpenNest.Math.Tolerance.Epsilon) continue;
|
||||||
|
|
||||||
|
var score = (double)textCount + openLineCount * 0.5;
|
||||||
|
|
||||||
|
var regionCenterX = (region.Left + region.Right) / 2;
|
||||||
|
var regionCenterY = (region.Bottom + region.Top) / 2;
|
||||||
|
if (regionCenterX > bounds.Center.X) score *= 1.3;
|
||||||
|
if (regionCenterY < bounds.Center.Y) score *= 1.3;
|
||||||
|
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
bestRegion = region;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Box> GenerateCandidateRegions(Box bounds)
|
||||||
|
{
|
||||||
|
var regions = new List<Box>();
|
||||||
|
var fractions = new[] { 0.25, 0.333, 0.5 };
|
||||||
|
|
||||||
|
foreach (var fx in fractions)
|
||||||
|
{
|
||||||
|
foreach (var fy in fractions)
|
||||||
|
{
|
||||||
|
var w = bounds.Length * fx;
|
||||||
|
var h = bounds.Width * fy;
|
||||||
|
|
||||||
|
regions.Add(new Box(bounds.Right - w, bounds.Bottom, w, h));
|
||||||
|
regions.Add(new Box(bounds.Left, bounds.Bottom, w, h));
|
||||||
|
regions.Add(new Box(bounds.Right - w, bounds.Top - h, w, h));
|
||||||
|
regions.Add(new Box(bounds.Left, bounds.Top - h, w, h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var fy in fractions)
|
||||||
|
{
|
||||||
|
var h = bounds.Width * fy;
|
||||||
|
regions.Add(new Box(bounds.Left, bounds.Bottom, bounds.Length, h));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var fx in fractions)
|
||||||
|
{
|
||||||
|
var w = bounds.Length * fx;
|
||||||
|
regions.Add(new Box(bounds.Right - w, bounds.Bottom, w, bounds.Width));
|
||||||
|
}
|
||||||
|
|
||||||
|
return regions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Line> FindOpenLines(List<Entity> entities)
|
||||||
|
{
|
||||||
|
var endpointUsers = new Dictionary<long, int>();
|
||||||
|
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
foreach (var ep in GetEntityEndpoints(entity))
|
||||||
|
{
|
||||||
|
var key = QuantizePoint(ep);
|
||||||
|
endpointUsers[key] = endpointUsers.GetValueOrDefault(key) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var openLines = new List<Line>();
|
||||||
|
foreach (var line in entities.OfType<Line>())
|
||||||
|
{
|
||||||
|
var startKey = QuantizePoint(line.StartPoint);
|
||||||
|
var endKey = QuantizePoint(line.EndPoint);
|
||||||
|
|
||||||
|
if (endpointUsers.GetValueOrDefault(startKey) <= 1 || endpointUsers.GetValueOrDefault(endKey) <= 1)
|
||||||
|
openLines.Add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return openLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Vector> GetEntityEndpoints(Entity entity)
|
||||||
|
{
|
||||||
|
return entity switch
|
||||||
|
{
|
||||||
|
Line line => new List<Vector> { line.StartPoint, line.EndPoint },
|
||||||
|
Arc arc => new List<Vector> { arc.StartPoint(), arc.EndPoint() },
|
||||||
|
_ => new List<Vector>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long QuantizePoint(Vector pt)
|
||||||
|
{
|
||||||
|
var qx = (long)(pt.X * 1000);
|
||||||
|
var qy = (long)(pt.Y * 1000);
|
||||||
|
return qx * 100000000L + qy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector? EntityCenter(Entity entity)
|
||||||
|
{
|
||||||
|
return entity switch
|
||||||
|
{
|
||||||
|
Line line => line.MidPoint,
|
||||||
|
Arc arc => arc.Center,
|
||||||
|
Circle circle => circle.Center,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RegionContains(Box box, Vector pt)
|
||||||
|
{
|
||||||
|
return pt.X >= box.Left && pt.X <= box.Right
|
||||||
|
&& pt.Y >= box.Bottom && pt.Y <= box.Top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,5 +134,21 @@ namespace OpenNest.Tests.IO
|
|||||||
Assert.NotNull(drawing.Program);
|
Assert.NotNull(drawing.Program);
|
||||||
Assert.NotNull(drawing.SourceEntities);
|
Assert.NotNull(drawing.SourceEntities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Import_WhenDetectTitleBlockTrue_PopulatesTitleBlockEntityIds()
|
||||||
|
{
|
||||||
|
var result = CadImporter.Import(TestDxf);
|
||||||
|
|
||||||
|
Assert.NotNull(result.TitleBlockEntityIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Import_WhenDetectTitleBlockFalse_TitleBlockEntityIdsIsNull()
|
||||||
|
{
|
||||||
|
var result = CadImporter.Import(TestDxf, new CadImportOptions { DetectTitleBlock = false });
|
||||||
|
|
||||||
|
Assert.Null(result.TitleBlockEntityIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,245 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using CSMath;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.IO
|
||||||
|
{
|
||||||
|
public class TitleBlockDetectorTests
|
||||||
|
{
|
||||||
|
private static Line MakeLine(double x1, double y1, double x2, double y2) =>
|
||||||
|
new Line(x1, y1, x2, y2);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectByLayerName_FlagsTitleLayer()
|
||||||
|
{
|
||||||
|
var line = MakeLine(0, 0, 10, 0);
|
||||||
|
line.Layer = new Layer("TITLE");
|
||||||
|
var entities = new List<Entity> { line };
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Contains(line.Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectByLayerName_CaseInsensitive()
|
||||||
|
{
|
||||||
|
var line = MakeLine(0, 0, 10, 0);
|
||||||
|
line.Layer = new Layer("border");
|
||||||
|
var entities = new List<Entity> { line };
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Contains(line.Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectByLayerName_IgnoresNonMatchingLayers()
|
||||||
|
{
|
||||||
|
var line = MakeLine(0, 0, 10, 0);
|
||||||
|
line.Layer = new Layer("0");
|
||||||
|
var entities = new List<Entity> { line };
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.DoesNotContain(line.Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("TITLE")]
|
||||||
|
[InlineData("TITLEBLOCK")]
|
||||||
|
[InlineData("TITLE_BLOCK")]
|
||||||
|
[InlineData("BORDER")]
|
||||||
|
[InlineData("FRAME")]
|
||||||
|
[InlineData("TB")]
|
||||||
|
[InlineData("INFO")]
|
||||||
|
[InlineData("SHEET")]
|
||||||
|
[InlineData("ANNOTATION")]
|
||||||
|
public void DetectByLayerName_AllKnownNames(string layerName)
|
||||||
|
{
|
||||||
|
var line = MakeLine(0, 0, 10, 0);
|
||||||
|
line.Layer = new Layer(layerName);
|
||||||
|
var entities = new List<Entity> { line };
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Contains(line.Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectBorder_FlagsLinesOnBoundingBoxEdges()
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(0, 0, 86, 0) { Layer = new Layer("0") },
|
||||||
|
new Line(86, 0, 86, 134) { Layer = new Layer("0") },
|
||||||
|
new Line(86, 134, 0, 134) { Layer = new Layer("0") },
|
||||||
|
new Line(0, 134, 0, 0) { Layer = new Layer("0") },
|
||||||
|
new Line(30, 40, 50, 90) { Layer = new Layer("0") },
|
||||||
|
new Line(50, 90, 70, 40) { Layer = new Layer("0") },
|
||||||
|
new Line(70, 40, 30, 40) { Layer = new Layer("0") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Contains(entities[0].Id, result);
|
||||||
|
Assert.Contains(entities[1].Id, result);
|
||||||
|
Assert.Contains(entities[2].Id, result);
|
||||||
|
Assert.Contains(entities[3].Id, result);
|
||||||
|
Assert.DoesNotContain(entities[4].Id, result);
|
||||||
|
Assert.DoesNotContain(entities[5].Id, result);
|
||||||
|
Assert.DoesNotContain(entities[6].Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectBorder_FlagsZoneMarkerTicks()
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(0, 0, 100, 0) { Layer = new Layer("0") },
|
||||||
|
new Line(100, 0, 100, 80) { Layer = new Layer("0") },
|
||||||
|
new Line(100, 80, 0, 80) { Layer = new Layer("0") },
|
||||||
|
new Line(0, 80, 0, 0) { Layer = new Layer("0") },
|
||||||
|
new Line(25, 80, 25, 77) { Layer = new Layer("0") },
|
||||||
|
new Line(50, 80, 50, 77) { Layer = new Layer("0") },
|
||||||
|
new Line(75, 80, 75, 77) { Layer = new Layer("0") },
|
||||||
|
new Line(40, 30, 60, 30) { Layer = new Layer("0") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Contains(entities[4].Id, result);
|
||||||
|
Assert.Contains(entities[5].Id, result);
|
||||||
|
Assert.Contains(entities[6].Id, result);
|
||||||
|
Assert.DoesNotContain(entities[7].Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectBorder_IgnoresWhenNoBorderPresent()
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(30, 40, 50, 90) { Layer = new Layer("0") },
|
||||||
|
new Line(50, 90, 70, 40) { Layer = new Layer("0") },
|
||||||
|
new Line(70, 40, 30, 40) { Layer = new Layer("0") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectBorder_ToleratesSlightRotation()
|
||||||
|
{
|
||||||
|
var angleRad = OpenNest.Math.Angle.ToRadians(0.5);
|
||||||
|
var endY = 86 * System.Math.Sin(angleRad);
|
||||||
|
var entities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(0, 0, 86, endY) { Layer = new Layer("0") },
|
||||||
|
new Line(86, endY, 86, 134) { Layer = new Layer("0") },
|
||||||
|
new Line(86, 134, 0, 134) { Layer = new Layer("0") },
|
||||||
|
new Line(0, 134, 0, 0) { Layer = new Layer("0") },
|
||||||
|
new Line(30, 40, 50, 90) { Layer = new Layer("0") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Contains(entities[0].Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectTitleBlock_FlagsEntitiesInTextDenseCorner()
|
||||||
|
{
|
||||||
|
var partLine1 = new Line(5, 70, 25, 120) { Layer = new Layer("0") };
|
||||||
|
var partLine2 = new Line(25, 120, 45, 70) { Layer = new Layer("0") };
|
||||||
|
var partLine3 = new Line(45, 70, 5, 70) { Layer = new Layer("0") };
|
||||||
|
|
||||||
|
var tbLines = new List<Entity>();
|
||||||
|
for (var x = 50; x <= 85; x += 5)
|
||||||
|
tbLines.Add(new Line(x, 0, x, 30) { Layer = new Layer("0") });
|
||||||
|
for (var y = 0; y <= 30; y += 5)
|
||||||
|
tbLines.Add(new Line(50, y, 85, y) { Layer = new Layer("0") });
|
||||||
|
|
||||||
|
var entities = new List<Entity> { partLine1, partLine2, partLine3 };
|
||||||
|
entities.AddRange(tbLines);
|
||||||
|
|
||||||
|
var doc = BuildDocWithTexts(
|
||||||
|
(60, 5, "TITLE: Test Part"),
|
||||||
|
(60, 10, "DWG NO: 12345"),
|
||||||
|
(60, 15, "SCALE: 1:1"),
|
||||||
|
(60, 20, "REV: A"),
|
||||||
|
(60, 25, "MATERIAL: STEEL"));
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, doc);
|
||||||
|
|
||||||
|
foreach (var tb in tbLines)
|
||||||
|
Assert.Contains(tb.Id, result);
|
||||||
|
Assert.DoesNotContain(partLine1.Id, result);
|
||||||
|
Assert.DoesNotContain(partLine2.Id, result);
|
||||||
|
Assert.DoesNotContain(partLine3.Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectTitleBlock_NoFalsePositivesWithoutText()
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(30, 40, 50, 90) { Layer = new Layer("0") },
|
||||||
|
new Line(50, 90, 70, 40) { Layer = new Layer("0") },
|
||||||
|
new Line(70, 40, 30, 40) { Layer = new Layer("0") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, null);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectTitleBlock_BottomEdgeStrip()
|
||||||
|
{
|
||||||
|
var partLine = new Line(20, 40, 80, 40) { Layer = new Layer("0") };
|
||||||
|
|
||||||
|
var tbLines = new List<Entity>();
|
||||||
|
for (var x = 0; x <= 100; x += 10)
|
||||||
|
tbLines.Add(new Line(x, 0, x, 20) { Layer = new Layer("0") });
|
||||||
|
for (var y = 0; y <= 20; y += 5)
|
||||||
|
tbLines.Add(new Line(0, y, 100, y) { Layer = new Layer("0") });
|
||||||
|
|
||||||
|
var entities = new List<Entity> { partLine };
|
||||||
|
entities.AddRange(tbLines);
|
||||||
|
|
||||||
|
var doc = BuildDocWithTexts(
|
||||||
|
(10, 5, "TITLE"),
|
||||||
|
(30, 5, "DWG NO"),
|
||||||
|
(50, 5, "SCALE"),
|
||||||
|
(70, 5, "REV"),
|
||||||
|
(90, 5, "DATE"));
|
||||||
|
|
||||||
|
var result = TitleBlockDetector.Detect(entities, doc);
|
||||||
|
|
||||||
|
foreach (var tb in tbLines)
|
||||||
|
Assert.Contains(tb.Id, result);
|
||||||
|
Assert.DoesNotContain(partLine.Id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ACadSharp.CadDocument BuildDocWithTexts(
|
||||||
|
params (double x, double y, string value)[] texts)
|
||||||
|
{
|
||||||
|
var doc = new ACadSharp.CadDocument();
|
||||||
|
foreach (var (x, y, value) in texts)
|
||||||
|
{
|
||||||
|
var mtext = new ACadSharp.Entities.MText
|
||||||
|
{
|
||||||
|
InsertPoint = new XYZ(x, y, 0),
|
||||||
|
Value = value,
|
||||||
|
Height = 2.0
|
||||||
|
};
|
||||||
|
doc.Entities.Add(mtext);
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,12 +29,17 @@ 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>();
|
||||||
|
public HashSet<Guid> TitleBlockEntityIds { get; set; }
|
||||||
|
|
||||||
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 Dictionary<int, Pen> ghostPenCache = 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));
|
||||||
|
private readonly SolidBrush ghostTextBrush = new SolidBrush(Color.FromArgb(50, 200, 200, 200));
|
||||||
|
|
||||||
public event EventHandler<Line> LinePicked;
|
public event EventHandler<Line> LinePicked;
|
||||||
public event EventHandler PickCancelled;
|
public event EventHandler PickCancelled;
|
||||||
@@ -100,6 +105,13 @@ namespace OpenNest.Controls
|
|||||||
foreach (var entity in Entities)
|
foreach (var entity in Entities)
|
||||||
{
|
{
|
||||||
if (IsEtchLayer(entity.Layer)) continue;
|
if (IsEtchLayer(entity.Layer)) continue;
|
||||||
|
|
||||||
|
if (TitleBlockEntityIds != null && TitleBlockEntityIds.Contains(entity.Id))
|
||||||
|
{
|
||||||
|
DrawGhostEntity(e.Graphics, entity);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var isHighlighted = simplifierHighlightSet != null && simplifierHighlightSet.Contains(entity);
|
var isHighlighted = simplifierHighlightSet != null && simplifierHighlightSet.Contains(entity);
|
||||||
var pen = isHighlighted
|
var pen = isHighlighted
|
||||||
? GetEntityPen(Color.FromArgb(60, entity.Color))
|
? GetEntityPen(Color.FromArgb(60, entity.Color))
|
||||||
@@ -116,6 +128,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);
|
||||||
|
|
||||||
@@ -239,11 +253,26 @@ namespace OpenNest.Controls
|
|||||||
return pen;
|
return pen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Pen GetGhostPen(Color color)
|
||||||
|
{
|
||||||
|
var ghostColor = Color.FromArgb(60, color.R, color.G, color.B);
|
||||||
|
var argb = ghostColor.ToArgb();
|
||||||
|
if (!ghostPenCache.TryGetValue(argb, out var pen))
|
||||||
|
{
|
||||||
|
pen = new Pen(ghostColor);
|
||||||
|
ghostPenCache[argb] = pen;
|
||||||
|
}
|
||||||
|
return pen;
|
||||||
|
}
|
||||||
|
|
||||||
public void ClearPenCache()
|
public void ClearPenCache()
|
||||||
{
|
{
|
||||||
foreach (var pen in penCache.Values)
|
foreach (var pen in penCache.Values)
|
||||||
pen.Dispose();
|
pen.Dispose();
|
||||||
penCache.Clear();
|
penCache.Clear();
|
||||||
|
foreach (var pen in ghostPenCache.Values)
|
||||||
|
pen.Dispose();
|
||||||
|
ghostPenCache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsEtchLayer(Layer layer) =>
|
private static bool IsEtchLayer(Layer layer) =>
|
||||||
@@ -408,10 +437,29 @@ namespace OpenNest.Controls
|
|||||||
labelFont.Dispose();
|
labelFont.Dispose();
|
||||||
labelBrush.Dispose();
|
labelBrush.Dispose();
|
||||||
labelBackBrush.Dispose();
|
labelBackBrush.Dispose();
|
||||||
|
textBrush.Dispose();
|
||||||
|
ghostTextBrush.Dispose();
|
||||||
}
|
}
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawGhostEntity(Graphics g, Entity e)
|
||||||
|
{
|
||||||
|
var pen = GetGhostPen(e.Color);
|
||||||
|
switch (e.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
DrawArc(g, (Arc)e, pen);
|
||||||
|
break;
|
||||||
|
case EntityType.Circle:
|
||||||
|
DrawCircle(g, (Circle)e, pen);
|
||||||
|
break;
|
||||||
|
case EntityType.Line:
|
||||||
|
DrawLine(g, (Line)e, pen);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawEntity(Graphics g, Entity e, Pen pen)
|
private void DrawEntity(Graphics g, Entity e, Pen pen)
|
||||||
{
|
{
|
||||||
if (!e.Layer.IsVisible || !e.IsVisible)
|
if (!e.Layer.IsVisible || !e.IsVisible)
|
||||||
@@ -474,6 +522,36 @@ 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;
|
||||||
|
|
||||||
|
var brush = TitleBlockEntityIds != null && TitleBlockEntityIds.Count > 0
|
||||||
|
? ghostTextBrush : textBrush;
|
||||||
|
using var font = new Font("Segoe UI", fontSize, GraphicsUnit.Pixel);
|
||||||
|
g.DrawString(text.Value, font, brush, 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);
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ namespace OpenNest.Controls
|
|||||||
public List<Entity> OriginalEntities { get; set; }
|
public List<Entity> OriginalEntities { get; set; }
|
||||||
public List<Bend> Bends { get; set; } = new();
|
public List<Bend> Bends { get; set; } = new();
|
||||||
public HashSet<Guid> SuppressedEntityIds { get; set; }
|
public HashSet<Guid> SuppressedEntityIds { get; set; }
|
||||||
|
public HashSet<Guid> TitleBlockEntityIds { 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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -92,9 +92,18 @@ 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),
|
||||||
|
TitleBlockEntityIds = result.TitleBlockEntityIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (result.TitleBlockEntityIds != null && result.TitleBlockEntityIds.Count > 0)
|
||||||
|
{
|
||||||
|
item.SuppressedEntityIds ??= new HashSet<Guid>();
|
||||||
|
foreach (var id in result.TitleBlockEntityIds)
|
||||||
|
item.SuppressedEntityIds.Add(id);
|
||||||
|
}
|
||||||
|
|
||||||
if (InvokeRequired)
|
if (InvokeRequired)
|
||||||
BeginInvoke((Action)(() => fileList.AddItem(item)));
|
BeginInvoke((Action)(() => fileList.AddItem(item)));
|
||||||
else
|
else
|
||||||
@@ -152,6 +161,8 @@ 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>();
|
||||||
|
entityView1.TitleBlockEntityIds = item.TitleBlockEntityIds;
|
||||||
|
|
||||||
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))
|
||||||
@@ -473,7 +484,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 +815,102 @@ 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,
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -138,9 +138,20 @@ namespace OpenNest
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case CodeType.RapidMove:
|
case CodeType.RapidMove:
|
||||||
cutPath.StartFigure();
|
{
|
||||||
leadPath.StartFigure();
|
var rapid = (RapidMove)code;
|
||||||
AddLine(cutPath, (RapidMove)code, mode, ref curpos);
|
var endpt = rapid.EndPoint;
|
||||||
|
if (mode == Mode.Incremental)
|
||||||
|
endpt += curpos;
|
||||||
|
var dx = endpt.X - curpos.X;
|
||||||
|
var dy = endpt.Y - curpos.Y;
|
||||||
|
if (dx * dx + dy * dy > 0.001 * 0.001)
|
||||||
|
{
|
||||||
|
cutPath.StartFigure();
|
||||||
|
leadPath.StartFigure();
|
||||||
|
}
|
||||||
|
curpos = endpt;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CodeType.SubProgramCall:
|
case CodeType.SubProgramCall:
|
||||||
@@ -300,8 +311,17 @@ namespace OpenNest
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case CodeType.RapidMove:
|
case CodeType.RapidMove:
|
||||||
Flush();
|
{
|
||||||
AddLine(path, (RapidMove)code, mode, ref curpos);
|
var rapid = (RapidMove)code;
|
||||||
|
var endpt = rapid.EndPoint;
|
||||||
|
if (mode == Mode.Incremental)
|
||||||
|
endpt += curpos;
|
||||||
|
var dx = endpt.X - curpos.X;
|
||||||
|
var dy = endpt.Y - curpos.Y;
|
||||||
|
if (dx * dx + dy * dy > 0.001 * 0.001)
|
||||||
|
Flush();
|
||||||
|
curpos = endpt;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CodeType.SubProgramCall:
|
case CodeType.SubProgramCall:
|
||||||
|
|||||||
Reference in New Issue
Block a user