Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2db8c49838 | |||
| 80e8693da3 | |||
| d7eb3ebd7a | |||
| 4404d3a5d0 | |||
| d27dee3db9 | |||
| 7081c7b4d0 | |||
| a6e813bc85 | |||
| 98453243fc | |||
| 64874857a1 | |||
| 5d3fcb2dc8 | |||
| ae9a63b5ce | |||
| 596328148d | |||
| 6cd48a623d | |||
| 42243c7df0 | |||
| 4b10d4801c | |||
| f0bdaa14e6 | |||
| 79ddce346b | |||
| 20777541c0 | |||
| 7c8168b002 | |||
| 203bd4eeea | |||
| 02d15dea9c | |||
| a88937b716 | |||
| 986a0412b1 | |||
| e7f2ee80e2 | |||
| 31063d954d | |||
| fc1fee54cd | |||
| 094b522644 | |||
| 45dea4ec2b | |||
| 743bb25f7b |
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"opennest": {
|
"opennest": {
|
||||||
"command": "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/OpenNest.Mcp.exe",
|
"command": "cmd",
|
||||||
"args": []
|
"args": ["/c", "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/run.cmd"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ public static class NestRunner
|
|||||||
if (!importer.GetGeometry(part.DxfPath, out var geometry) || geometry.Count == 0)
|
if (!importer.GetGeometry(part.DxfPath, out var geometry) || geometry.Count == 0)
|
||||||
throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}");
|
throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}");
|
||||||
|
|
||||||
var pgm = ConvertGeometry.ToProgram(geometry);
|
var normalized = ShapeProfile.NormalizeEntities(geometry);
|
||||||
|
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||||
var name = Path.GetFileNameWithoutExtension(part.DxfPath);
|
var name = Path.GetFileNameWithoutExtension(part.DxfPath);
|
||||||
var drawing = new Drawing(name);
|
var drawing = new Drawing(name);
|
||||||
drawing.Program = pgm;
|
drawing.Program = pgm;
|
||||||
|
|||||||
@@ -255,7 +255,8 @@ static class NestConsole
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pgm = ConvertGeometry.ToProgram(geometry);
|
var normalized = ShapeProfile.NormalizeEntities(geometry);
|
||||||
|
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||||
|
|
||||||
if (pgm == null)
|
if (pgm == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
namespace OpenNest.Bending
|
namespace OpenNest.Bending
|
||||||
{
|
{
|
||||||
public class Bend
|
public class Bend
|
||||||
{
|
{
|
||||||
|
public static readonly Layer EtchLayer = new Layer("ETCH")
|
||||||
|
{
|
||||||
|
Color = Color.Green,
|
||||||
|
IsVisible = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private const double DefaultEtchLength = 1.0;
|
||||||
|
|
||||||
public Vector StartPoint { get; set; }
|
public Vector StartPoint { get; set; }
|
||||||
public Vector EndPoint { get; set; }
|
public Vector EndPoint { get; set; }
|
||||||
public BendDirection Direction { get; set; }
|
public BendDirection Direction { get; set; }
|
||||||
@@ -29,6 +39,52 @@ namespace OpenNest.Bending
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double LineAngle => StartPoint.AngleTo(EndPoint);
|
public double LineAngle => StartPoint.AngleTo(EndPoint);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates etch mark entities for this bend (up bends only).
|
||||||
|
/// Returns 1" dashes at each end of the bend line, or the full line if shorter than 3".
|
||||||
|
/// </summary>
|
||||||
|
public List<Line> GetEtchEntities(double etchLength = DefaultEtchLength)
|
||||||
|
{
|
||||||
|
var result = new List<Line>();
|
||||||
|
if (Direction != BendDirection.Up)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var length = Length;
|
||||||
|
|
||||||
|
if (length < etchLength * 3.0)
|
||||||
|
{
|
||||||
|
result.Add(CreateEtchLine(StartPoint, EndPoint));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var angle = StartPoint.AngleTo(EndPoint);
|
||||||
|
var dx = System.Math.Cos(angle) * etchLength;
|
||||||
|
var dy = System.Math.Sin(angle) * etchLength;
|
||||||
|
|
||||||
|
result.Add(CreateEtchLine(StartPoint, new Vector(StartPoint.X + dx, StartPoint.Y + dy)));
|
||||||
|
result.Add(CreateEtchLine(new Vector(EndPoint.X - dx, EndPoint.Y - dy), EndPoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes existing etch entities from the list and regenerates from the given bends.
|
||||||
|
/// </summary>
|
||||||
|
public static void UpdateEtchEntities(List<Entity> entities, List<Bend> bends)
|
||||||
|
{
|
||||||
|
entities.RemoveAll(e => e.Layer == EtchLayer);
|
||||||
|
if (bends == null) return;
|
||||||
|
|
||||||
|
foreach (var bend in bends)
|
||||||
|
entities.AddRange(bend.GetEtchEntities());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Line CreateEtchLine(Vector start, Vector end)
|
||||||
|
{
|
||||||
|
return new Line(start, end) { Layer = EtchLayer, Color = Color.Green };
|
||||||
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var dir = Direction.ToString();
|
var dir = Direction.ToString();
|
||||||
|
|||||||
@@ -108,7 +108,10 @@ namespace OpenNest.Converters
|
|||||||
if (line.StartPoint != lastpt)
|
if (line.StartPoint != lastpt)
|
||||||
pgm.MoveTo(line.StartPoint);
|
pgm.MoveTo(line.StartPoint);
|
||||||
|
|
||||||
pgm.LineTo(line.EndPoint);
|
var move = new LinearMove(line.EndPoint);
|
||||||
|
if (string.Equals(line.Layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
move.Layer = LayerType.Scribe;
|
||||||
|
pgm.Codes.Add(move);
|
||||||
|
|
||||||
lastpt = line.EndPoint;
|
lastpt = line.EndPoint;
|
||||||
return lastpt;
|
return lastpt;
|
||||||
|
|||||||
@@ -598,6 +598,41 @@ namespace OpenNest.Geometry
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offsets the shape inward by the given distance.
|
||||||
|
/// Normalizes to CCW winding before offsetting Left (which is inward for CCW),
|
||||||
|
/// making the method independent of the original contour winding direction.
|
||||||
|
/// </summary>
|
||||||
|
public Shape OffsetInward(double distance)
|
||||||
|
{
|
||||||
|
var poly = ToPolygon();
|
||||||
|
|
||||||
|
if (poly == null || poly.Vertices.Count < 3
|
||||||
|
|| poly.RotationDirection() == RotationType.CCW)
|
||||||
|
return OffsetEntity(distance, OffsetSide.Left) as Shape;
|
||||||
|
|
||||||
|
// Create a reversed copy to avoid mutating shared entity objects.
|
||||||
|
var copy = new Shape();
|
||||||
|
|
||||||
|
for (var i = Entities.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
switch (Entities[i])
|
||||||
|
{
|
||||||
|
case Line l:
|
||||||
|
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
|
||||||
|
break;
|
||||||
|
case Arc a:
|
||||||
|
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
||||||
|
break;
|
||||||
|
case Circle c:
|
||||||
|
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy.OffsetEntity(distance, OffsetSide.Left) as Shape;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the closest point on the shape to the given point.
|
/// Gets the closest point on the shape to the given point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -41,5 +42,52 @@ namespace OpenNest.Geometry
|
|||||||
public Shape Perimeter { get; set; }
|
public Shape Perimeter { get; set; }
|
||||||
|
|
||||||
public List<Shape> Cutouts { get; set; }
|
public List<Shape> Cutouts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures CNC-standard winding: perimeter CW (kerf left = outward),
|
||||||
|
/// cutouts CCW (kerf left = inward). Reverses contours in-place as needed.
|
||||||
|
/// </summary>
|
||||||
|
public void NormalizeWinding()
|
||||||
|
{
|
||||||
|
EnsureWinding(Perimeter, RotationType.CW);
|
||||||
|
|
||||||
|
foreach (var cutout in Cutouts)
|
||||||
|
EnsureWinding(cutout, RotationType.CCW);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the entities in normalized winding order (perimeter first, then cutouts).
|
||||||
|
/// </summary>
|
||||||
|
public List<Entity> ToNormalizedEntities()
|
||||||
|
{
|
||||||
|
NormalizeWinding();
|
||||||
|
var result = new List<Entity>(Perimeter.Entities);
|
||||||
|
|
||||||
|
foreach (var cutout in Cutouts)
|
||||||
|
result.AddRange(cutout.Entities);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convenience method: builds a ShapeProfile from raw entities,
|
||||||
|
/// normalizes winding, and returns the corrected entity list.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Entity> NormalizeEntities(IEnumerable<Entity> entities)
|
||||||
|
{
|
||||||
|
var profile = new ShapeProfile(entities.ToList());
|
||||||
|
return profile.ToNormalizedEntities();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureWinding(Shape shape, RotationType desired)
|
||||||
|
{
|
||||||
|
var poly = shape.ToPolygon();
|
||||||
|
|
||||||
|
if (poly != null && poly.Vertices.Count >= 3
|
||||||
|
&& poly.RotationDirection() != desired)
|
||||||
|
{
|
||||||
|
shape.Reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,23 +42,17 @@ namespace OpenNest
|
|||||||
public static List<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001)
|
public static List<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001)
|
||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
var profile = new ShapeProfile(
|
||||||
|
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
var lines = new List<Line>();
|
var lines = new List<Line>();
|
||||||
|
var totalSpacing = spacing + chordTolerance;
|
||||||
|
|
||||||
foreach (var shape in shapes)
|
AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||||
{
|
chordTolerance, part.Location);
|
||||||
// Add chord tolerance to compensate for inscribed polygon chords
|
|
||||||
// being inside the actual offset arcs.
|
|
||||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
|
||||||
|
|
||||||
if (offsetEntity == null)
|
foreach (var cutout in profile.Cutouts)
|
||||||
continue;
|
AddOffsetLines(lines, cutout.OffsetInward(totalSpacing),
|
||||||
|
chordTolerance, part.Location);
|
||||||
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
|
||||||
polygon.RemoveSelfIntersections();
|
|
||||||
polygon.Offset(part.Location);
|
|
||||||
lines.AddRange(polygon.ToLines());
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
@@ -66,21 +60,17 @@ namespace OpenNest
|
|||||||
public static List<Line> GetOffsetPartLines(Part part, double spacing, PushDirection facingDirection, double chordTolerance = 0.001)
|
public static List<Line> GetOffsetPartLines(Part part, double spacing, PushDirection facingDirection, double chordTolerance = 0.001)
|
||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
var profile = new ShapeProfile(
|
||||||
|
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
var lines = new List<Line>();
|
var lines = new List<Line>();
|
||||||
|
var totalSpacing = spacing + chordTolerance;
|
||||||
|
|
||||||
foreach (var shape in shapes)
|
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||||
{
|
chordTolerance, part.Location, facingDirection);
|
||||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
|
||||||
|
|
||||||
if (offsetEntity == null)
|
foreach (var cutout in profile.Cutouts)
|
||||||
continue;
|
AddOffsetDirectionalLines(lines, cutout.OffsetInward(totalSpacing),
|
||||||
|
chordTolerance, part.Location, facingDirection);
|
||||||
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
|
||||||
polygon.RemoveSelfIntersections();
|
|
||||||
polygon.Offset(part.Location);
|
|
||||||
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
@@ -104,21 +94,17 @@ namespace OpenNest
|
|||||||
public static List<Line> GetOffsetPartLines(Part part, double spacing, Vector facingDirection, double chordTolerance = 0.001)
|
public static List<Line> GetOffsetPartLines(Part part, double spacing, Vector facingDirection, double chordTolerance = 0.001)
|
||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
var profile = new ShapeProfile(
|
||||||
|
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
var lines = new List<Line>();
|
var lines = new List<Line>();
|
||||||
|
var totalSpacing = spacing + chordTolerance;
|
||||||
|
|
||||||
foreach (var shape in shapes)
|
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||||
{
|
chordTolerance, part.Location, facingDirection);
|
||||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
|
||||||
|
|
||||||
if (offsetEntity == null)
|
foreach (var cutout in profile.Cutouts)
|
||||||
continue;
|
AddOffsetDirectionalLines(lines, cutout.OffsetInward(totalSpacing),
|
||||||
|
chordTolerance, part.Location, facingDirection);
|
||||||
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
|
||||||
polygon.RemoveSelfIntersections();
|
|
||||||
polygon.Offset(part.Location);
|
|
||||||
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
@@ -189,5 +175,41 @@ namespace OpenNest
|
|||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddOffsetLines(List<Line> lines, Shape offsetEntity,
|
||||||
|
double chordTolerance, Vector location)
|
||||||
|
{
|
||||||
|
if (offsetEntity == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.RemoveSelfIntersections();
|
||||||
|
polygon.Offset(location);
|
||||||
|
lines.AddRange(polygon.ToLines());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddOffsetDirectionalLines(List<Line> lines, Shape offsetEntity,
|
||||||
|
double chordTolerance, Vector location, PushDirection facingDirection)
|
||||||
|
{
|
||||||
|
if (offsetEntity == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.RemoveSelfIntersections();
|
||||||
|
polygon.Offset(location);
|
||||||
|
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddOffsetDirectionalLines(List<Line> lines, Shape offsetEntity,
|
||||||
|
double chordTolerance, Vector location, Vector facingDirection)
|
||||||
|
{
|
||||||
|
if (offsetEntity == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.RemoveSelfIntersections();
|
||||||
|
polygon.Offset(location);
|
||||||
|
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
public class CutOffConfig
|
||||||
|
{
|
||||||
|
public double PartClearance { get; set; } = 0.02;
|
||||||
|
public double Overtravel { get; set; }
|
||||||
|
public double MinSegmentLength { get; set; } = 0.05;
|
||||||
|
public string Direction { get; set; } = "AwayFromOrigin";
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000980001",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"name": "CL-980",
|
||||||
|
"type": "laser",
|
||||||
|
"units": "inches",
|
||||||
|
"materials": [
|
||||||
|
{
|
||||||
|
"name": "Mild Steel",
|
||||||
|
"grade": "A36",
|
||||||
|
"density": 0.2836,
|
||||||
|
"thicknesses": [
|
||||||
|
{
|
||||||
|
"value": 0.060,
|
||||||
|
"kerf": 0.008,
|
||||||
|
"assistGas": "O2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x120", "60x120" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.075,
|
||||||
|
"kerf": 0.008,
|
||||||
|
"assistGas": "O2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x120", "60x120" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.105,
|
||||||
|
"kerf": 0.010,
|
||||||
|
"assistGas": "O2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.1875, "angle": 90.0, "radius": 0.09375 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.09375, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.375, "overtravel": 0.1875, "minSegmentLength": 0.75, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x120", "60x120" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.135,
|
||||||
|
"kerf": 0.010,
|
||||||
|
"assistGas": "O2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.1875, "angle": 90.0, "radius": 0.09375 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.09375, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.375, "overtravel": 0.1875, "minSegmentLength": 0.75, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x120", "60x120", "60x144" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.1875,
|
||||||
|
"kerf": 0.012,
|
||||||
|
"assistGas": "O2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.25, "angle": 90.0, "radius": 0.125 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.125, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.5, "overtravel": 0.25, "minSegmentLength": 1.0, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x120", "60x120", "60x144" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.250,
|
||||||
|
"kerf": 0.012,
|
||||||
|
"assistGas": "O2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.25, "angle": 90.0, "radius": 0.125 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.125, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.5, "overtravel": 0.25, "minSegmentLength": 1.0, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x120", "60x120", "60x144" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.375,
|
||||||
|
"kerf": 0.016,
|
||||||
|
"assistGas": "O2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.375, "angle": 90.0, "radius": 0.1875 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.1875, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.625, "overtravel": 0.3125, "minSegmentLength": 1.25, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "60x120", "60x144", "72x120" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.500,
|
||||||
|
"kerf": 0.020,
|
||||||
|
"assistGas": "O2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.5, "angle": 90.0, "radius": 0.25 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.25, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.75, "overtravel": 0.375, "minSegmentLength": 1.5, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "60x120", "60x144", "72x120" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stainless Steel",
|
||||||
|
"grade": "304",
|
||||||
|
"density": 0.289,
|
||||||
|
"thicknesses": [
|
||||||
|
{
|
||||||
|
"value": 0.060,
|
||||||
|
"kerf": 0.008,
|
||||||
|
"assistGas": "N2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x96", "48x120", "60x120" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.075,
|
||||||
|
"kerf": 0.008,
|
||||||
|
"assistGas": "N2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x96", "48x120", "60x120" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.105,
|
||||||
|
"kerf": 0.010,
|
||||||
|
"assistGas": "N2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.1875, "angle": 90.0, "radius": 0.09375 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.09375, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.375, "overtravel": 0.1875, "minSegmentLength": 0.75, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x96", "48x120", "60x120" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.250,
|
||||||
|
"kerf": 0.014,
|
||||||
|
"assistGas": "N2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.25, "angle": 90.0, "radius": 0.125 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.125, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.5, "overtravel": 0.25, "minSegmentLength": 1.0, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x96", "48x120", "60x120" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Aluminum",
|
||||||
|
"grade": "5052",
|
||||||
|
"density": 0.097,
|
||||||
|
"thicknesses": [
|
||||||
|
{
|
||||||
|
"value": 0.060,
|
||||||
|
"kerf": 0.008,
|
||||||
|
"assistGas": "N2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x96", "48x120", "60x120" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.080,
|
||||||
|
"kerf": 0.008,
|
||||||
|
"assistGas": "N2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x96", "48x120", "60x120" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.125,
|
||||||
|
"kerf": 0.010,
|
||||||
|
"assistGas": "N2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.1875, "angle": 90.0, "radius": 0.09375 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.09375, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.375, "overtravel": 0.1875, "minSegmentLength": 0.75, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x96", "48x120", "60x120" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.250,
|
||||||
|
"kerf": 0.014,
|
||||||
|
"assistGas": "N2",
|
||||||
|
"leadIn": { "type": "Arc", "length": 0.25, "angle": 90.0, "radius": 0.125 },
|
||||||
|
"leadOut": { "type": "Line", "length": 0.125, "angle": 90.0, "radius": 0.0 },
|
||||||
|
"cutOff": { "partClearance": 0.5, "overtravel": 0.25, "minSegmentLength": 1.0, "direction": "AwayFromOrigin" },
|
||||||
|
"plateSizes": [ "48x96", "48x120", "60x120" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
public interface IDataProvider
|
||||||
|
{
|
||||||
|
IReadOnlyList<MachineSummary> GetMachines();
|
||||||
|
MachineConfig? GetMachine(Guid id);
|
||||||
|
void SaveMachine(MachineConfig machine);
|
||||||
|
void DeleteMachine(Guid id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
public class LeadConfig
|
||||||
|
{
|
||||||
|
public string Type { get; set; } = "Line";
|
||||||
|
public double Length { get; set; }
|
||||||
|
public double Angle { get; set; } = 90.0;
|
||||||
|
public double Radius { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
public class LocalJsonProvider : IDataProvider
|
||||||
|
{
|
||||||
|
private readonly string _directory;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||||
|
};
|
||||||
|
|
||||||
|
public LocalJsonProvider(string directory)
|
||||||
|
{
|
||||||
|
_directory = directory;
|
||||||
|
Directory.CreateDirectory(_directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<MachineSummary> GetMachines()
|
||||||
|
{
|
||||||
|
var summaries = new List<MachineSummary>();
|
||||||
|
foreach (var file in Directory.GetFiles(_directory, "*.json"))
|
||||||
|
{
|
||||||
|
var machine = ReadFile(file);
|
||||||
|
if (machine is not null)
|
||||||
|
summaries.Add(new MachineSummary(machine.Id, machine.Name));
|
||||||
|
}
|
||||||
|
return summaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MachineConfig? GetMachine(Guid id)
|
||||||
|
{
|
||||||
|
var path = GetPath(id);
|
||||||
|
return File.Exists(path) ? ReadFile(path) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveMachine(MachineConfig machine)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(machine, JsonOptions);
|
||||||
|
var path = GetPath(machine.Id);
|
||||||
|
WriteWithRetry(path, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteMachine(Guid id)
|
||||||
|
{
|
||||||
|
var path = GetPath(id);
|
||||||
|
if (File.Exists(path))
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPath(Guid id) => Path.Combine(_directory, $"{id}.json");
|
||||||
|
|
||||||
|
private static MachineConfig? ReadFile(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return JsonSerializer.Deserialize<MachineConfig>(json, JsonOptions);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnsureDefaults()
|
||||||
|
{
|
||||||
|
if (Directory.GetFiles(_directory, "*.json").Length > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var assembly = typeof(LocalJsonProvider).Assembly;
|
||||||
|
var resourceName = assembly.GetManifestResourceNames()
|
||||||
|
.FirstOrDefault(n => n.EndsWith("CL-980.json"));
|
||||||
|
|
||||||
|
if (resourceName is null) return;
|
||||||
|
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
|
if (stream is null) return;
|
||||||
|
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var json = reader.ReadToEnd();
|
||||||
|
|
||||||
|
var config = JsonSerializer.Deserialize<MachineConfig>(json, JsonOptions);
|
||||||
|
if (config is null) return;
|
||||||
|
|
||||||
|
SaveMachine(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteWithRetry(string path, string json, int maxRetries = 3)
|
||||||
|
{
|
||||||
|
for (var attempt = 0; attempt < maxRetries; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (IOException) when (attempt < maxRetries - 1)
|
||||||
|
{
|
||||||
|
Thread.Sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
public class MachineConfig
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public int SchemaVersion { get; set; } = 1;
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public MachineType Type { get; set; } = MachineType.Laser;
|
||||||
|
public UnitSystem Units { get; set; } = UnitSystem.Inches;
|
||||||
|
public List<MaterialConfig> Materials { get; set; } = new();
|
||||||
|
|
||||||
|
public ThicknessConfig? GetParameters(string material, double thickness)
|
||||||
|
{
|
||||||
|
var mat = GetMaterial(material);
|
||||||
|
if (mat is null) return null;
|
||||||
|
return mat.Thicknesses.FirstOrDefault(t => t.Value.IsEqualTo(thickness));
|
||||||
|
}
|
||||||
|
|
||||||
|
public MaterialConfig? GetMaterial(string name)
|
||||||
|
{
|
||||||
|
return Materials.FirstOrDefault(m =>
|
||||||
|
string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
public record MachineSummary(Guid Id, string Name);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
public enum MachineType
|
||||||
|
{
|
||||||
|
Laser,
|
||||||
|
Plasma,
|
||||||
|
Waterjet
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
public class MaterialConfig
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Grade { get; set; } = "";
|
||||||
|
public double Density { get; set; }
|
||||||
|
public List<ThicknessConfig> Thicknesses { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<RootNamespace>OpenNest.Data</RootNamespace>
|
||||||
|
<AssemblyName>OpenNest.Data</AssemblyName>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Defaults\CL-980.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
public class ThicknessConfig
|
||||||
|
{
|
||||||
|
public double Value { get; set; }
|
||||||
|
public double Kerf { get; set; }
|
||||||
|
public string AssistGas { get; set; } = "";
|
||||||
|
public LeadConfig LeadIn { get; set; } = new();
|
||||||
|
public LeadConfig LeadOut { get; set; } = new();
|
||||||
|
public CutOffConfig CutOff { get; set; } = new();
|
||||||
|
public List<string> PlateSizes { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
public enum UnitSystem
|
||||||
|
{
|
||||||
|
Inches,
|
||||||
|
Millimeters
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
public double MaxPlateHeight { get; set; }
|
public double MaxPlateHeight { get; set; }
|
||||||
public double MaxAspectRatio { get; set; } = 5.0;
|
public double MaxAspectRatio { get; set; } = 5.0;
|
||||||
public double MinUtilization { get; set; } = 0.3;
|
public double MinUtilization { get; set; } = 0.3;
|
||||||
|
public double UtilizationOverride { get; set; } = 0.75;
|
||||||
|
|
||||||
public void Apply(List<BestFitResult> results)
|
public void Apply(List<BestFitResult> results)
|
||||||
{
|
{
|
||||||
@@ -25,7 +26,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
var aspect = result.LongestSide / result.ShortestSide;
|
var aspect = result.LongestSide / result.ShortestSide;
|
||||||
|
|
||||||
if (aspect > MaxAspectRatio)
|
if (aspect > MaxAspectRatio && result.Utilization < UtilizationOverride)
|
||||||
{
|
{
|
||||||
result.Keep = false;
|
result.Keep = false;
|
||||||
result.Reason = string.Format("Aspect ratio {0:F1} exceeds max {1}", aspect, MaxAspectRatio);
|
result.Reason = string.Format("Aspect ratio {0:F1} exceeds max {1}", aspect, MaxAspectRatio);
|
||||||
|
|||||||
@@ -22,18 +22,11 @@ namespace OpenNest.Engine.BestFit
|
|||||||
if (perimeter == null)
|
if (perimeter == null)
|
||||||
return new PolygonExtractionResult(null, Vector.Zero);
|
return new PolygonExtractionResult(null, Vector.Zero);
|
||||||
|
|
||||||
// Inflate by half-spacing if spacing is non-zero.
|
// Ensure CW winding for correct outward offset direction.
|
||||||
// Detect winding direction to choose the correct outward offset side.
|
definedShape.NormalizeWinding();
|
||||||
var outwardSide = OffsetSide.Right;
|
|
||||||
if (halfSpacing > 0)
|
|
||||||
{
|
|
||||||
var testPoly = perimeter.ToPolygon();
|
|
||||||
if (testPoly.Vertices.Count >= 3 && testPoly.RotationDirection() == RotationType.CW)
|
|
||||||
outwardSide = OffsetSide.Left;
|
|
||||||
}
|
|
||||||
|
|
||||||
var inflated = halfSpacing > 0
|
var inflated = halfSpacing > 0
|
||||||
? (perimeter.OffsetEntity(halfSpacing, outwardSide) as Shape ?? perimeter)
|
? (perimeter.OffsetOutward(halfSpacing) ?? perimeter)
|
||||||
: perimeter;
|
: perimeter;
|
||||||
|
|
||||||
// Convert to polygon with circumscribed arcs for tight nesting.
|
// Convert to polygon with circumscribed arcs for tight nesting.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using OpenNest.Math;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Fill
|
namespace OpenNest.Engine.Fill
|
||||||
@@ -349,6 +350,21 @@ namespace OpenNest.Engine.Fill
|
|||||||
if (copyDistance <= Tolerance.Epsilon)
|
if (copyDistance <= Tolerance.Epsilon)
|
||||||
copyDistance = columnWidth + partSpacing;
|
copyDistance = columnWidth + partSpacing;
|
||||||
|
|
||||||
|
// Safety: if the compacted test column overlaps the original column,
|
||||||
|
// fall back to bbox-based spacing.
|
||||||
|
var probe = new List<Part>(column);
|
||||||
|
probe.AddRange(testColumn.Where(IsWithinWorkArea));
|
||||||
|
if (HasOverlappingParts(probe))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[FillExtents] Compacted column overlaps, falling back to bbox spacing");
|
||||||
|
copyDistance = columnWidth + partSpacing;
|
||||||
|
|
||||||
|
// Rebuild test column at safe distance.
|
||||||
|
testColumn.Clear();
|
||||||
|
foreach (var part in column)
|
||||||
|
testColumn.Add(part.CloneAtOffset(new Vector(copyDistance, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})");
|
Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})");
|
||||||
|
|
||||||
// Build all columns.
|
// Build all columns.
|
||||||
|
|||||||
@@ -287,6 +287,65 @@ namespace OpenNest.Engine.Fill
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fallback tiling using bounding-box spacing when geometry-aware tiling
|
||||||
|
/// produces overlapping parts.
|
||||||
|
/// </summary>
|
||||||
|
private List<Part> TilePatternBbox(Pattern basePattern, NestDirection direction)
|
||||||
|
{
|
||||||
|
var copyDistance = GetDimension(basePattern.BoundingBox, direction) + PartSpacing;
|
||||||
|
|
||||||
|
if (copyDistance <= 0)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
var dim = GetDimension(basePattern.BoundingBox, direction);
|
||||||
|
var start = GetStart(basePattern.BoundingBox, direction);
|
||||||
|
var limit = GetLimit(direction);
|
||||||
|
|
||||||
|
var result = new List<Part>();
|
||||||
|
var count = 1;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var nextPos = start + copyDistance * count;
|
||||||
|
|
||||||
|
if (nextPos + dim > limit + Tolerance.Epsilon)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var offset = MakeOffset(direction, copyDistance * count);
|
||||||
|
|
||||||
|
foreach (var part in basePattern.Parts)
|
||||||
|
result.Add(part.CloneAtOffset(offset));
|
||||||
|
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasOverlappingParts(List<Part> parts)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var b1 = parts[i].BoundingBox;
|
||||||
|
|
||||||
|
for (var j = i + 1; j < parts.Count; j++)
|
||||||
|
{
|
||||||
|
var b2 = parts[j].BoundingBox;
|
||||||
|
|
||||||
|
var overlapX = System.Math.Min(b1.Right, b2.Right)
|
||||||
|
- System.Math.Max(b1.Left, b2.Left);
|
||||||
|
var overlapY = System.Math.Min(b1.Top, b2.Top)
|
||||||
|
- System.Math.Max(b1.Bottom, b2.Bottom);
|
||||||
|
|
||||||
|
if (overlapX > Tolerance.Epsilon && overlapY > Tolerance.Epsilon)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a seed pattern containing a single part positioned at the work area origin.
|
/// Creates a seed pattern containing a single part positioned at the work area origin.
|
||||||
/// Returns an empty pattern if the part does not fit.
|
/// Returns an empty pattern if the part does not fit.
|
||||||
@@ -325,10 +384,25 @@ namespace OpenNest.Engine.Fill
|
|||||||
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, boundaries));
|
||||||
|
|
||||||
|
// Safety: if geometry-aware spacing produced overlapping parts,
|
||||||
|
// fall back to bbox-based spacing for this axis.
|
||||||
|
if (pattern.Parts.Count > 1 && HasOverlappingParts(row))
|
||||||
|
{
|
||||||
|
row = new List<Part>(pattern.Parts);
|
||||||
|
row.AddRange(TilePatternBbox(pattern, direction));
|
||||||
|
}
|
||||||
|
|
||||||
// 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, boundaries));
|
||||||
|
|
||||||
|
if (pattern.Parts.Count > 1 && HasOverlappingParts(row))
|
||||||
|
{
|
||||||
|
row = new List<Part>(pattern.Parts);
|
||||||
|
row.AddRange(TilePatternBbox(pattern, perpAxis));
|
||||||
|
}
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -321,9 +321,19 @@ namespace OpenNest.Engine.Fill
|
|||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
var remnantEngine = NestEngineRegistry.Create(plate);
|
var filler = new FillLinear(remnantBox, partSpacing);
|
||||||
var item = new NestItem { Drawing = drawing };
|
List<Part> parts = null;
|
||||||
var parts = remnantEngine.Fill(item, remnantBox, null, token);
|
|
||||||
|
foreach (var angle in new[] { 0.0, Angle.HalfPI })
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var result = FillHelpers.FillWithDirectionPreference(
|
||||||
|
dir => filler.Fill(drawing, angle, dir),
|
||||||
|
null, comparer, remnantBox);
|
||||||
|
|
||||||
|
if (result != null && result.Count > (parts?.Count ?? 0))
|
||||||
|
parts = result;
|
||||||
|
}
|
||||||
|
|
||||||
Debug.WriteLine($"[PairFiller] Remnant: {parts?.Count ?? 0} parts in " +
|
Debug.WriteLine($"[PairFiller] Remnant: {parts?.Count ?? 0} parts in " +
|
||||||
$"{remnantBox.Width:F2}x{remnantBox.Length:F2}");
|
$"{remnantBox.Width:F2}x{remnantBox.Length:F2}");
|
||||||
|
|||||||
@@ -244,28 +244,29 @@ public class StripeFiller
|
|||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear");
|
var filler = new FillLinear(remnantBox, spacing);
|
||||||
try
|
List<Part> best = null;
|
||||||
|
|
||||||
|
foreach (var angle in new[] { 0.0, Angle.HalfPI })
|
||||||
{
|
{
|
||||||
var engine = CreateRemnantEngine(_context.Plate);
|
_context.Token.ThrowIfCancellationRequested();
|
||||||
var item = new NestItem { Drawing = drawing };
|
var result = FillHelpers.FillWithDirectionPreference(
|
||||||
var parts = engine.Fill(item, remnantBox, _context.Progress, _context.Token);
|
dir => filler.Fill(drawing, angle, dir),
|
||||||
|
null, _comparer, remnantBox);
|
||||||
|
|
||||||
Debug.WriteLine($"[StripeFiller] Remnant engine ({engine.Name}): {parts?.Count ?? 0} parts, " +
|
if (result != null && result.Count > (best?.Count ?? 0))
|
||||||
$"winner={engine.WinnerPhase}");
|
best = result;
|
||||||
|
|
||||||
if (parts != null && parts.Count > 0)
|
|
||||||
{
|
|
||||||
FillResultCache.Store(drawing, remnantBox, spacing, parts);
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
Debug.WriteLine($"[StripeFiller] Remnant linear: {best?.Count ?? 0} parts");
|
||||||
|
|
||||||
|
if (best != null && best.Count > 0)
|
||||||
{
|
{
|
||||||
FillStrategyRegistry.SetEnabled(null);
|
FillResultCache.Store(drawing, remnantBox, spacing, best);
|
||||||
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double FindAngleForTargetSpan(
|
public static double FindAngleForTargetSpan(
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.IO.Bom
|
||||||
|
{
|
||||||
|
public class BomAnalysis
|
||||||
|
{
|
||||||
|
public List<MaterialGroup> Groups { get; set; } = new List<MaterialGroup>();
|
||||||
|
public List<BomItem> Skipped { get; set; } = new List<BomItem>();
|
||||||
|
public List<BomItem> Unmatched { get; set; } = new List<BomItem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MaterialGroup
|
||||||
|
{
|
||||||
|
public string Material { get; set; }
|
||||||
|
public double Thickness { get; set; }
|
||||||
|
public List<MatchedPart> Parts { get; set; } = new List<MatchedPart>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MatchedPart
|
||||||
|
{
|
||||||
|
public BomItem Item { get; set; }
|
||||||
|
public string DxfPath { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BomAnalyzer
|
||||||
|
{
|
||||||
|
public static BomAnalysis Analyze(List<BomItem> items, string dxfFolder)
|
||||||
|
{
|
||||||
|
var result = new BomAnalysis();
|
||||||
|
|
||||||
|
// Build a case-insensitive lookup of DXF files in the folder (if it exists)
|
||||||
|
var folderExists = Directory.Exists(dxfFolder);
|
||||||
|
var dxfFiles = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (folderExists)
|
||||||
|
{
|
||||||
|
foreach (var file in Directory.GetFiles(dxfFolder, "*.dxf"))
|
||||||
|
{
|
||||||
|
var nameWithoutExt = Path.GetFileNameWithoutExtension(file);
|
||||||
|
dxfFiles[nameWithoutExt] = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partition items into: skipped, unmatched, or matched (grouped)
|
||||||
|
var matched = new List<MatchedPart>();
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(item.FileName) || !item.Thickness.HasValue)
|
||||||
|
{
|
||||||
|
result.Skipped.Add(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lookupName = item.FileName;
|
||||||
|
|
||||||
|
// Strip .dxf extension if the BOM includes it
|
||||||
|
if (lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase))
|
||||||
|
lookupName = Path.GetFileNameWithoutExtension(lookupName);
|
||||||
|
|
||||||
|
if (!folderExists)
|
||||||
|
{
|
||||||
|
// No folder to search — group items without a DXF path
|
||||||
|
matched.Add(new MatchedPart { Item = item, DxfPath = null });
|
||||||
|
}
|
||||||
|
else if (dxfFiles.TryGetValue(lookupName, out var dxfPath))
|
||||||
|
{
|
||||||
|
matched.Add(new MatchedPart { Item = item, DxfPath = dxfPath });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Unmatched.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group matched parts by material + thickness
|
||||||
|
var groups = matched
|
||||||
|
.GroupBy(p => new
|
||||||
|
{
|
||||||
|
Material = (p.Item.Material ?? "").ToUpperInvariant(),
|
||||||
|
Thickness = p.Item.Thickness.Value
|
||||||
|
})
|
||||||
|
.Select(g => new MaterialGroup
|
||||||
|
{
|
||||||
|
Material = g.First().Item.Material ?? "",
|
||||||
|
Thickness = g.Key.Thickness,
|
||||||
|
Parts = g.ToList()
|
||||||
|
})
|
||||||
|
.OrderBy(g => g.Material)
|
||||||
|
.ThenBy(g => g.Thickness)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
result.Groups = groups;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
namespace OpenNest.IO.Bom
|
||||||
|
{
|
||||||
|
public class BomItem
|
||||||
|
{
|
||||||
|
[Column("Item #", "Item Number", "Item Num")]
|
||||||
|
public int? ItemNum { get; set; }
|
||||||
|
|
||||||
|
[Column("File Name")]
|
||||||
|
public string FileName { get; set; }
|
||||||
|
|
||||||
|
[Column("Qty", "Quantity")]
|
||||||
|
public int? Qty { get; set; }
|
||||||
|
|
||||||
|
[Column("Description")]
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[Column("Part", "Part Name")]
|
||||||
|
public string PartName { get; set; }
|
||||||
|
|
||||||
|
[Column("Config", "Configuration")]
|
||||||
|
public string ConfigurationName { get; set; }
|
||||||
|
|
||||||
|
[Column("Thickness")]
|
||||||
|
public double? Thickness { get; set; }
|
||||||
|
|
||||||
|
[Column("Material")]
|
||||||
|
public string Material { get; set; }
|
||||||
|
|
||||||
|
[Column("K-Factor")]
|
||||||
|
public double? KFactor { get; set; }
|
||||||
|
|
||||||
|
[Column("Default Bend Radius")]
|
||||||
|
public double? DefaultBendRadius { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using ClosedXML.Excel;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace OpenNest.IO.Bom
|
||||||
|
{
|
||||||
|
public class BomReader : IDisposable
|
||||||
|
{
|
||||||
|
private readonly XLWorkbook workbook;
|
||||||
|
private Dictionary<PropertyInfo, int> columnNameIndexDict;
|
||||||
|
|
||||||
|
public BomReader(string file)
|
||||||
|
{
|
||||||
|
workbook = new XLWorkbook(file);
|
||||||
|
columnNameIndexDict = new Dictionary<PropertyInfo, int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IXLWorksheet GetPartsWorksheet()
|
||||||
|
{
|
||||||
|
if (!workbook.TryGetWorksheet("Parts", out var worksheet))
|
||||||
|
throw new InvalidOperationException("BOM file does not contain a 'Parts' worksheet.");
|
||||||
|
return worksheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FindColumnIndexes(IXLWorksheet worksheet)
|
||||||
|
{
|
||||||
|
var lastColumn = worksheet.LastColumnUsed()?.ColumnNumber() ?? 0;
|
||||||
|
var properties = typeof(BomItem).GetProperties();
|
||||||
|
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
var column = property.GetCustomAttribute<ColumnAttribute>();
|
||||||
|
|
||||||
|
if (column == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var classColumnNames = column.Names.Select(n => n.ToUpper());
|
||||||
|
|
||||||
|
for (var columnIndex = 1; columnIndex <= lastColumn; columnIndex++)
|
||||||
|
{
|
||||||
|
var cell = worksheet.Cell(1, columnIndex);
|
||||||
|
if (cell.IsEmpty()) continue;
|
||||||
|
|
||||||
|
var excelColumnName = cell.GetString().ToUpper();
|
||||||
|
var isMatch = classColumnNames.Any(n => n == excelColumnName);
|
||||||
|
|
||||||
|
if (!isMatch)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
columnNameIndexDict.Add(property, columnIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BomItem> GetItems()
|
||||||
|
{
|
||||||
|
var worksheet = GetPartsWorksheet();
|
||||||
|
|
||||||
|
FindColumnIndexes(worksheet);
|
||||||
|
|
||||||
|
var lastRow = worksheet.LastRowUsed()?.RowNumber() ?? 1;
|
||||||
|
var items = new List<BomItem>();
|
||||||
|
|
||||||
|
for (var rowIndex = 2; rowIndex <= lastRow; rowIndex++)
|
||||||
|
{
|
||||||
|
var item = new BomItem();
|
||||||
|
|
||||||
|
foreach (var dictItem in columnNameIndexDict)
|
||||||
|
{
|
||||||
|
var property = dictItem.Key;
|
||||||
|
var excelColumnIndex = dictItem.Value;
|
||||||
|
var cell = worksheet.Cell(rowIndex, excelColumnIndex);
|
||||||
|
var type = property.PropertyType;
|
||||||
|
|
||||||
|
if (type == typeof(int?))
|
||||||
|
property.SetValue(item, cell.ToIntOrNull());
|
||||||
|
else if (type == typeof(string))
|
||||||
|
property.SetValue(item, cell.IsEmpty() ? null : cell.GetString());
|
||||||
|
else if (type == typeof(double?))
|
||||||
|
property.SetValue(item, cell.ToDoubleOrNull());
|
||||||
|
else
|
||||||
|
throw new NotImplementedException($"Unsupported property type: {type}");
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
workbook?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using ClosedXML.Excel;
|
||||||
|
|
||||||
|
namespace OpenNest.IO.Bom
|
||||||
|
{
|
||||||
|
public static class CellExtensions
|
||||||
|
{
|
||||||
|
public static int? ToIntOrNull(this IXLCell cell)
|
||||||
|
{
|
||||||
|
if (cell.IsEmpty()) return null;
|
||||||
|
if (cell.DataType == XLDataType.Number) return (int)cell.GetDouble();
|
||||||
|
if (int.TryParse(cell.GetString(), out var i)) return i;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double? ToDoubleOrNull(this IXLCell cell)
|
||||||
|
{
|
||||||
|
if (cell.IsEmpty()) return null;
|
||||||
|
if (cell.DataType == XLDataType.Number) return cell.GetDouble();
|
||||||
|
if (double.TryParse(cell.GetString(), out var result)) return result;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace OpenNest.IO.Bom
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class ColumnAttribute : Attribute
|
||||||
|
{
|
||||||
|
public ColumnAttribute(params string[] names)
|
||||||
|
{
|
||||||
|
Names = names;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] Names { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace OpenNest.IO.Bom
|
||||||
|
{
|
||||||
|
public static class Fraction
|
||||||
|
{
|
||||||
|
public static readonly Regex FractionRegex =
|
||||||
|
new Regex(@"((?<WholeNum>\d+)(\ |-))?(?<Fraction>\d+\/\d+)");
|
||||||
|
|
||||||
|
public static bool IsValid(string s)
|
||||||
|
{
|
||||||
|
return FractionRegex.IsMatch(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double Parse(string s)
|
||||||
|
{
|
||||||
|
var match = FractionRegex.Match(s);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
throw new FormatException("Invalid fraction format.");
|
||||||
|
|
||||||
|
var value = 0.0;
|
||||||
|
|
||||||
|
var wholeNumGroup = match.Groups["WholeNum"];
|
||||||
|
var fractionGroup = match.Groups["Fraction"];
|
||||||
|
|
||||||
|
if (wholeNumGroup.Success)
|
||||||
|
value = double.Parse(wholeNumGroup.Value);
|
||||||
|
|
||||||
|
if (fractionGroup.Success)
|
||||||
|
{
|
||||||
|
var parts = fractionGroup.Value.Split('/');
|
||||||
|
var numerator = double.Parse(parts[0]);
|
||||||
|
var denominator = double.Parse(parts[1]);
|
||||||
|
value += System.Math.Round(numerator / denominator, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParse(string s, out double fraction)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fraction = Parse(s);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
fraction = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ReplaceFractionsWithDecimals(string input)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(input);
|
||||||
|
|
||||||
|
var fractionMatches = FractionRegex.Matches(sb.ToString())
|
||||||
|
.Cast<Match>()
|
||||||
|
.OrderByDescending(m => m.Index);
|
||||||
|
|
||||||
|
foreach (var fractionMatch in fractionMatches)
|
||||||
|
{
|
||||||
|
var decimalValue = Parse(fractionMatch.Value);
|
||||||
|
sb.Remove(fractionMatch.Index, fractionMatch.Length);
|
||||||
|
sb.Insert(fractionMatch.Index, decimalValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,5 +8,6 @@
|
|||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
<PackageReference Include="ACadSharp" Version="3.1.32" />
|
<PackageReference Include="ACadSharp" Version="3.1.32" />
|
||||||
|
<PackageReference Include="ClosedXML" Version="0.104.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
using OpenNest.Shapes;
|
using OpenNest.Shapes;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
@@ -103,7 +104,8 @@ namespace OpenNest.Mcp.Tools
|
|||||||
if (geometry.Count == 0)
|
if (geometry.Count == 0)
|
||||||
return "Error: no geometry found in DXF file";
|
return "Error: no geometry found in DXF file";
|
||||||
|
|
||||||
var pgm = ConvertGeometry.ToProgram(geometry);
|
var normalized = ShapeProfile.NormalizeEntities(geometry);
|
||||||
|
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||||
|
|
||||||
if (pgm == null)
|
if (pgm == null)
|
||||||
return "Error: failed to convert geometry to program";
|
return "Error: failed to convert geometry to program";
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
namespace OpenNest.Tests.Bom;
|
||||||
|
|
||||||
|
using OpenNest.IO.Bom;
|
||||||
|
|
||||||
|
public class BomAnalyzerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_GroupsByMaterialAndThickness()
|
||||||
|
{
|
||||||
|
var items = new List<BomItem>
|
||||||
|
{
|
||||||
|
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 },
|
||||||
|
new BomItem { FileName = "PT02", Thickness = 0.25, Material = "AISI 304", Qty = 3 },
|
||||||
|
new BomItem { FileName = "PT03", Thickness = 0.375, Material = "AISI 304", Qty = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = BomAnalyzer.Analyze(items, "C:\\fake");
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Groups.Count);
|
||||||
|
Assert.Single(result.Groups, g => g.Thickness == 0.25 && g.Material == "AISI 304");
|
||||||
|
Assert.Single(result.Groups, g => g.Thickness == 0.375 && g.Material == "AISI 304");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_SkipsItemsWithNoFileName()
|
||||||
|
{
|
||||||
|
var items = new List<BomItem>
|
||||||
|
{
|
||||||
|
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 },
|
||||||
|
new BomItem { FileName = null, Thickness = 0.25, Material = "AISI 304", Qty = 3 },
|
||||||
|
new BomItem { FileName = "", Thickness = 0.25, Material = "AISI 304", Qty = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = BomAnalyzer.Analyze(items, "C:\\fake");
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Skipped.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_SkipsItemsWithNoThickness()
|
||||||
|
{
|
||||||
|
var items = new List<BomItem>
|
||||||
|
{
|
||||||
|
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 },
|
||||||
|
new BomItem { FileName = "PT02", Thickness = null, Material = "AISI 304", Qty = 3 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = BomAnalyzer.Analyze(items, "C:\\fake");
|
||||||
|
|
||||||
|
Assert.Single(result.Skipped);
|
||||||
|
Assert.Equal("PT02", result.Skipped[0].FileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_GroupsMaterialCaseInsensitive()
|
||||||
|
{
|
||||||
|
var items = new List<BomItem>
|
||||||
|
{
|
||||||
|
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 1 },
|
||||||
|
new BomItem { FileName = "PT02", Thickness = 0.25, Material = "aisi 304", Qty = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = BomAnalyzer.Analyze(items, "C:\\fake");
|
||||||
|
|
||||||
|
Assert.Single(result.Groups);
|
||||||
|
Assert.Equal(2, result.Groups[0].Parts.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_MatchesDxfFiles_WithAndWithoutExtension()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), "BomAnalyzerTest_" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(Path.Combine(tempDir, "PT01.dxf"), "");
|
||||||
|
|
||||||
|
var items = new List<BomItem>
|
||||||
|
{
|
||||||
|
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = BomAnalyzer.Analyze(items, tempDir);
|
||||||
|
|
||||||
|
Assert.Single(result.Groups);
|
||||||
|
Assert.Single(result.Groups[0].Parts);
|
||||||
|
Assert.EndsWith(".dxf", result.Groups[0].Parts[0].DxfPath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Empty(result.Unmatched);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_ReportsUnmatchedItems_WhenDxfNotFound()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), "BomAnalyzerTest_" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var items = new List<BomItem>
|
||||||
|
{
|
||||||
|
new BomItem { FileName = "PT99", Thickness = 0.25, Material = "AISI 304", Qty = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = BomAnalyzer.Analyze(items, tempDir);
|
||||||
|
|
||||||
|
Assert.Single(result.Unmatched);
|
||||||
|
Assert.Empty(result.Groups);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_DifferentMaterials_SameThickness_SeparateGroups()
|
||||||
|
{
|
||||||
|
var items = new List<BomItem>
|
||||||
|
{
|
||||||
|
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 1 },
|
||||||
|
new BomItem { FileName = "PT02", Thickness = 0.25, Material = "Plain Carbon Steel", Qty = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = BomAnalyzer.Analyze(items, "C:\\fake");
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Groups.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_GroupPartsCount_MatchesBomItems()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), "BomAnalyzerTest_" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(Path.Combine(tempDir, "PT01.dxf"), "");
|
||||||
|
File.WriteAllText(Path.Combine(tempDir, "PT02.dxf"), "");
|
||||||
|
File.WriteAllText(Path.Combine(tempDir, "PT03.dxf"), "");
|
||||||
|
|
||||||
|
var items = new List<BomItem>
|
||||||
|
{
|
||||||
|
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 },
|
||||||
|
new BomItem { FileName = "PT02", Thickness = 0.25, Material = "AISI 304", Qty = 5 },
|
||||||
|
new BomItem { FileName = "PT03", Thickness = 0.375, Material = "AISI 304", Qty = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = BomAnalyzer.Analyze(items, tempDir);
|
||||||
|
|
||||||
|
var quarterGroup = result.Groups.First(g => g.Thickness == 0.25);
|
||||||
|
Assert.Equal(2, quarterGroup.Parts.Count);
|
||||||
|
Assert.Equal(2, quarterGroup.Parts.First(p => p.Item.FileName == "PT01").Item.Qty);
|
||||||
|
|
||||||
|
var threeEighthsGroup = result.Groups.First(g => g.Thickness == 0.375);
|
||||||
|
Assert.Single(threeEighthsGroup.Parts);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using OpenNest.Data;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Data;
|
||||||
|
|
||||||
|
public class DefaultConfigTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _testDir;
|
||||||
|
|
||||||
|
public DefaultConfigTests()
|
||||||
|
{
|
||||||
|
_testDir = Path.Combine(Path.GetTempPath(), "OpenNestTests", Guid.NewGuid().ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_testDir))
|
||||||
|
Directory.Delete(_testDir, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnsureDefaults_EmptyDirectory_CopiesDefaultConfig()
|
||||||
|
{
|
||||||
|
var provider = new LocalJsonProvider(_testDir);
|
||||||
|
provider.EnsureDefaults();
|
||||||
|
var machines = provider.GetMachines();
|
||||||
|
Assert.Single(machines);
|
||||||
|
Assert.Equal("CL-980", machines[0].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnsureDefaults_ExistingFiles_DoesNotCopy()
|
||||||
|
{
|
||||||
|
var provider = new LocalJsonProvider(_testDir);
|
||||||
|
var existing = new MachineConfig { Name = "My Machine" };
|
||||||
|
provider.SaveMachine(existing);
|
||||||
|
|
||||||
|
provider.EnsureDefaults();
|
||||||
|
|
||||||
|
var machines = provider.GetMachines();
|
||||||
|
Assert.Single(machines);
|
||||||
|
Assert.Equal("My Machine", machines[0].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultConfig_HasValidStructure()
|
||||||
|
{
|
||||||
|
var provider = new LocalJsonProvider(_testDir);
|
||||||
|
provider.EnsureDefaults();
|
||||||
|
|
||||||
|
var machines = provider.GetMachines();
|
||||||
|
var config = provider.GetMachine(machines[0].Id);
|
||||||
|
|
||||||
|
Assert.NotNull(config);
|
||||||
|
Assert.Equal(1, config.SchemaVersion);
|
||||||
|
Assert.Equal(MachineType.Laser, config.Type);
|
||||||
|
Assert.Equal(UnitSystem.Inches, config.Units);
|
||||||
|
Assert.NotEmpty(config.Materials);
|
||||||
|
|
||||||
|
foreach (var material in config.Materials)
|
||||||
|
{
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(material.Name));
|
||||||
|
Assert.NotEmpty(material.Thicknesses);
|
||||||
|
|
||||||
|
foreach (var thickness in material.Thicknesses)
|
||||||
|
{
|
||||||
|
Assert.True(thickness.Value > 0);
|
||||||
|
Assert.True(thickness.Kerf > 0);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(thickness.AssistGas));
|
||||||
|
Assert.NotNull(thickness.LeadIn);
|
||||||
|
Assert.NotNull(thickness.LeadOut);
|
||||||
|
Assert.NotNull(thickness.CutOff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
using OpenNest.Data;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Data;
|
||||||
|
|
||||||
|
public class LocalJsonProviderTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _testDir;
|
||||||
|
|
||||||
|
public LocalJsonProviderTests()
|
||||||
|
{
|
||||||
|
_testDir = Path.Combine(Path.GetTempPath(), "OpenNestTests", Guid.NewGuid().ToString());
|
||||||
|
Directory.CreateDirectory(_testDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_testDir))
|
||||||
|
Directory.Delete(_testDir, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalJsonProvider CreateProvider() => new(_testDir);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMachines_EmptyDirectory_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
var provider = CreateProvider();
|
||||||
|
var result = provider.GetMachines();
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SaveMachine_ThenGetMachine_RoundTrips()
|
||||||
|
{
|
||||||
|
var provider = CreateProvider();
|
||||||
|
var machine = new MachineConfig
|
||||||
|
{
|
||||||
|
Name = "Test Laser",
|
||||||
|
Type = MachineType.Laser,
|
||||||
|
Units = UnitSystem.Inches,
|
||||||
|
Materials = new List<MaterialConfig>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Mild Steel",
|
||||||
|
Grade = "A36",
|
||||||
|
Density = 0.2836,
|
||||||
|
Thicknesses = new List<ThicknessConfig>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Value = 0.250,
|
||||||
|
Kerf = 0.012,
|
||||||
|
AssistGas = "O2",
|
||||||
|
LeadIn = new LeadConfig { Type = "Arc", Length = 0.25, Angle = 90.0, Radius = 0.125 },
|
||||||
|
LeadOut = new LeadConfig { Type = "Line", Length = 0.125 },
|
||||||
|
CutOff = new CutOffConfig
|
||||||
|
{
|
||||||
|
PartClearance = 0.5,
|
||||||
|
Overtravel = 0.25,
|
||||||
|
Direction = "AwayFromOrigin",
|
||||||
|
MinSegmentLength = 1.0
|
||||||
|
},
|
||||||
|
PlateSizes = new List<string> { "60x120", "48x96" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
provider.SaveMachine(machine);
|
||||||
|
var loaded = provider.GetMachine(machine.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(loaded);
|
||||||
|
Assert.Equal("Test Laser", loaded.Name);
|
||||||
|
Assert.Equal(MachineType.Laser, loaded.Type);
|
||||||
|
Assert.Equal(UnitSystem.Inches, loaded.Units);
|
||||||
|
Assert.Single(loaded.Materials);
|
||||||
|
|
||||||
|
var mat = loaded.Materials[0];
|
||||||
|
Assert.Equal("Mild Steel", mat.Name);
|
||||||
|
Assert.Equal("A36", mat.Grade);
|
||||||
|
Assert.Equal(0.2836, mat.Density);
|
||||||
|
Assert.Single(mat.Thicknesses);
|
||||||
|
|
||||||
|
var thick = mat.Thicknesses[0];
|
||||||
|
Assert.Equal(0.250, thick.Value);
|
||||||
|
Assert.Equal(0.012, thick.Kerf);
|
||||||
|
Assert.Equal("O2", thick.AssistGas);
|
||||||
|
Assert.Equal("Arc", thick.LeadIn.Type);
|
||||||
|
Assert.Equal(0.25, thick.LeadIn.Length);
|
||||||
|
Assert.Equal("Line", thick.LeadOut.Type);
|
||||||
|
Assert.Equal(0.125, thick.LeadOut.Length);
|
||||||
|
Assert.Equal(0.5, thick.CutOff.PartClearance);
|
||||||
|
Assert.Equal("AwayFromOrigin", thick.CutOff.Direction);
|
||||||
|
Assert.Equal(new List<string> { "60x120", "48x96" }, thick.PlateSizes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMachines_ReturnsSummaries()
|
||||||
|
{
|
||||||
|
var provider = CreateProvider();
|
||||||
|
var m1 = new MachineConfig { Name = "Laser A" };
|
||||||
|
var m2 = new MachineConfig { Name = "Plasma B" };
|
||||||
|
|
||||||
|
provider.SaveMachine(m1);
|
||||||
|
provider.SaveMachine(m2);
|
||||||
|
var summaries = provider.GetMachines();
|
||||||
|
|
||||||
|
Assert.Equal(2, summaries.Count);
|
||||||
|
Assert.Contains(summaries, s => s.Name == "Laser A" && s.Id == m1.Id);
|
||||||
|
Assert.Contains(summaries, s => s.Name == "Plasma B" && s.Id == m2.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMachine_NotFound_ReturnsNull()
|
||||||
|
{
|
||||||
|
var provider = CreateProvider();
|
||||||
|
var result = provider.GetMachine(Guid.NewGuid());
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteMachine_RemovesFile()
|
||||||
|
{
|
||||||
|
var provider = CreateProvider();
|
||||||
|
var machine = new MachineConfig { Name = "To Delete" };
|
||||||
|
provider.SaveMachine(machine);
|
||||||
|
|
||||||
|
provider.DeleteMachine(machine.Id);
|
||||||
|
|
||||||
|
Assert.Null(provider.GetMachine(machine.Id));
|
||||||
|
Assert.Empty(provider.GetMachines());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SaveMachine_OverwritesExisting()
|
||||||
|
{
|
||||||
|
var provider = CreateProvider();
|
||||||
|
var machine = new MachineConfig { Name = "Original" };
|
||||||
|
provider.SaveMachine(machine);
|
||||||
|
|
||||||
|
machine.Name = "Updated";
|
||||||
|
provider.SaveMachine(machine);
|
||||||
|
|
||||||
|
var loaded = provider.GetMachine(machine.Id);
|
||||||
|
Assert.NotNull(loaded);
|
||||||
|
Assert.Equal("Updated", loaded.Name);
|
||||||
|
Assert.Single(provider.GetMachines());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SaveMachine_PreservesSchemaVersion()
|
||||||
|
{
|
||||||
|
var provider = CreateProvider();
|
||||||
|
var machine = new MachineConfig { Name = "Versioned", SchemaVersion = 1 };
|
||||||
|
|
||||||
|
provider.SaveMachine(machine);
|
||||||
|
var loaded = provider.GetMachine(machine.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(loaded);
|
||||||
|
Assert.Equal(1, loaded.SchemaVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using OpenNest.Data;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Data;
|
||||||
|
|
||||||
|
public class MachineConfigTests
|
||||||
|
{
|
||||||
|
private static MachineConfig CreateTestMachine()
|
||||||
|
{
|
||||||
|
return new MachineConfig
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Test Laser",
|
||||||
|
Type = MachineType.Laser,
|
||||||
|
Units = UnitSystem.Inches,
|
||||||
|
Materials = new List<MaterialConfig>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Mild Steel",
|
||||||
|
Grade = "A36",
|
||||||
|
Density = 0.2836,
|
||||||
|
Thicknesses = new List<ThicknessConfig>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Value = 0.250,
|
||||||
|
Kerf = 0.012,
|
||||||
|
AssistGas = "O2",
|
||||||
|
LeadIn = new LeadConfig { Type = "Arc", Radius = 0.25 },
|
||||||
|
LeadOut = new LeadConfig { Type = "Line", Length = 0.125 },
|
||||||
|
PlateSizes = new List<string> { "60x120", "48x96" }
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Value = 0.500,
|
||||||
|
Kerf = 0.020,
|
||||||
|
AssistGas = "O2",
|
||||||
|
LeadIn = new LeadConfig { Type = "Arc", Radius = 0.375 },
|
||||||
|
LeadOut = new LeadConfig { Type = "Line", Length = 0.25 },
|
||||||
|
PlateSizes = new List<string> { "60x120" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Stainless Steel",
|
||||||
|
Grade = "304",
|
||||||
|
Density = 0.289,
|
||||||
|
Thicknesses = new List<ThicknessConfig>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Value = 0.250,
|
||||||
|
Kerf = 0.014,
|
||||||
|
AssistGas = "N2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetParameters_ExactMatch_ReturnsThickness()
|
||||||
|
{
|
||||||
|
var machine = CreateTestMachine();
|
||||||
|
var result = machine.GetParameters("Mild Steel", 0.250);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(0.012, result.Kerf);
|
||||||
|
Assert.Equal("O2", result.AssistGas);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetParameters_WithinTolerance_ReturnsThickness()
|
||||||
|
{
|
||||||
|
var machine = CreateTestMachine();
|
||||||
|
var result = machine.GetParameters("Mild Steel", 0.250001);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(0.012, result.Kerf);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetParameters_NoMatch_ReturnsNull()
|
||||||
|
{
|
||||||
|
var machine = CreateTestMachine();
|
||||||
|
var result = machine.GetParameters("Mild Steel", 0.375);
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetParameters_CaseInsensitiveMaterial()
|
||||||
|
{
|
||||||
|
var machine = CreateTestMachine();
|
||||||
|
var result = machine.GetParameters("mild steel", 0.250);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(0.012, result.Kerf);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetParameters_UnknownMaterial_ReturnsNull()
|
||||||
|
{
|
||||||
|
var machine = CreateTestMachine();
|
||||||
|
var result = machine.GetParameters("Titanium", 0.250);
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMaterial_ReturnsMaterialByName()
|
||||||
|
{
|
||||||
|
var machine = CreateTestMachine();
|
||||||
|
var result = machine.GetMaterial("Stainless Steel");
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("304", result.Grade);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMaterial_CaseInsensitive()
|
||||||
|
{
|
||||||
|
var machine = CreateTestMachine();
|
||||||
|
var result = machine.GetMaterial("stainless steel");
|
||||||
|
Assert.NotNull(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMaterial_NotFound_ReturnsNull()
|
||||||
|
{
|
||||||
|
var machine = CreateTestMachine();
|
||||||
|
var result = machine.GetMaterial("Titanium");
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
|
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Data\OpenNest.Data.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
public class PairOverlapDiagnosticTests
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
|
public PairOverlapDiagnosticTests(ITestOutputHelper output) => _output = output;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a 5x3.31 rectangle with rounded corners on the top-right and bottom-right
|
||||||
|
/// (radius 0.5), similar to "4526 A14 PT13".
|
||||||
|
/// </summary>
|
||||||
|
private static Drawing MakeRoundedRect(double w = 5.0, double h = 3.31, double r = 0.5)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
// Bottom edge
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w - r, 0)));
|
||||||
|
// Bottom-right rounded corner
|
||||||
|
pgm.Codes.Add(new ArcMove(new Vector(w, r), new Vector(w - r, r), RotationType.CW));
|
||||||
|
// Right edge
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, h - r)));
|
||||||
|
// Top-right rounded corner
|
||||||
|
pgm.Codes.Add(new ArcMove(new Vector(w - r, h), new Vector(w - r, h - r), RotationType.CW));
|
||||||
|
// Top edge
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||||
|
// Left edge back to start
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
return new Drawing("rounded-rect", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Drawing MakeSimpleRect(double w = 5.0, double h = 3.31)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
return new Drawing("rect", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)] // 0 degrees
|
||||||
|
[InlineData(90)] // 90 degrees
|
||||||
|
[InlineData(180)] // 180 degrees
|
||||||
|
[InlineData(270)] // 270 degrees
|
||||||
|
public void PartBoundary_HasEdgesAtAllRotations_RoundedRect(double angleDeg)
|
||||||
|
{
|
||||||
|
var drawing = MakeRoundedRect();
|
||||||
|
var part = new Part(drawing);
|
||||||
|
if (angleDeg != 0)
|
||||||
|
part.Rotate(Angle.ToRadians(angleDeg));
|
||||||
|
|
||||||
|
var boundary = new PartBoundary(part, 0.125);
|
||||||
|
|
||||||
|
var left = boundary.GetEdges(PushDirection.Left);
|
||||||
|
var right = boundary.GetEdges(PushDirection.Right);
|
||||||
|
var up = boundary.GetEdges(PushDirection.Up);
|
||||||
|
var down = boundary.GetEdges(PushDirection.Down);
|
||||||
|
|
||||||
|
_output.WriteLine($"Rotation: {angleDeg}°");
|
||||||
|
_output.WriteLine($" Left edges: {left.Length}");
|
||||||
|
_output.WriteLine($" Right edges: {right.Length}");
|
||||||
|
_output.WriteLine($" Up edges: {up.Length}");
|
||||||
|
_output.WriteLine($" Down edges: {down.Length}");
|
||||||
|
|
||||||
|
Assert.True(left.Length > 0, $"No left edges at {angleDeg}°");
|
||||||
|
Assert.True(right.Length > 0, $"No right edges at {angleDeg}°");
|
||||||
|
Assert.True(up.Length > 0, $"No up edges at {angleDeg}°");
|
||||||
|
Assert.True(down.Length > 0, $"No down edges at {angleDeg}°");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(90)]
|
||||||
|
[InlineData(180)]
|
||||||
|
[InlineData(270)]
|
||||||
|
public void PartBoundary_HasEdgesAtAllRotations_SimpleRect(double angleDeg)
|
||||||
|
{
|
||||||
|
var drawing = MakeSimpleRect();
|
||||||
|
var part = new Part(drawing);
|
||||||
|
if (angleDeg != 0)
|
||||||
|
part.Rotate(Angle.ToRadians(angleDeg));
|
||||||
|
|
||||||
|
var boundary = new PartBoundary(part, 0.125);
|
||||||
|
|
||||||
|
var left = boundary.GetEdges(PushDirection.Left);
|
||||||
|
var right = boundary.GetEdges(PushDirection.Right);
|
||||||
|
var up = boundary.GetEdges(PushDirection.Up);
|
||||||
|
var down = boundary.GetEdges(PushDirection.Down);
|
||||||
|
|
||||||
|
_output.WriteLine($"Rotation: {angleDeg}°");
|
||||||
|
_output.WriteLine($" Left edges: {left.Length}");
|
||||||
|
_output.WriteLine($" Right edges: {right.Length}");
|
||||||
|
_output.WriteLine($" Up edges: {up.Length}");
|
||||||
|
_output.WriteLine($" Down edges: {down.Length}");
|
||||||
|
|
||||||
|
Assert.True(left.Length > 0, $"No left edges at {angleDeg}°");
|
||||||
|
Assert.True(right.Length > 0, $"No right edges at {angleDeg}°");
|
||||||
|
Assert.True(up.Length > 0, $"No up edges at {angleDeg}°");
|
||||||
|
Assert.True(down.Length > 0, $"No down edges at {angleDeg}°");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)] // simple rect
|
||||||
|
[InlineData(true)] // rounded rect
|
||||||
|
public void FillExtents_NoPairOverlap_At90Degrees(bool rounded)
|
||||||
|
{
|
||||||
|
var drawing = rounded ? MakeRoundedRect() : MakeSimpleRect();
|
||||||
|
var workArea = new Box(0, 0, 20, 20);
|
||||||
|
var partSpacing = 0.25;
|
||||||
|
|
||||||
|
var filler = new FillExtents(workArea, partSpacing);
|
||||||
|
var parts = filler.Fill(drawing, Angle.ToRadians(90));
|
||||||
|
|
||||||
|
_output.WriteLine($"Shape: {(rounded ? "rounded rect" : "simple rect")}");
|
||||||
|
_output.WriteLine($"Parts: {parts.Count}");
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var p = parts[i];
|
||||||
|
_output.WriteLine($" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° " +
|
||||||
|
$"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for overlapping bounding boxes
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var b1 = parts[i].BoundingBox;
|
||||||
|
for (var j = i + 1; j < parts.Count; j++)
|
||||||
|
{
|
||||||
|
var b2 = parts[j].BoundingBox;
|
||||||
|
var overlapX = System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left);
|
||||||
|
var overlapY = System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom);
|
||||||
|
|
||||||
|
if (overlapX > 0.01 && overlapY > 0.01)
|
||||||
|
_output.WriteLine($" OVERLAP: [{i}] and [{j}] overlap by ({overlapX:F3}, {overlapY:F3})");
|
||||||
|
|
||||||
|
Assert.False(overlapX > 0.01 && overlapY > 0.01,
|
||||||
|
$"Parts [{i}] and [{j}] have overlapping bounding boxes " +
|
||||||
|
$"({overlapX:F3} x {overlapY:F3})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void FillLinear_PairPattern_NoPairOverlap_At90Degrees(bool rounded)
|
||||||
|
{
|
||||||
|
var drawing = rounded ? MakeRoundedRect() : MakeSimpleRect();
|
||||||
|
var workArea = new Box(0, 0, 20, 20);
|
||||||
|
var partSpacing = 0.25;
|
||||||
|
|
||||||
|
// Build a pair at 90°/270°
|
||||||
|
var part1 = Part.CreateAtOrigin(drawing, Angle.ToRadians(90));
|
||||||
|
var part2 = Part.CreateAtOrigin(drawing, Angle.ToRadians(270));
|
||||||
|
|
||||||
|
// Slide part2 right of part1
|
||||||
|
var offset = part1.BoundingBox.Width + part2.BoundingBox.Width + partSpacing;
|
||||||
|
part2.Offset(offset, 0);
|
||||||
|
part2.UpdateBounds();
|
||||||
|
|
||||||
|
// Slide part2 left toward part1 using geometry
|
||||||
|
var b1 = new PartBoundary(part1, partSpacing / 2);
|
||||||
|
var b2 = new PartBoundary(part2, partSpacing / 2);
|
||||||
|
|
||||||
|
_output.WriteLine($"Part1 (90°) boundary edges: L={b1.GetEdges(PushDirection.Left).Length} R={b1.GetEdges(PushDirection.Right).Length}");
|
||||||
|
_output.WriteLine($"Part2 (270°) boundary edges: L={b2.GetEdges(PushDirection.Left).Length} R={b2.GetEdges(PushDirection.Right).Length}");
|
||||||
|
|
||||||
|
var movingLines = b2.GetLines(part2.Location, PushDirection.Left);
|
||||||
|
var stationaryLines = b1.GetLines(part1.Location, PushDirection.Right);
|
||||||
|
|
||||||
|
_output.WriteLine($"Part1 loc: ({part1.Location.X:F4},{part1.Location.Y:F4})");
|
||||||
|
_output.WriteLine($"Part2 loc: ({part2.Location.X:F4},{part2.Location.Y:F4})");
|
||||||
|
|
||||||
|
_output.WriteLine($"Moving lines (part2 left): {movingLines.Count}");
|
||||||
|
foreach (var l in movingLines)
|
||||||
|
_output.WriteLine($" ({l.pt1.X:F4},{l.pt1.Y:F4})->({l.pt2.X:F4},{l.pt2.Y:F4})");
|
||||||
|
|
||||||
|
_output.WriteLine($"Stationary lines (part1 right): {stationaryLines.Count}");
|
||||||
|
foreach (var l in stationaryLines)
|
||||||
|
_output.WriteLine($" ({l.pt1.X:F4},{l.pt1.Y:F4})->({l.pt2.X:F4},{l.pt2.Y:F4})");
|
||||||
|
|
||||||
|
var slideDist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left);
|
||||||
|
_output.WriteLine($"Slide distance: {slideDist:F4}");
|
||||||
|
|
||||||
|
if (slideDist < double.MaxValue && slideDist > 0)
|
||||||
|
{
|
||||||
|
part2.Offset(-slideDist, 0);
|
||||||
|
part2.UpdateBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
_output.WriteLine($"Part1 bbox: ({part1.BoundingBox.Left:F2},{part1.BoundingBox.Bottom:F2})-({part1.BoundingBox.Right:F2},{part1.BoundingBox.Top:F2})");
|
||||||
|
_output.WriteLine($"Part2 bbox: ({part2.BoundingBox.Left:F2},{part2.BoundingBox.Bottom:F2})-({part2.BoundingBox.Right:F2},{part2.BoundingBox.Top:F2})");
|
||||||
|
|
||||||
|
// Now tile this pair pattern
|
||||||
|
var pattern = new Pattern();
|
||||||
|
pattern.Parts.Add(part1);
|
||||||
|
pattern.Parts.Add(part2);
|
||||||
|
pattern.UpdateBounds();
|
||||||
|
|
||||||
|
_output.WriteLine($"Pattern bbox width: {pattern.BoundingBox.Width:F2}");
|
||||||
|
|
||||||
|
var engine = new FillLinear(workArea, partSpacing);
|
||||||
|
var parts = engine.Fill(pattern, NestDirection.Horizontal);
|
||||||
|
|
||||||
|
_output.WriteLine($"Total parts: {parts.Count}");
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var p = parts[i];
|
||||||
|
_output.WriteLine($" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° " +
|
||||||
|
$"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for overlaps
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var bi = parts[i].BoundingBox;
|
||||||
|
for (var j = i + 1; j < parts.Count; j++)
|
||||||
|
{
|
||||||
|
var bj = parts[j].BoundingBox;
|
||||||
|
var ox = System.Math.Min(bi.Right, bj.Right) - System.Math.Max(bi.Left, bj.Left);
|
||||||
|
var oy = System.Math.Min(bi.Top, bj.Top) - System.Math.Max(bi.Bottom, bj.Bottom);
|
||||||
|
|
||||||
|
Assert.False(ox > 0.01 && oy > 0.01,
|
||||||
|
$"Parts [{i}] and [{j}] overlap ({ox:F3} x {oy:F3})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,7 +151,8 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
var drawing = new Drawing(Path.GetFileName(file));
|
var drawing = new Drawing(Path.GetFileName(file));
|
||||||
drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(entities);
|
var normalized = ShapeProfile.NormalizeEntities(entities);
|
||||||
|
drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(normalized);
|
||||||
drawing.UpdateArea();
|
drawing.UpdateArea();
|
||||||
drawing.Color = PartColors[colorIndex % PartColors.Length];
|
drawing.Color = PartColors[colorIndex % PartColors.Length];
|
||||||
colorIndex++;
|
colorIndex++;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PostProcessors", "PostProce
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Posts.Cincinnati", "OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj", "{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Posts.Cincinnati", "OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj", "{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Data", "OpenNest.Data\OpenNest.Data.csproj", "{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -172,6 +174,18 @@ Global
|
|||||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x64.Build.0 = Release|Any CPU
|
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x86.ActiveCfg = Release|Any CPU
|
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x86.Build.0 = Release|Any CPU
|
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -112,8 +112,6 @@ namespace OpenNest.Controls
|
|||||||
DrawEntity(e.Graphics, entity, pen);
|
DrawEntity(e.Graphics, entity, pen);
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawEtchMarks(e.Graphics);
|
|
||||||
|
|
||||||
if (SimplifierPreview != null)
|
if (SimplifierPreview != null)
|
||||||
{
|
{
|
||||||
// Draw tolerance zone (offset lines each side of original geometry)
|
// Draw tolerance zone (offset lines each side of original geometry)
|
||||||
@@ -240,46 +238,6 @@ namespace OpenNest.Controls
|
|||||||
private static bool IsEtchLayer(Layer layer) =>
|
private static bool IsEtchLayer(Layer layer) =>
|
||||||
string.Equals(layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase);
|
string.Equals(layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private void DrawEtchMarks(Graphics g)
|
|
||||||
{
|
|
||||||
if (Bends == null || Bends.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
using var etchPen = new Pen(Color.Green, 1.5f);
|
|
||||||
var etchLength = 1.0;
|
|
||||||
|
|
||||||
foreach (var bend in Bends)
|
|
||||||
{
|
|
||||||
if (bend.Direction != BendDirection.Up)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var start = bend.StartPoint;
|
|
||||||
var end = bend.EndPoint;
|
|
||||||
var length = bend.Length;
|
|
||||||
|
|
||||||
if (length < etchLength * 3.0)
|
|
||||||
{
|
|
||||||
var pt1 = PointWorldToGraph(start);
|
|
||||||
var pt2 = PointWorldToGraph(end);
|
|
||||||
g.DrawLine(etchPen, pt1, pt2);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var angle = start.AngleTo(end);
|
|
||||||
var dx = System.Math.Cos(angle) * etchLength;
|
|
||||||
var dy = System.Math.Sin(angle) * etchLength;
|
|
||||||
|
|
||||||
var s1 = PointWorldToGraph(start);
|
|
||||||
var e1 = PointWorldToGraph(new Vector(start.X + dx, start.Y + dy));
|
|
||||||
g.DrawLine(etchPen, s1, e1);
|
|
||||||
|
|
||||||
var s2 = PointWorldToGraph(end);
|
|
||||||
var e2 = PointWorldToGraph(new Vector(end.X - dx, end.Y - dy));
|
|
||||||
g.DrawLine(etchPen, s2, e2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawBendLines(Graphics g)
|
private void DrawBendLines(Graphics g)
|
||||||
{
|
{
|
||||||
if (Bends == null || Bends.Count == 0)
|
if (Bends == null || Bends.Count == 0)
|
||||||
|
|||||||
@@ -584,6 +584,7 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
part.Draw(g, (i + 1).ToString());
|
part.Draw(g, (i + 1).ToString());
|
||||||
DrawBendLines(g, part.BasePart);
|
DrawBendLines(g, part.BasePart);
|
||||||
|
DrawEtchMarks(g, part.BasePart);
|
||||||
DrawGrainWarning(g, part.BasePart);
|
DrawGrainWarning(g, part.BasePart);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,6 +658,58 @@ namespace OpenNest.Controls
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawEtchMarks(Graphics g, Part part)
|
||||||
|
{
|
||||||
|
if (!ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var etchPen = new Pen(Color.Green, 1.5f);
|
||||||
|
var etchLength = 1.0;
|
||||||
|
|
||||||
|
foreach (var bend in part.BaseDrawing.Bends)
|
||||||
|
{
|
||||||
|
if (bend.Direction != BendDirection.Up)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var start = bend.StartPoint;
|
||||||
|
var end = bend.EndPoint;
|
||||||
|
|
||||||
|
// Apply part rotation
|
||||||
|
if (part.Rotation != 0)
|
||||||
|
{
|
||||||
|
start = start.Rotate(part.Rotation);
|
||||||
|
end = end.Rotate(part.Rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply part offset
|
||||||
|
start = start + part.Location;
|
||||||
|
end = end + part.Location;
|
||||||
|
|
||||||
|
var length = bend.Length;
|
||||||
|
var angle = bend.StartPoint.AngleTo(bend.EndPoint) + part.Rotation;
|
||||||
|
|
||||||
|
if (length < etchLength * 3.0)
|
||||||
|
{
|
||||||
|
var pt1 = PointWorldToGraph(start);
|
||||||
|
var pt2 = PointWorldToGraph(end);
|
||||||
|
g.DrawLine(etchPen, pt1, pt2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var dx = System.Math.Cos(angle) * etchLength;
|
||||||
|
var dy = System.Math.Sin(angle) * etchLength;
|
||||||
|
|
||||||
|
var s1 = PointWorldToGraph(start);
|
||||||
|
var e1 = PointWorldToGraph(new Vector(start.X + dx, start.Y + dy));
|
||||||
|
g.DrawLine(etchPen, s1, e1);
|
||||||
|
|
||||||
|
var s2 = PointWorldToGraph(end);
|
||||||
|
var e2 = PointWorldToGraph(new Vector(end.X - dx, end.Y - dy));
|
||||||
|
g.DrawLine(etchPen, s2, e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawGrainWarning(Graphics g, Part part)
|
private void DrawGrainWarning(Graphics g, Part part)
|
||||||
{
|
{
|
||||||
if (!ShowBendLines || Plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
|
if (!ShowBendLines || Plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
|
||||||
|
|||||||
Generated
+400
@@ -0,0 +1,400 @@
|
|||||||
|
namespace OpenNest.Forms
|
||||||
|
{
|
||||||
|
partial class BomImportForm
|
||||||
|
{
|
||||||
|
private System.ComponentModel.IContainer components = null;
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing && (components != null))
|
||||||
|
components.Dispose();
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Windows Form Designer generated code
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
grpInput = new System.Windows.Forms.GroupBox();
|
||||||
|
tbl = new System.Windows.Forms.TableLayoutPanel();
|
||||||
|
lblJobName = new System.Windows.Forms.Label();
|
||||||
|
txtJobName = new System.Windows.Forms.TextBox();
|
||||||
|
lblBomFile = new System.Windows.Forms.Label();
|
||||||
|
txtBomFile = new System.Windows.Forms.TextBox();
|
||||||
|
btnBrowseBom = new System.Windows.Forms.Button();
|
||||||
|
lblDxfFolder = new System.Windows.Forms.Label();
|
||||||
|
txtDxfFolder = new System.Windows.Forms.TextBox();
|
||||||
|
btnBrowseDxf = new System.Windows.Forms.Button();
|
||||||
|
lblPlateSize = new System.Windows.Forms.Label();
|
||||||
|
platePanel = new System.Windows.Forms.FlowLayoutPanel();
|
||||||
|
txtPlateWidth = new System.Windows.Forms.TextBox();
|
||||||
|
lblPlateX = new System.Windows.Forms.Label();
|
||||||
|
txtPlateLength = new System.Windows.Forms.TextBox();
|
||||||
|
btnAnalyze = new System.Windows.Forms.Button();
|
||||||
|
tabControl = new System.Windows.Forms.TabControl();
|
||||||
|
tabParts = new System.Windows.Forms.TabPage();
|
||||||
|
dgvParts = new System.Windows.Forms.DataGridView();
|
||||||
|
tabGroups = new System.Windows.Forms.TabPage();
|
||||||
|
dgvGroups = new System.Windows.Forms.DataGridView();
|
||||||
|
pnlBottom = new System.Windows.Forms.Panel();
|
||||||
|
lblSummary = new System.Windows.Forms.Label();
|
||||||
|
btnCreateNests = new System.Windows.Forms.Button();
|
||||||
|
btnClose = new System.Windows.Forms.Button();
|
||||||
|
grpInput.SuspendLayout();
|
||||||
|
tbl.SuspendLayout();
|
||||||
|
platePanel.SuspendLayout();
|
||||||
|
tabControl.SuspendLayout();
|
||||||
|
tabParts.SuspendLayout();
|
||||||
|
((System.ComponentModel.ISupportInitialize)dgvParts).BeginInit();
|
||||||
|
tabGroups.SuspendLayout();
|
||||||
|
((System.ComponentModel.ISupportInitialize)dgvGroups).BeginInit();
|
||||||
|
pnlBottom.SuspendLayout();
|
||||||
|
SuspendLayout();
|
||||||
|
//
|
||||||
|
// grpInput
|
||||||
|
//
|
||||||
|
grpInput.Controls.Add(tbl);
|
||||||
|
grpInput.Dock = System.Windows.Forms.DockStyle.Top;
|
||||||
|
grpInput.Location = new System.Drawing.Point(0, 0);
|
||||||
|
grpInput.Name = "grpInput";
|
||||||
|
grpInput.Padding = new System.Windows.Forms.Padding(6);
|
||||||
|
grpInput.Size = new System.Drawing.Size(804, 200);
|
||||||
|
grpInput.TabIndex = 0;
|
||||||
|
grpInput.TabStop = false;
|
||||||
|
grpInput.Text = "Input";
|
||||||
|
//
|
||||||
|
// tbl
|
||||||
|
//
|
||||||
|
tbl.ColumnCount = 3;
|
||||||
|
tbl.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||||
|
tbl.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
tbl.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||||
|
tbl.Controls.Add(lblJobName, 0, 0);
|
||||||
|
tbl.Controls.Add(txtJobName, 1, 0);
|
||||||
|
tbl.Controls.Add(lblBomFile, 0, 1);
|
||||||
|
tbl.Controls.Add(txtBomFile, 1, 1);
|
||||||
|
tbl.Controls.Add(btnBrowseBom, 2, 1);
|
||||||
|
tbl.Controls.Add(lblDxfFolder, 0, 2);
|
||||||
|
tbl.Controls.Add(txtDxfFolder, 1, 2);
|
||||||
|
tbl.Controls.Add(btnBrowseDxf, 2, 2);
|
||||||
|
tbl.Controls.Add(lblPlateSize, 0, 3);
|
||||||
|
tbl.Controls.Add(platePanel, 1, 3);
|
||||||
|
tbl.Controls.Add(btnAnalyze, 1, 4);
|
||||||
|
tbl.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
tbl.Location = new System.Drawing.Point(6, 22);
|
||||||
|
tbl.Name = "tbl";
|
||||||
|
tbl.Padding = new System.Windows.Forms.Padding(3);
|
||||||
|
tbl.RowCount = 5;
|
||||||
|
tbl.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tbl.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tbl.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tbl.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tbl.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tbl.Size = new System.Drawing.Size(792, 172);
|
||||||
|
tbl.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// lblJobName
|
||||||
|
//
|
||||||
|
lblJobName.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||||
|
lblJobName.AutoSize = true;
|
||||||
|
lblJobName.Location = new System.Drawing.Point(6, 13);
|
||||||
|
lblJobName.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3);
|
||||||
|
lblJobName.Name = "lblJobName";
|
||||||
|
lblJobName.Size = new System.Drawing.Size(63, 15);
|
||||||
|
lblJobName.TabIndex = 0;
|
||||||
|
lblJobName.Text = "Job Name:";
|
||||||
|
//
|
||||||
|
// txtJobName
|
||||||
|
//
|
||||||
|
tbl.SetColumnSpan(txtJobName, 2);
|
||||||
|
txtJobName.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
txtJobName.Location = new System.Drawing.Point(79, 9);
|
||||||
|
txtJobName.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3);
|
||||||
|
txtJobName.Name = "txtJobName";
|
||||||
|
txtJobName.Size = new System.Drawing.Size(707, 23);
|
||||||
|
txtJobName.TabIndex = 1;
|
||||||
|
//
|
||||||
|
// lblBomFile
|
||||||
|
//
|
||||||
|
lblBomFile.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||||
|
lblBomFile.AutoSize = true;
|
||||||
|
lblBomFile.Location = new System.Drawing.Point(6, 45);
|
||||||
|
lblBomFile.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3);
|
||||||
|
lblBomFile.Name = "lblBomFile";
|
||||||
|
lblBomFile.Size = new System.Drawing.Size(58, 15);
|
||||||
|
lblBomFile.TabIndex = 2;
|
||||||
|
lblBomFile.Text = "BOM File:";
|
||||||
|
//
|
||||||
|
// txtBomFile
|
||||||
|
//
|
||||||
|
txtBomFile.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
txtBomFile.Location = new System.Drawing.Point(79, 41);
|
||||||
|
txtBomFile.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3);
|
||||||
|
txtBomFile.Name = "txtBomFile";
|
||||||
|
txtBomFile.ReadOnly = true;
|
||||||
|
txtBomFile.Size = new System.Drawing.Size(669, 23);
|
||||||
|
txtBomFile.TabIndex = 3;
|
||||||
|
//
|
||||||
|
// btnBrowseBom
|
||||||
|
//
|
||||||
|
btnBrowseBom.Location = new System.Drawing.Point(751, 40);
|
||||||
|
btnBrowseBom.Margin = new System.Windows.Forms.Padding(0, 5, 3, 3);
|
||||||
|
btnBrowseBom.Name = "btnBrowseBom";
|
||||||
|
btnBrowseBom.Size = new System.Drawing.Size(35, 25);
|
||||||
|
btnBrowseBom.TabIndex = 4;
|
||||||
|
btnBrowseBom.Text = "...";
|
||||||
|
btnBrowseBom.Click += BrowseBom_Click;
|
||||||
|
//
|
||||||
|
// lblDxfFolder
|
||||||
|
//
|
||||||
|
lblDxfFolder.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||||
|
lblDxfFolder.AutoSize = true;
|
||||||
|
lblDxfFolder.Location = new System.Drawing.Point(6, 78);
|
||||||
|
lblDxfFolder.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3);
|
||||||
|
lblDxfFolder.Name = "lblDxfFolder";
|
||||||
|
lblDxfFolder.Size = new System.Drawing.Size(67, 15);
|
||||||
|
lblDxfFolder.TabIndex = 5;
|
||||||
|
lblDxfFolder.Text = "DXF Folder:";
|
||||||
|
//
|
||||||
|
// txtDxfFolder
|
||||||
|
//
|
||||||
|
txtDxfFolder.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
txtDxfFolder.Location = new System.Drawing.Point(79, 74);
|
||||||
|
txtDxfFolder.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3);
|
||||||
|
txtDxfFolder.Name = "txtDxfFolder";
|
||||||
|
txtDxfFolder.ReadOnly = true;
|
||||||
|
txtDxfFolder.Size = new System.Drawing.Size(669, 23);
|
||||||
|
txtDxfFolder.TabIndex = 6;
|
||||||
|
//
|
||||||
|
// btnBrowseDxf
|
||||||
|
//
|
||||||
|
btnBrowseDxf.Location = new System.Drawing.Point(751, 73);
|
||||||
|
btnBrowseDxf.Margin = new System.Windows.Forms.Padding(0, 5, 3, 3);
|
||||||
|
btnBrowseDxf.Name = "btnBrowseDxf";
|
||||||
|
btnBrowseDxf.Size = new System.Drawing.Size(35, 25);
|
||||||
|
btnBrowseDxf.TabIndex = 7;
|
||||||
|
btnBrowseDxf.Text = "...";
|
||||||
|
btnBrowseDxf.Click += BrowseDxf_Click;
|
||||||
|
//
|
||||||
|
// lblPlateSize
|
||||||
|
//
|
||||||
|
lblPlateSize.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||||
|
lblPlateSize.AutoSize = true;
|
||||||
|
lblPlateSize.Location = new System.Drawing.Point(6, 112);
|
||||||
|
lblPlateSize.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3);
|
||||||
|
lblPlateSize.Name = "lblPlateSize";
|
||||||
|
lblPlateSize.Size = new System.Drawing.Size(59, 15);
|
||||||
|
lblPlateSize.TabIndex = 8;
|
||||||
|
lblPlateSize.Text = "Plate Size:";
|
||||||
|
//
|
||||||
|
// platePanel
|
||||||
|
//
|
||||||
|
platePanel.AutoSize = true;
|
||||||
|
platePanel.Controls.Add(txtPlateWidth);
|
||||||
|
platePanel.Controls.Add(lblPlateX);
|
||||||
|
platePanel.Controls.Add(txtPlateLength);
|
||||||
|
platePanel.Location = new System.Drawing.Point(76, 104);
|
||||||
|
platePanel.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3);
|
||||||
|
platePanel.Name = "platePanel";
|
||||||
|
platePanel.Size = new System.Drawing.Size(156, 29);
|
||||||
|
platePanel.TabIndex = 9;
|
||||||
|
platePanel.WrapContents = false;
|
||||||
|
//
|
||||||
|
// txtPlateWidth
|
||||||
|
//
|
||||||
|
txtPlateWidth.Location = new System.Drawing.Point(3, 3);
|
||||||
|
txtPlateWidth.Name = "txtPlateWidth";
|
||||||
|
txtPlateWidth.Size = new System.Drawing.Size(60, 23);
|
||||||
|
txtPlateWidth.TabIndex = 0;
|
||||||
|
txtPlateWidth.Text = "60";
|
||||||
|
//
|
||||||
|
// lblPlateX
|
||||||
|
//
|
||||||
|
lblPlateX.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||||
|
lblPlateX.AutoSize = true;
|
||||||
|
lblPlateX.Location = new System.Drawing.Point(69, 7);
|
||||||
|
lblPlateX.Name = "lblPlateX";
|
||||||
|
lblPlateX.Size = new System.Drawing.Size(18, 15);
|
||||||
|
lblPlateX.TabIndex = 1;
|
||||||
|
lblPlateX.Text = " x ";
|
||||||
|
//
|
||||||
|
// txtPlateLength
|
||||||
|
//
|
||||||
|
txtPlateLength.Location = new System.Drawing.Point(93, 3);
|
||||||
|
txtPlateLength.Name = "txtPlateLength";
|
||||||
|
txtPlateLength.Size = new System.Drawing.Size(60, 23);
|
||||||
|
txtPlateLength.TabIndex = 2;
|
||||||
|
txtPlateLength.Text = "120";
|
||||||
|
//
|
||||||
|
// btnAnalyze
|
||||||
|
//
|
||||||
|
btnAnalyze.Anchor = System.Windows.Forms.AnchorStyles.Right;
|
||||||
|
tbl.SetColumnSpan(btnAnalyze, 2);
|
||||||
|
btnAnalyze.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
|
||||||
|
btnAnalyze.Location = new System.Drawing.Point(676, 142);
|
||||||
|
btnAnalyze.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3);
|
||||||
|
btnAnalyze.Name = "btnAnalyze";
|
||||||
|
btnAnalyze.Size = new System.Drawing.Size(110, 30);
|
||||||
|
btnAnalyze.TabIndex = 10;
|
||||||
|
btnAnalyze.Text = "Analyze";
|
||||||
|
btnAnalyze.Click += Analyze_Click;
|
||||||
|
//
|
||||||
|
// tabControl
|
||||||
|
//
|
||||||
|
tabControl.Controls.Add(tabParts);
|
||||||
|
tabControl.Controls.Add(tabGroups);
|
||||||
|
tabControl.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
tabControl.Location = new System.Drawing.Point(0, 200);
|
||||||
|
tabControl.Name = "tabControl";
|
||||||
|
tabControl.SelectedIndex = 0;
|
||||||
|
tabControl.Size = new System.Drawing.Size(804, 365);
|
||||||
|
tabControl.TabIndex = 1;
|
||||||
|
//
|
||||||
|
// tabParts
|
||||||
|
//
|
||||||
|
tabParts.Controls.Add(dgvParts);
|
||||||
|
tabParts.Name = "tabParts";
|
||||||
|
tabParts.Padding = new System.Windows.Forms.Padding(3);
|
||||||
|
tabParts.Text = "Parts";
|
||||||
|
//
|
||||||
|
// dgvParts
|
||||||
|
//
|
||||||
|
dgvParts.AllowUserToAddRows = false;
|
||||||
|
dgvParts.AllowUserToDeleteRows = false;
|
||||||
|
dgvParts.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
|
||||||
|
dgvParts.BackgroundColor = System.Drawing.SystemColors.Window;
|
||||||
|
dgvParts.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||||
|
dgvParts.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||||
|
dgvParts.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
dgvParts.Name = "dgvParts";
|
||||||
|
dgvParts.RowHeadersVisible = false;
|
||||||
|
dgvParts.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
|
||||||
|
dgvParts.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// tabGroups
|
||||||
|
//
|
||||||
|
tabGroups.Controls.Add(dgvGroups);
|
||||||
|
tabGroups.Name = "tabGroups";
|
||||||
|
tabGroups.Padding = new System.Windows.Forms.Padding(3);
|
||||||
|
tabGroups.Text = "Groups";
|
||||||
|
//
|
||||||
|
// dgvGroups
|
||||||
|
//
|
||||||
|
dgvGroups.AllowUserToAddRows = false;
|
||||||
|
dgvGroups.AllowUserToDeleteRows = false;
|
||||||
|
dgvGroups.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
|
||||||
|
dgvGroups.BackgroundColor = System.Drawing.SystemColors.Window;
|
||||||
|
dgvGroups.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||||
|
dgvGroups.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||||
|
dgvGroups.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
dgvGroups.Name = "dgvGroups";
|
||||||
|
dgvGroups.RowHeadersVisible = false;
|
||||||
|
dgvGroups.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
|
||||||
|
dgvGroups.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// pnlBottom
|
||||||
|
//
|
||||||
|
pnlBottom.Controls.Add(lblSummary);
|
||||||
|
pnlBottom.Controls.Add(btnCreateNests);
|
||||||
|
pnlBottom.Controls.Add(btnClose);
|
||||||
|
pnlBottom.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||||
|
pnlBottom.Location = new System.Drawing.Point(0, 565);
|
||||||
|
pnlBottom.Name = "pnlBottom";
|
||||||
|
pnlBottom.Padding = new System.Windows.Forms.Padding(10);
|
||||||
|
pnlBottom.Size = new System.Drawing.Size(804, 50);
|
||||||
|
pnlBottom.TabIndex = 2;
|
||||||
|
//
|
||||||
|
// lblSummary
|
||||||
|
//
|
||||||
|
lblSummary.AutoSize = true;
|
||||||
|
lblSummary.Dock = System.Windows.Forms.DockStyle.Left;
|
||||||
|
lblSummary.ForeColor = System.Drawing.Color.Gray;
|
||||||
|
lblSummary.Location = new System.Drawing.Point(10, 10);
|
||||||
|
lblSummary.Name = "lblSummary";
|
||||||
|
lblSummary.Size = new System.Drawing.Size(0, 15);
|
||||||
|
lblSummary.TabIndex = 0;
|
||||||
|
lblSummary.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||||
|
//
|
||||||
|
// btnCreateNests
|
||||||
|
//
|
||||||
|
btnCreateNests.Dock = System.Windows.Forms.DockStyle.Right;
|
||||||
|
btnCreateNests.Enabled = false;
|
||||||
|
btnCreateNests.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
|
||||||
|
btnCreateNests.Location = new System.Drawing.Point(604, 10);
|
||||||
|
btnCreateNests.Margin = new System.Windows.Forms.Padding(0, 0, 6, 0);
|
||||||
|
btnCreateNests.Name = "btnCreateNests";
|
||||||
|
btnCreateNests.Size = new System.Drawing.Size(110, 30);
|
||||||
|
btnCreateNests.TabIndex = 1;
|
||||||
|
btnCreateNests.Text = "Create Nests";
|
||||||
|
btnCreateNests.Click += CreateNests_Click;
|
||||||
|
//
|
||||||
|
// btnClose
|
||||||
|
//
|
||||||
|
btnClose.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||||
|
btnClose.Dock = System.Windows.Forms.DockStyle.Right;
|
||||||
|
btnClose.Location = new System.Drawing.Point(714, 10);
|
||||||
|
btnClose.Name = "btnClose";
|
||||||
|
btnClose.Size = new System.Drawing.Size(80, 30);
|
||||||
|
btnClose.TabIndex = 2;
|
||||||
|
btnClose.Text = "Close";
|
||||||
|
btnClose.Click += BtnClose_Click;
|
||||||
|
//
|
||||||
|
// BomImportForm
|
||||||
|
//
|
||||||
|
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
|
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
|
CancelButton = btnClose;
|
||||||
|
ClientSize = new System.Drawing.Size(804, 615);
|
||||||
|
Controls.Add(tabControl);
|
||||||
|
Controls.Add(pnlBottom);
|
||||||
|
Controls.Add(grpInput);
|
||||||
|
Font = new System.Drawing.Font("Segoe UI", 9F);
|
||||||
|
MaximizeBox = false;
|
||||||
|
MinimumSize = new System.Drawing.Size(400, 350);
|
||||||
|
Name = "BomImportForm";
|
||||||
|
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
|
Text = "Import BOM";
|
||||||
|
grpInput.ResumeLayout(false);
|
||||||
|
tbl.ResumeLayout(false);
|
||||||
|
tbl.PerformLayout();
|
||||||
|
platePanel.ResumeLayout(false);
|
||||||
|
platePanel.PerformLayout();
|
||||||
|
tabParts.ResumeLayout(false);
|
||||||
|
((System.ComponentModel.ISupportInitialize)dgvParts).EndInit();
|
||||||
|
tabGroups.ResumeLayout(false);
|
||||||
|
((System.ComponentModel.ISupportInitialize)dgvGroups).EndInit();
|
||||||
|
tabControl.ResumeLayout(false);
|
||||||
|
pnlBottom.ResumeLayout(false);
|
||||||
|
pnlBottom.PerformLayout();
|
||||||
|
ResumeLayout(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private System.Windows.Forms.GroupBox grpInput;
|
||||||
|
private System.Windows.Forms.TextBox txtJobName;
|
||||||
|
private System.Windows.Forms.TextBox txtBomFile;
|
||||||
|
private System.Windows.Forms.Button btnBrowseBom;
|
||||||
|
private System.Windows.Forms.TextBox txtDxfFolder;
|
||||||
|
private System.Windows.Forms.Button btnBrowseDxf;
|
||||||
|
private System.Windows.Forms.TextBox txtPlateWidth;
|
||||||
|
private System.Windows.Forms.TextBox txtPlateLength;
|
||||||
|
private System.Windows.Forms.Button btnAnalyze;
|
||||||
|
private System.Windows.Forms.TabControl tabControl;
|
||||||
|
private System.Windows.Forms.TabPage tabParts;
|
||||||
|
private System.Windows.Forms.DataGridView dgvParts;
|
||||||
|
private System.Windows.Forms.TabPage tabGroups;
|
||||||
|
private System.Windows.Forms.DataGridView dgvGroups;
|
||||||
|
private System.Windows.Forms.Panel pnlBottom;
|
||||||
|
private System.Windows.Forms.Label lblSummary;
|
||||||
|
private System.Windows.Forms.Button btnCreateNests;
|
||||||
|
private System.Windows.Forms.Button btnClose;
|
||||||
|
private System.Windows.Forms.TableLayoutPanel tbl;
|
||||||
|
private System.Windows.Forms.Label lblJobName;
|
||||||
|
private System.Windows.Forms.Label lblBomFile;
|
||||||
|
private System.Windows.Forms.Label lblDxfFolder;
|
||||||
|
private System.Windows.Forms.Label lblPlateSize;
|
||||||
|
private System.Windows.Forms.FlowLayoutPanel platePanel;
|
||||||
|
private System.Windows.Forms.Label lblPlateX;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
|
using OpenNest.IO.Bom;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest.Forms
|
||||||
|
{
|
||||||
|
public partial class BomImportForm : Form
|
||||||
|
{
|
||||||
|
private List<BomPartRow> _parts;
|
||||||
|
private Dictionary<string, (double Width, double Length)> _plateSizes;
|
||||||
|
private bool _suppressRegroup;
|
||||||
|
|
||||||
|
public Form MdiParentForm { get; set; }
|
||||||
|
|
||||||
|
public BomImportForm()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_parts = new List<BomPartRow>();
|
||||||
|
_plateSizes = new Dictionary<string, (double, double)>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region File Browsing
|
||||||
|
|
||||||
|
private void BrowseBom_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
using var dlg = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Title = "Select BOM File",
|
||||||
|
Filter = "Excel Files|*.xlsx|All Files|*.*",
|
||||||
|
FilterIndex = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dlg.ShowDialog(this) != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
txtBomFile.Text = dlg.FileName;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(txtDxfFolder.Text))
|
||||||
|
txtDxfFolder.Text = Path.GetDirectoryName(dlg.FileName);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(txtJobName.Text))
|
||||||
|
{
|
||||||
|
var name = Path.GetFileNameWithoutExtension(dlg.FileName);
|
||||||
|
if (name.EndsWith(" BOM", StringComparison.OrdinalIgnoreCase))
|
||||||
|
name = name.Substring(0, name.Length - 4).TrimEnd();
|
||||||
|
txtJobName.Text = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BrowseDxf_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
using var dlg = new FolderBrowserDialog
|
||||||
|
{
|
||||||
|
Description = "Select DXF Folder",
|
||||||
|
SelectedPath = txtDxfFolder.Text,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dlg.ShowDialog(this) != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
txtDxfFolder.Text = dlg.SelectedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Analyze
|
||||||
|
|
||||||
|
private void Analyze_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (!File.Exists(txtBomFile.Text))
|
||||||
|
{
|
||||||
|
MessageBox.Show("BOM file does not exist.", "Validation Error",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<BomItem> items;
|
||||||
|
using (var reader = new BomReader(txtBomFile.Text))
|
||||||
|
items = reader.GetItems();
|
||||||
|
|
||||||
|
var analysis = BomAnalyzer.Analyze(items, txtDxfFolder.Text);
|
||||||
|
BuildPartRows(items, analysis);
|
||||||
|
PopulatePartsGrid();
|
||||||
|
RebuildGroups();
|
||||||
|
UpdateSummary();
|
||||||
|
btnCreateNests.Enabled = true;
|
||||||
|
tabControl.SelectedTab = tabParts;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Error reading BOM: {ex.Message}", "Error",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildPartRows(List<BomItem> items, BomAnalysis analysis)
|
||||||
|
{
|
||||||
|
var matchedPaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var group in analysis.Groups)
|
||||||
|
foreach (var part in group.Parts)
|
||||||
|
if (part.DxfPath != null)
|
||||||
|
matchedPaths[part.Item.FileName ?? ""] = part.DxfPath;
|
||||||
|
|
||||||
|
_parts = new List<BomPartRow>();
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
var row = new BomPartRow
|
||||||
|
{
|
||||||
|
ItemNum = item.ItemNum,
|
||||||
|
FileName = item.FileName,
|
||||||
|
Qty = item.Qty,
|
||||||
|
Description = item.Description,
|
||||||
|
Material = item.Material,
|
||||||
|
Thickness = item.Thickness,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(item.FileName))
|
||||||
|
{
|
||||||
|
row.Status = "Skipped";
|
||||||
|
row.IsEditable = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var lookupName = item.FileName;
|
||||||
|
if (lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase))
|
||||||
|
lookupName = Path.GetFileNameWithoutExtension(lookupName);
|
||||||
|
|
||||||
|
if (matchedPaths.TryGetValue(lookupName, out var dxfPath))
|
||||||
|
{
|
||||||
|
row.DxfPath = dxfPath;
|
||||||
|
row.Status = "Matched";
|
||||||
|
row.IsEditable = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
row.Status = "No DXF";
|
||||||
|
row.IsEditable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parts.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
_plateSizes.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Parts Tab
|
||||||
|
|
||||||
|
private void PopulatePartsGrid()
|
||||||
|
{
|
||||||
|
_suppressRegroup = true;
|
||||||
|
|
||||||
|
var table = new DataTable();
|
||||||
|
table.Columns.Add("Item #", typeof(string));
|
||||||
|
table.Columns.Add("File Name", typeof(string));
|
||||||
|
table.Columns.Add("Qty", typeof(string));
|
||||||
|
table.Columns.Add("Description", typeof(string));
|
||||||
|
table.Columns.Add("Material", typeof(string));
|
||||||
|
table.Columns.Add("Thickness", typeof(string));
|
||||||
|
table.Columns.Add("Status", typeof(string));
|
||||||
|
|
||||||
|
foreach (var part in _parts)
|
||||||
|
{
|
||||||
|
table.Rows.Add(
|
||||||
|
part.ItemNum?.ToString() ?? "",
|
||||||
|
part.FileName ?? "",
|
||||||
|
part.Qty?.ToString() ?? "",
|
||||||
|
part.Description ?? "",
|
||||||
|
part.Material ?? "",
|
||||||
|
part.Thickness?.ToString("0.####") ?? "",
|
||||||
|
part.Status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dgvParts.DataSource = table;
|
||||||
|
|
||||||
|
// Make non-editable columns read-only
|
||||||
|
foreach (DataGridViewColumn col in dgvParts.Columns)
|
||||||
|
{
|
||||||
|
if (col.Name != "Material" && col.Name != "Thickness")
|
||||||
|
col.ReadOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style rows by status
|
||||||
|
for (var i = 0; i < _parts.Count; i++)
|
||||||
|
{
|
||||||
|
if (!_parts[i].IsEditable)
|
||||||
|
{
|
||||||
|
dgvParts.Rows[i].ReadOnly = true;
|
||||||
|
dgvParts.Rows[i].DefaultCellStyle.ForeColor = Color.Gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dgvParts.CellValueChanged -= DgvParts_CellValueChanged;
|
||||||
|
dgvParts.CellValueChanged += DgvParts_CellValueChanged;
|
||||||
|
|
||||||
|
_suppressRegroup = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DgvParts_CellValueChanged(object sender, DataGridViewCellEventArgs e)
|
||||||
|
{
|
||||||
|
if (_suppressRegroup || e.RowIndex < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var colName = dgvParts.Columns[e.ColumnIndex].Name;
|
||||||
|
if (colName != "Material" && colName != "Thickness")
|
||||||
|
return;
|
||||||
|
|
||||||
|
var part = _parts[e.RowIndex];
|
||||||
|
if (!part.IsEditable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (colName == "Material")
|
||||||
|
part.Material = dgvParts.Rows[e.RowIndex].Cells[e.ColumnIndex].Value?.ToString();
|
||||||
|
|
||||||
|
if (colName == "Thickness")
|
||||||
|
{
|
||||||
|
var text = dgvParts.Rows[e.RowIndex].Cells[e.ColumnIndex].Value?.ToString();
|
||||||
|
part.Thickness = double.TryParse(text, out var t) ? t : (double?)null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RebuildGroups();
|
||||||
|
UpdateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Groups Tab
|
||||||
|
|
||||||
|
private void RebuildGroups()
|
||||||
|
{
|
||||||
|
// Save existing plate sizes before rebuilding
|
||||||
|
SavePlateSizes();
|
||||||
|
|
||||||
|
var defaultWidth = double.TryParse(txtPlateWidth.Text, out var w) ? w : 60;
|
||||||
|
var defaultLength = double.TryParse(txtPlateLength.Text, out var l) ? l : 120;
|
||||||
|
|
||||||
|
var groups = _parts
|
||||||
|
.Where(p => p.IsEditable
|
||||||
|
&& !string.IsNullOrWhiteSpace(p.Material)
|
||||||
|
&& p.Thickness.HasValue)
|
||||||
|
.GroupBy(p => new
|
||||||
|
{
|
||||||
|
Material = p.Material.ToUpperInvariant(),
|
||||||
|
Thickness = p.Thickness.Value
|
||||||
|
})
|
||||||
|
.OrderBy(g => g.First().Material)
|
||||||
|
.ThenBy(g => g.Key.Thickness)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var table = new DataTable();
|
||||||
|
table.Columns.Add("Material", typeof(string));
|
||||||
|
table.Columns.Add("Thickness", typeof(double));
|
||||||
|
table.Columns.Add("Parts", typeof(int));
|
||||||
|
table.Columns.Add("Total Qty", typeof(int));
|
||||||
|
table.Columns.Add("Plate Width", typeof(double));
|
||||||
|
table.Columns.Add("Plate Length", typeof(double));
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
var material = group.First().Material;
|
||||||
|
var thickness = group.Key.Thickness;
|
||||||
|
var key = GroupKey(material, thickness);
|
||||||
|
|
||||||
|
var plateWidth = _plateSizes.TryGetValue(key, out var size) ? size.Width : defaultWidth;
|
||||||
|
var plateLength = _plateSizes.TryGetValue(key, out _) ? size.Length : defaultLength;
|
||||||
|
|
||||||
|
table.Rows.Add(
|
||||||
|
material,
|
||||||
|
thickness,
|
||||||
|
group.Count(),
|
||||||
|
group.Sum(p => p.Qty ?? 0),
|
||||||
|
plateWidth,
|
||||||
|
plateLength
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dgvGroups.DataSource = table;
|
||||||
|
|
||||||
|
// Material, Thickness, Parts, Total Qty are read-only
|
||||||
|
if (dgvGroups.Columns.Count >= 6)
|
||||||
|
{
|
||||||
|
dgvGroups.Columns["Material"].ReadOnly = true;
|
||||||
|
dgvGroups.Columns["Thickness"].ReadOnly = true;
|
||||||
|
dgvGroups.Columns["Parts"].ReadOnly = true;
|
||||||
|
dgvGroups.Columns["Total Qty"].ReadOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
btnCreateNests.Enabled = table.Rows.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SavePlateSizes()
|
||||||
|
{
|
||||||
|
if (dgvGroups.DataSource is not DataTable table)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_plateSizes.Clear();
|
||||||
|
foreach (DataRow row in table.Rows)
|
||||||
|
{
|
||||||
|
var material = row["Material"]?.ToString() ?? "";
|
||||||
|
var thickness = row["Thickness"] is double t ? t : 0;
|
||||||
|
var key = GroupKey(material, thickness);
|
||||||
|
|
||||||
|
var width = row["Plate Width"] is double pw ? pw : 60;
|
||||||
|
var length = row["Plate Length"] is double pl ? pl : 120;
|
||||||
|
|
||||||
|
_plateSizes[key] = (width, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GroupKey(string material, double thickness)
|
||||||
|
=> $"{material?.ToUpperInvariant()}|{thickness}";
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Summary
|
||||||
|
|
||||||
|
private void UpdateSummary()
|
||||||
|
{
|
||||||
|
var skipped = _parts.Count(p => p.Status == "Skipped");
|
||||||
|
var noDxf = _parts.Count(p => p.Status == "No DXF");
|
||||||
|
var matched = _parts.Count(p => p.Status == "Matched");
|
||||||
|
|
||||||
|
var summaryParts = new List<string>();
|
||||||
|
if (skipped > 0)
|
||||||
|
summaryParts.Add($"{skipped} skipped (no file name)");
|
||||||
|
if (noDxf > 0)
|
||||||
|
summaryParts.Add($"{noDxf} no DXF found");
|
||||||
|
|
||||||
|
lblSummary.Text = summaryParts.Count > 0
|
||||||
|
? string.Join(", ", summaryParts)
|
||||||
|
: $"{matched} parts matched";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Create Nests
|
||||||
|
|
||||||
|
private void CreateNests_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_parts == null || _parts.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Save latest plate size edits
|
||||||
|
SavePlateSizes();
|
||||||
|
|
||||||
|
var defaultWidth = double.TryParse(txtPlateWidth.Text, out var dw) ? dw : 60;
|
||||||
|
var defaultLength = double.TryParse(txtPlateLength.Text, out var dl) ? dl : 120;
|
||||||
|
|
||||||
|
var groups = _parts
|
||||||
|
.Where(p => p.IsEditable
|
||||||
|
&& !string.IsNullOrWhiteSpace(p.Material)
|
||||||
|
&& p.Thickness.HasValue
|
||||||
|
&& !string.IsNullOrWhiteSpace(p.DxfPath))
|
||||||
|
.GroupBy(p => new
|
||||||
|
{
|
||||||
|
Material = p.Material.ToUpperInvariant(),
|
||||||
|
Thickness = p.Thickness.Value
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (groups.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show("No groups with matched DXF files to create nests from.", "Nothing to Create",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobName = txtJobName.Text.Trim();
|
||||||
|
var importer = new DxfImporter();
|
||||||
|
var nestsCreated = 0;
|
||||||
|
var importErrors = new List<string>();
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
var material = group.First().Material;
|
||||||
|
var thickness = group.Key.Thickness;
|
||||||
|
var key = GroupKey(material, thickness);
|
||||||
|
|
||||||
|
var plateWidth = _plateSizes.TryGetValue(key, out var size) ? size.Width : defaultWidth;
|
||||||
|
var plateLength = _plateSizes.TryGetValue(key, out _) ? size.Length : defaultLength;
|
||||||
|
|
||||||
|
var nestName = $"{jobName} - {thickness:0.###} {material}";
|
||||||
|
var nest = new Nest(nestName);
|
||||||
|
nest.DateCreated = DateTime.Now;
|
||||||
|
nest.DateLastModified = DateTime.Now;
|
||||||
|
nest.PlateDefaults.Size = new Geometry.Size(plateWidth, plateLength);
|
||||||
|
nest.PlateDefaults.Thickness = thickness;
|
||||||
|
nest.PlateDefaults.Material = new Material(material);
|
||||||
|
nest.PlateDefaults.Quadrant = 1;
|
||||||
|
nest.PlateDefaults.PartSpacing = 1;
|
||||||
|
nest.PlateDefaults.EdgeSpacing = new Spacing(1, 1, 1, 1);
|
||||||
|
|
||||||
|
foreach (var part in group)
|
||||||
|
{
|
||||||
|
if (!File.Exists(part.DxfPath))
|
||||||
|
{
|
||||||
|
importErrors.Add($"{part.FileName}: DXF file not found");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = importer.Import(part.DxfPath);
|
||||||
|
|
||||||
|
var drawingName = Path.GetFileNameWithoutExtension(part.DxfPath);
|
||||||
|
var drawing = new Drawing(drawingName);
|
||||||
|
drawing.Source.Path = part.DxfPath;
|
||||||
|
drawing.Quantity.Required = part.Qty ?? 1;
|
||||||
|
drawing.Material = new Material(material);
|
||||||
|
|
||||||
|
var normalized = ShapeProfile.NormalizeEntities(result.Entities);
|
||||||
|
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||||
|
|
||||||
|
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
|
||||||
|
{
|
||||||
|
var rapid = (RapidMove)pgm[0];
|
||||||
|
drawing.Source.Offset = rapid.EndPoint;
|
||||||
|
pgm.Offset(-rapid.EndPoint);
|
||||||
|
pgm.Codes.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawing.Program = pgm;
|
||||||
|
nest.Drawings.Add(drawing);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
importErrors.Add($"{part.FileName}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nest.Drawings.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
nest.CreatePlate();
|
||||||
|
|
||||||
|
var editForm = new EditNestForm(nest);
|
||||||
|
editForm.MdiParent = MdiParentForm;
|
||||||
|
editForm.Show();
|
||||||
|
editForm.PlateView.ZoomToFit();
|
||||||
|
|
||||||
|
nestsCreated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = $"{nestsCreated} nest{(nestsCreated != 1 ? "s" : "")} created.";
|
||||||
|
if (importErrors.Count > 0)
|
||||||
|
summary += $"\n\n{importErrors.Count} import error(s):\n" + string.Join("\n", importErrors);
|
||||||
|
|
||||||
|
MessageBox.Show(summary, "Import Complete", MessageBoxButtons.OK,
|
||||||
|
importErrors.Count > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void BtnClose_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class BomPartRow
|
||||||
|
{
|
||||||
|
public int? ItemNum { get; set; }
|
||||||
|
public string FileName { get; set; }
|
||||||
|
public int? Qty { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Material { get; set; }
|
||||||
|
public double? Thickness { get; set; }
|
||||||
|
public string DxfPath { get; set; }
|
||||||
|
public string Status { get; set; }
|
||||||
|
public bool IsEditable { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,8 @@ namespace OpenNest.Forms
|
|||||||
?? new List<Bend>();
|
?? new List<Bend>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bend.UpdateEtchEntities(result.Entities, bends);
|
||||||
|
|
||||||
var item = new FileListItem
|
var item = new FileListItem
|
||||||
{
|
{
|
||||||
Name = Path.GetFileNameWithoutExtension(file),
|
Name = Path.GetFileNameWithoutExtension(file),
|
||||||
@@ -245,6 +247,9 @@ namespace OpenNest.Forms
|
|||||||
bend.SourceEntity.IsVisible = true;
|
bend.SourceEntity.IsVisible = true;
|
||||||
|
|
||||||
item.Bends.RemoveAt(index);
|
item.Bends.RemoveAt(index);
|
||||||
|
Bend.UpdateEtchEntities(item.Entities, item.Bends);
|
||||||
|
entityView1.Entities.Clear();
|
||||||
|
entityView1.Entities.AddRange(item.Entities);
|
||||||
entityView1.Bends = item.Bends;
|
entityView1.Bends = item.Bends;
|
||||||
entityView1.SelectedBendIndex = -1;
|
entityView1.SelectedBendIndex = -1;
|
||||||
filterPanel.LoadItem(item.Entities, item.Bends);
|
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||||
@@ -281,16 +286,8 @@ namespace OpenNest.Forms
|
|||||||
var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList();
|
var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList();
|
||||||
if (entities.Count == 0) return;
|
if (entities.Count == 0) return;
|
||||||
|
|
||||||
var shape = new ShapeProfile(entities);
|
var normalized = ShapeProfile.NormalizeEntities(entities);
|
||||||
SetRotation(shape.Perimeter, RotationType.CW);
|
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||||
foreach (var cutout in shape.Cutouts)
|
|
||||||
SetRotation(cutout, RotationType.CCW);
|
|
||||||
|
|
||||||
var drawEntities = new List<Entity>();
|
|
||||||
drawEntities.AddRange(shape.Perimeter.Entities);
|
|
||||||
shape.Cutouts.ForEach(c => drawEntities.AddRange(c.Entities));
|
|
||||||
|
|
||||||
var pgm = ConvertGeometry.ToProgram(drawEntities);
|
|
||||||
var originOffset = Vector.Zero;
|
var originOffset = Vector.Zero;
|
||||||
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
|
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
|
||||||
{
|
{
|
||||||
@@ -395,6 +392,9 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
line.IsVisible = false;
|
line.IsVisible = false;
|
||||||
item.Bends.Add(bend);
|
item.Bends.Add(bend);
|
||||||
|
Bend.UpdateEtchEntities(item.Entities, item.Bends);
|
||||||
|
entityView1.Entities.Clear();
|
||||||
|
entityView1.Entities.AddRange(item.Entities);
|
||||||
entityView1.Bends = item.Bends;
|
entityView1.Bends = item.Bends;
|
||||||
filterPanel.LoadItem(item.Entities, item.Bends);
|
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||||
entityView1.Invalidate();
|
entityView1.Invalidate();
|
||||||
@@ -560,18 +560,8 @@ namespace OpenNest.Forms
|
|||||||
if (item.Bends != null)
|
if (item.Bends != null)
|
||||||
drawing.Bends.AddRange(item.Bends);
|
drawing.Bends.AddRange(item.Bends);
|
||||||
|
|
||||||
var shape = new ShapeProfile(entities);
|
var normalized = ShapeProfile.NormalizeEntities(entities);
|
||||||
|
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||||
SetRotation(shape.Perimeter, RotationType.CW);
|
|
||||||
|
|
||||||
foreach (var cutout in shape.Cutouts)
|
|
||||||
SetRotation(cutout, RotationType.CCW);
|
|
||||||
|
|
||||||
entities = new List<Entity>();
|
|
||||||
entities.AddRange(shape.Perimeter.Entities);
|
|
||||||
shape.Cutouts.ForEach(cutout => entities.AddRange(cutout.Entities));
|
|
||||||
|
|
||||||
var pgm = ConvertGeometry.ToProgram(entities);
|
|
||||||
var firstCode = pgm[0];
|
var firstCode = pgm[0];
|
||||||
|
|
||||||
if (firstCode.Type == CodeType.RapidMove)
|
if (firstCode.Type == CodeType.RapidMove)
|
||||||
@@ -605,15 +595,6 @@ namespace OpenNest.Forms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetRotation(Shape shape, RotationType rotation)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var dir = shape.ToPolygon(3).RotationDirection();
|
|
||||||
if (dir != rotation) shape.Reverse();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Color GetNextColor()
|
private static Color GetNextColor()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,419 @@
|
|||||||
|
using OpenNest.Data;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest.Forms
|
||||||
|
{
|
||||||
|
public class MachineConfigForm : Form
|
||||||
|
{
|
||||||
|
private readonly IDataProvider _provider;
|
||||||
|
private readonly TreeView _tree;
|
||||||
|
private readonly Panel _detailPanel;
|
||||||
|
|
||||||
|
private MachineConfig _currentMachine;
|
||||||
|
|
||||||
|
public MachineConfigForm(IDataProvider provider)
|
||||||
|
{
|
||||||
|
_provider = provider;
|
||||||
|
|
||||||
|
Text = "Machine Configuration";
|
||||||
|
Size = new Size(900, 600);
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
MinimumSize = new Size(700, 400);
|
||||||
|
|
||||||
|
var splitContainer = new SplitContainer
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
SplitterDistance = 250,
|
||||||
|
FixedPanel = FixedPanel.Panel1
|
||||||
|
};
|
||||||
|
|
||||||
|
_tree = new TreeView
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
HideSelection = false
|
||||||
|
};
|
||||||
|
_tree.AfterSelect += Tree_AfterSelect;
|
||||||
|
|
||||||
|
var treeButtonPanel = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Bottom,
|
||||||
|
AutoSize = true,
|
||||||
|
FlowDirection = FlowDirection.LeftToRight,
|
||||||
|
WrapContents = true,
|
||||||
|
Padding = new Padding(2)
|
||||||
|
};
|
||||||
|
|
||||||
|
var addMachineButton = new Button { Text = "+ Machine", AutoSize = true };
|
||||||
|
addMachineButton.Click += AddMachine_Click;
|
||||||
|
var removeMachineButton = new Button { Text = "- Machine", AutoSize = true };
|
||||||
|
removeMachineButton.Click += RemoveMachine_Click;
|
||||||
|
var addMaterialButton = new Button { Text = "+ Material", AutoSize = true };
|
||||||
|
addMaterialButton.Click += AddMaterial_Click;
|
||||||
|
var removeMaterialButton = new Button { Text = "- Material", AutoSize = true };
|
||||||
|
removeMaterialButton.Click += RemoveMaterial_Click;
|
||||||
|
var addThicknessButton = new Button { Text = "+ Thickness", AutoSize = true };
|
||||||
|
addThicknessButton.Click += AddThickness_Click;
|
||||||
|
var removeThicknessButton = new Button { Text = "- Thickness", AutoSize = true };
|
||||||
|
removeThicknessButton.Click += RemoveThickness_Click;
|
||||||
|
|
||||||
|
treeButtonPanel.Controls.AddRange(new Control[]
|
||||||
|
{
|
||||||
|
addMachineButton, removeMachineButton,
|
||||||
|
addMaterialButton, removeMaterialButton,
|
||||||
|
addThicknessButton, removeThicknessButton
|
||||||
|
});
|
||||||
|
|
||||||
|
splitContainer.Panel1.Controls.Add(_tree);
|
||||||
|
splitContainer.Panel1.Controls.Add(treeButtonPanel);
|
||||||
|
|
||||||
|
_detailPanel = new Panel { Dock = DockStyle.Fill, AutoScroll = true };
|
||||||
|
splitContainer.Panel2.Controls.Add(_detailPanel);
|
||||||
|
|
||||||
|
var bottomPanel = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Bottom,
|
||||||
|
AutoSize = true,
|
||||||
|
FlowDirection = FlowDirection.RightToLeft,
|
||||||
|
Padding = new Padding(4)
|
||||||
|
};
|
||||||
|
|
||||||
|
var saveButton = new Button { Text = "Save", AutoSize = true };
|
||||||
|
saveButton.Click += Save_Click;
|
||||||
|
var importButton = new Button { Text = "Import...", AutoSize = true };
|
||||||
|
importButton.Click += Import_Click;
|
||||||
|
var exportButton = new Button { Text = "Export...", AutoSize = true };
|
||||||
|
exportButton.Click += Export_Click;
|
||||||
|
|
||||||
|
bottomPanel.Controls.AddRange(new Control[] { saveButton, exportButton, importButton });
|
||||||
|
|
||||||
|
Controls.Add(splitContainer);
|
||||||
|
Controls.Add(bottomPanel);
|
||||||
|
|
||||||
|
LoadTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadTree()
|
||||||
|
{
|
||||||
|
_tree.Nodes.Clear();
|
||||||
|
foreach (var summary in _provider.GetMachines())
|
||||||
|
{
|
||||||
|
var machine = _provider.GetMachine(summary.Id);
|
||||||
|
if (machine is null) continue;
|
||||||
|
|
||||||
|
var machineNode = new TreeNode(machine.Name) { Tag = machine };
|
||||||
|
foreach (var material in machine.Materials)
|
||||||
|
{
|
||||||
|
var matNode = new TreeNode(material.Name) { Tag = material };
|
||||||
|
foreach (var thickness in material.Thicknesses)
|
||||||
|
{
|
||||||
|
var thickNode = new TreeNode(thickness.Value.ToString("0.####")) { Tag = thickness };
|
||||||
|
matNode.Nodes.Add(thickNode);
|
||||||
|
}
|
||||||
|
machineNode.Nodes.Add(matNode);
|
||||||
|
}
|
||||||
|
_tree.Nodes.Add(machineNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_tree.Nodes.Count > 0)
|
||||||
|
_tree.SelectedNode = _tree.Nodes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Tree_AfterSelect(object sender, TreeViewEventArgs e)
|
||||||
|
{
|
||||||
|
_detailPanel.Controls.Clear();
|
||||||
|
if (e.Node?.Tag is null) return;
|
||||||
|
|
||||||
|
switch (e.Node.Tag)
|
||||||
|
{
|
||||||
|
case MachineConfig machine:
|
||||||
|
_currentMachine = machine;
|
||||||
|
ShowMachineDetails(machine);
|
||||||
|
break;
|
||||||
|
case MaterialConfig material:
|
||||||
|
_currentMachine = e.Node.Parent?.Tag as MachineConfig;
|
||||||
|
ShowMaterialDetails(material);
|
||||||
|
break;
|
||||||
|
case ThicknessConfig thickness:
|
||||||
|
_currentMachine = e.Node.Parent?.Parent?.Tag as MachineConfig;
|
||||||
|
ShowThicknessDetails(thickness);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowMachineDetails(MachineConfig machine)
|
||||||
|
{
|
||||||
|
var layout = CreateDetailLayout();
|
||||||
|
var row = 0;
|
||||||
|
|
||||||
|
AddField(layout, ref row, "Name:", CreateTextBox(machine.Name, v => machine.Name = v));
|
||||||
|
AddField(layout, ref row, "Type:", CreateEnumCombo(machine.Type, v => machine.Type = v));
|
||||||
|
AddField(layout, ref row, "Units:", CreateEnumCombo(machine.Units, v => machine.Units = v));
|
||||||
|
|
||||||
|
_detailPanel.Controls.Add(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowMaterialDetails(MaterialConfig material)
|
||||||
|
{
|
||||||
|
var layout = CreateDetailLayout();
|
||||||
|
var row = 0;
|
||||||
|
|
||||||
|
AddField(layout, ref row, "Name:", CreateTextBox(material.Name, v => material.Name = v));
|
||||||
|
AddField(layout, ref row, "Grade:", CreateTextBox(material.Grade, v => material.Grade = v));
|
||||||
|
AddField(layout, ref row, "Density:", CreateNumericBox(material.Density, v => material.Density = v, 4));
|
||||||
|
|
||||||
|
_detailPanel.Controls.Add(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowThicknessDetails(ThicknessConfig thickness)
|
||||||
|
{
|
||||||
|
var layout = CreateDetailLayout();
|
||||||
|
var row = 0;
|
||||||
|
|
||||||
|
AddField(layout, ref row, "Thickness:", CreateNumericBox(thickness.Value, v => thickness.Value = v, 4));
|
||||||
|
AddField(layout, ref row, "Kerf:", CreateNumericBox(thickness.Kerf, v => thickness.Kerf = v, 4));
|
||||||
|
AddField(layout, ref row, "Assist Gas:", CreateTextBox(thickness.AssistGas, v => thickness.AssistGas = v));
|
||||||
|
|
||||||
|
AddSectionHeader(layout, ref row, "Lead In");
|
||||||
|
AddField(layout, ref row, "Type:", CreateTextBox(thickness.LeadIn.Type, v => thickness.LeadIn.Type = v));
|
||||||
|
AddField(layout, ref row, "Length:", CreateNumericBox(thickness.LeadIn.Length, v => thickness.LeadIn.Length = v, 4));
|
||||||
|
AddField(layout, ref row, "Angle:", CreateNumericBox(thickness.LeadIn.Angle, v => thickness.LeadIn.Angle = v, 1));
|
||||||
|
AddField(layout, ref row, "Radius:", CreateNumericBox(thickness.LeadIn.Radius, v => thickness.LeadIn.Radius = v, 4));
|
||||||
|
|
||||||
|
AddSectionHeader(layout, ref row, "Lead Out");
|
||||||
|
AddField(layout, ref row, "Type:", CreateTextBox(thickness.LeadOut.Type, v => thickness.LeadOut.Type = v));
|
||||||
|
AddField(layout, ref row, "Length:", CreateNumericBox(thickness.LeadOut.Length, v => thickness.LeadOut.Length = v, 4));
|
||||||
|
AddField(layout, ref row, "Angle:", CreateNumericBox(thickness.LeadOut.Angle, v => thickness.LeadOut.Angle = v, 1));
|
||||||
|
AddField(layout, ref row, "Radius:", CreateNumericBox(thickness.LeadOut.Radius, v => thickness.LeadOut.Radius = v, 4));
|
||||||
|
|
||||||
|
AddSectionHeader(layout, ref row, "Cut Off");
|
||||||
|
AddField(layout, ref row, "Part Clearance:", CreateNumericBox(thickness.CutOff.PartClearance, v => thickness.CutOff.PartClearance = v, 4));
|
||||||
|
AddField(layout, ref row, "Overtravel:", CreateNumericBox(thickness.CutOff.Overtravel, v => thickness.CutOff.Overtravel = v, 4));
|
||||||
|
AddField(layout, ref row, "Min Segment:", CreateNumericBox(thickness.CutOff.MinSegmentLength, v => thickness.CutOff.MinSegmentLength = v, 4));
|
||||||
|
AddField(layout, ref row, "Direction:", CreateTextBox(thickness.CutOff.Direction, v => thickness.CutOff.Direction = v));
|
||||||
|
|
||||||
|
AddSectionHeader(layout, ref row, "Plate Sizes");
|
||||||
|
var sizesText = string.Join(", ", thickness.PlateSizes);
|
||||||
|
AddField(layout, ref row, "Sizes:", CreateTextBox(sizesText, v =>
|
||||||
|
{
|
||||||
|
thickness.PlateSizes = v.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||||
|
}));
|
||||||
|
|
||||||
|
_detailPanel.Controls.Add(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TableLayoutPanel CreateDetailLayout()
|
||||||
|
{
|
||||||
|
var layout = new TableLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
AutoSize = true,
|
||||||
|
ColumnCount = 2,
|
||||||
|
Padding = new Padding(8)
|
||||||
|
};
|
||||||
|
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||||
|
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddField(TableLayoutPanel layout, ref int row, string label, Control control)
|
||||||
|
{
|
||||||
|
layout.RowCount = row + 1;
|
||||||
|
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||||
|
layout.Controls.Add(new Label { Text = label, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 6, 8, 0) }, 0, row);
|
||||||
|
control.Dock = DockStyle.Fill;
|
||||||
|
layout.Controls.Add(control, 1, row);
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddSectionHeader(TableLayoutPanel layout, ref int row, string text)
|
||||||
|
{
|
||||||
|
layout.RowCount = row + 1;
|
||||||
|
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||||
|
var label = new Label
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
AutoSize = true,
|
||||||
|
Font = new Font(Control.DefaultFont, FontStyle.Bold),
|
||||||
|
Margin = new Padding(0, 12, 0, 4)
|
||||||
|
};
|
||||||
|
layout.SetColumnSpan(label, 2);
|
||||||
|
layout.Controls.Add(label, 0, row);
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextBox CreateTextBox(string value, Action<string> setter)
|
||||||
|
{
|
||||||
|
var textBox = new TextBox { Text = value };
|
||||||
|
textBox.TextChanged += (s, e) => setter(textBox.Text);
|
||||||
|
return textBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NumericUpDown CreateNumericBox(double value, Action<double> setter, int decimals)
|
||||||
|
{
|
||||||
|
var numeric = new NumericUpDown
|
||||||
|
{
|
||||||
|
DecimalPlaces = decimals,
|
||||||
|
Minimum = 0,
|
||||||
|
Maximum = 10000,
|
||||||
|
Increment = (decimal)System.Math.Pow(10, -decimals),
|
||||||
|
Value = (decimal)value
|
||||||
|
};
|
||||||
|
numeric.ValueChanged += (s, e) => setter((double)numeric.Value);
|
||||||
|
return numeric;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ComboBox CreateEnumCombo<T>(T currentValue, Action<T> setter) where T : struct, Enum
|
||||||
|
{
|
||||||
|
var combo = new ComboBox
|
||||||
|
{
|
||||||
|
DropDownStyle = ComboBoxStyle.DropDownList
|
||||||
|
};
|
||||||
|
combo.Items.AddRange(Enum.GetNames<T>().Cast<object>().ToArray());
|
||||||
|
combo.SelectedItem = currentValue.ToString();
|
||||||
|
combo.SelectedIndexChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (Enum.TryParse<T>(combo.SelectedItem?.ToString(), out var val))
|
||||||
|
setter(val);
|
||||||
|
};
|
||||||
|
return combo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
foreach (TreeNode machineNode in _tree.Nodes)
|
||||||
|
{
|
||||||
|
if (machineNode.Tag is MachineConfig machine)
|
||||||
|
_provider.SaveMachine(machine);
|
||||||
|
}
|
||||||
|
MessageBox.Show("Machine configurations saved.", "Saved", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddMachine_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var machine = new MachineConfig { Name = "New Machine" };
|
||||||
|
_provider.SaveMachine(machine);
|
||||||
|
LoadTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveMachine_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_tree.SelectedNode?.Tag is not MachineConfig machine) return;
|
||||||
|
if (MessageBox.Show($"Delete machine '{machine.Name}'?", "Confirm", MessageBoxButtons.YesNo) != DialogResult.Yes) return;
|
||||||
|
|
||||||
|
_provider.DeleteMachine(machine.Id);
|
||||||
|
LoadTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddMaterial_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_currentMachine is null) return;
|
||||||
|
|
||||||
|
_currentMachine.Materials.Add(new MaterialConfig { Name = "New Material" });
|
||||||
|
_provider.SaveMachine(_currentMachine);
|
||||||
|
LoadTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveMaterial_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_tree.SelectedNode?.Tag is not MaterialConfig material) return;
|
||||||
|
if (_currentMachine is null) return;
|
||||||
|
|
||||||
|
_currentMachine.Materials.Remove(material);
|
||||||
|
_provider.SaveMachine(_currentMachine);
|
||||||
|
LoadTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddThickness_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var material = _tree.SelectedNode?.Tag as MaterialConfig;
|
||||||
|
if (material is null && _tree.SelectedNode?.Tag is ThicknessConfig)
|
||||||
|
material = _tree.SelectedNode.Parent?.Tag as MaterialConfig;
|
||||||
|
if (material is null || _currentMachine is null) return;
|
||||||
|
|
||||||
|
material.Thicknesses.Add(new ThicknessConfig { Value = 0.250 });
|
||||||
|
_provider.SaveMachine(_currentMachine);
|
||||||
|
LoadTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveThickness_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_tree.SelectedNode?.Tag is not ThicknessConfig thickness) return;
|
||||||
|
var material = _tree.SelectedNode.Parent?.Tag as MaterialConfig;
|
||||||
|
if (material is null || _currentMachine is null) return;
|
||||||
|
|
||||||
|
material.Thicknesses.Remove(thickness);
|
||||||
|
_provider.SaveMachine(_currentMachine);
|
||||||
|
LoadTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Import_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
using (var dialog = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Filter = "JSON files (*.json)|*.json",
|
||||||
|
Title = "Import Machine Configuration"
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (dialog.ShowDialog() != DialogResult.OK) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(dialog.FileName);
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||||
|
};
|
||||||
|
var machine = JsonSerializer.Deserialize<MachineConfig>(json, options);
|
||||||
|
if (machine is null) return;
|
||||||
|
|
||||||
|
machine.Id = Guid.NewGuid();
|
||||||
|
_provider.SaveMachine(machine);
|
||||||
|
LoadTree();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Failed to import: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Export_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_currentMachine is null) return;
|
||||||
|
|
||||||
|
using (var dialog = new SaveFileDialog
|
||||||
|
{
|
||||||
|
Filter = "JSON files (*.json)|*.json",
|
||||||
|
FileName = $"{_currentMachine.Name}.json",
|
||||||
|
Title = "Export Machine Configuration"
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (dialog.ShowDialog() != DialogResult.OK) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(_currentMachine, options);
|
||||||
|
File.WriteAllText(dialog.FileName, json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Failed to export: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+20
-2
@@ -32,6 +32,7 @@
|
|||||||
mnuFile = new System.Windows.Forms.ToolStripMenuItem();
|
mnuFile = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuFileNew = new System.Windows.Forms.ToolStripMenuItem();
|
mnuFileNew = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuFileOpen = new System.Windows.Forms.ToolStripMenuItem();
|
mnuFileOpen = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
mnuFileImportBom = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
toolStripMenuItem1 = new System.Windows.Forms.ToolStripSeparator();
|
toolStripMenuItem1 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
mnuFileSave = new System.Windows.Forms.ToolStripMenuItem();
|
mnuFileSave = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuFileSaveAs = new System.Windows.Forms.ToolStripMenuItem();
|
mnuFileSaveAs = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
mnuSetOffsetIncrement = new System.Windows.Forms.ToolStripMenuItem();
|
mnuSetOffsetIncrement = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuSetRotationIncrement = new System.Windows.Forms.ToolStripMenuItem();
|
mnuSetRotationIncrement = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
toolStripMenuItem15 = new System.Windows.Forms.ToolStripSeparator();
|
toolStripMenuItem15 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
|
mnuToolsMachineConfig = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuToolsOptions = new System.Windows.Forms.ToolStripMenuItem();
|
mnuToolsOptions = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuNest = new System.Windows.Forms.ToolStripMenuItem();
|
mnuNest = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem();
|
mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
@@ -171,7 +173,7 @@
|
|||||||
//
|
//
|
||||||
// mnuFile
|
// mnuFile
|
||||||
//
|
//
|
||||||
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, toolStripMenuItem3, mnuFileExit });
|
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, toolStripMenuItem3, mnuFileExit });
|
||||||
mnuFile.Name = "mnuFile";
|
mnuFile.Name = "mnuFile";
|
||||||
mnuFile.Size = new System.Drawing.Size(37, 20);
|
mnuFile.Size = new System.Drawing.Size(37, 20);
|
||||||
mnuFile.Text = "&File";
|
mnuFile.Text = "&File";
|
||||||
@@ -194,6 +196,13 @@
|
|||||||
mnuFileOpen.Text = "Open";
|
mnuFileOpen.Text = "Open";
|
||||||
mnuFileOpen.Click += Open_Click;
|
mnuFileOpen.Click += Open_Click;
|
||||||
//
|
//
|
||||||
|
// mnuFileImportBom
|
||||||
|
//
|
||||||
|
mnuFileImportBom.Name = "mnuFileImportBom";
|
||||||
|
mnuFileImportBom.Size = new System.Drawing.Size(180, 22);
|
||||||
|
mnuFileImportBom.Text = "Import BOM...";
|
||||||
|
mnuFileImportBom.Click += ImportBom_Click;
|
||||||
|
//
|
||||||
// toolStripMenuItem1
|
// toolStripMenuItem1
|
||||||
//
|
//
|
||||||
toolStripMenuItem1.Name = "toolStripMenuItem1";
|
toolStripMenuItem1.Name = "toolStripMenuItem1";
|
||||||
@@ -395,7 +404,7 @@
|
|||||||
//
|
//
|
||||||
// mnuTools
|
// mnuTools
|
||||||
//
|
//
|
||||||
mnuTools.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuToolsMeasureArea, mnuToolsBestFitViewer, mnuToolsPatternTile, mnuToolsAlign, toolStripMenuItem14, mnuSetOffsetIncrement, mnuSetRotationIncrement, toolStripMenuItem15, mnuToolsOptions });
|
mnuTools.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuToolsMeasureArea, mnuToolsBestFitViewer, mnuToolsPatternTile, mnuToolsAlign, toolStripMenuItem14, mnuSetOffsetIncrement, mnuSetRotationIncrement, toolStripMenuItem15, mnuToolsMachineConfig, mnuToolsOptions });
|
||||||
mnuTools.Name = "mnuTools";
|
mnuTools.Name = "mnuTools";
|
||||||
mnuTools.Size = new System.Drawing.Size(47, 20);
|
mnuTools.Size = new System.Drawing.Size(47, 20);
|
||||||
mnuTools.Text = "&Tools";
|
mnuTools.Text = "&Tools";
|
||||||
@@ -520,6 +529,13 @@
|
|||||||
toolStripMenuItem15.Name = "toolStripMenuItem15";
|
toolStripMenuItem15.Name = "toolStripMenuItem15";
|
||||||
toolStripMenuItem15.Size = new System.Drawing.Size(211, 6);
|
toolStripMenuItem15.Size = new System.Drawing.Size(211, 6);
|
||||||
//
|
//
|
||||||
|
// mnuToolsMachineConfig
|
||||||
|
//
|
||||||
|
mnuToolsMachineConfig.Name = "mnuToolsMachineConfig";
|
||||||
|
mnuToolsMachineConfig.Size = new System.Drawing.Size(214, 22);
|
||||||
|
mnuToolsMachineConfig.Text = "Machine Configuration...";
|
||||||
|
mnuToolsMachineConfig.Click += MachineConfig_Click;
|
||||||
|
//
|
||||||
// mnuToolsOptions
|
// mnuToolsOptions
|
||||||
//
|
//
|
||||||
mnuToolsOptions.Name = "mnuToolsOptions";
|
mnuToolsOptions.Name = "mnuToolsOptions";
|
||||||
@@ -1125,6 +1141,7 @@
|
|||||||
private System.Windows.Forms.ToolStripMenuItem mnuFile;
|
private System.Windows.Forms.ToolStripMenuItem mnuFile;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuFileNew;
|
private System.Windows.Forms.ToolStripMenuItem mnuFileNew;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuFileOpen;
|
private System.Windows.Forms.ToolStripMenuItem mnuFileOpen;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuFileImportBom;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem1;
|
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem1;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuFileSave;
|
private System.Windows.Forms.ToolStripMenuItem mnuFileSave;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuFileSaveAs;
|
private System.Windows.Forms.ToolStripMenuItem mnuFileSaveAs;
|
||||||
@@ -1145,6 +1162,7 @@
|
|||||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawOffset;
|
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawOffset;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem5;
|
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem5;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuTools;
|
private System.Windows.Forms.ToolStripMenuItem mnuTools;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuToolsMachineConfig;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuToolsOptions;
|
private System.Windows.Forms.ToolStripMenuItem mnuToolsOptions;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuNest;
|
private System.Windows.Forms.ToolStripMenuItem mnuNest;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuNestEdit;
|
private System.Windows.Forms.ToolStripMenuItem mnuNestEdit;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using OpenNest.Actions;
|
using OpenNest.Actions;
|
||||||
using OpenNest.Collections;
|
using OpenNest.Collections;
|
||||||
|
using OpenNest.Data;
|
||||||
using OpenNest.Engine.BestFit;
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
@@ -473,6 +474,13 @@ namespace OpenNest.Forms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ImportBom_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var form = new BomImportForm();
|
||||||
|
form.MdiParentForm = this;
|
||||||
|
form.ShowDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
private void Save_Click(object sender, EventArgs e)
|
private void Save_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (activeForm != null)
|
if (activeForm != null)
|
||||||
@@ -732,6 +740,17 @@ namespace OpenNest.Forms
|
|||||||
form.ShowDialog();
|
form.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void MachineConfig_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "OpenNest", "Machines");
|
||||||
|
var provider = new LocalJsonProvider(appDataPath);
|
||||||
|
provider.EnsureDefaults();
|
||||||
|
using (var form = new MachineConfigForm(provider))
|
||||||
|
{
|
||||||
|
form.ShowDialog(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void AlignLeft_Click(object sender, EventArgs e)
|
private void AlignLeft_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (activeForm == null) return;
|
if (activeForm == null) return;
|
||||||
|
|||||||
+24
-20
@@ -178,32 +178,36 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var result = new List<PointF[]>();
|
var result = new List<PointF[]>();
|
||||||
var entities = ConvertProgram.ToGeometry(BasePart.Program);
|
var entities = ConvertProgram.ToGeometry(BasePart.Program);
|
||||||
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
var profile = new ShapeProfile(
|
||||||
|
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
|
||||||
foreach (var shape in shapes)
|
AddOffsetPolygon(result, profile.Perimeter.OffsetOutward(spacing), tolerance);
|
||||||
{
|
|
||||||
var offsetEntity = shape.OffsetOutward(spacing);
|
|
||||||
|
|
||||||
if (offsetEntity == null)
|
foreach (var cutout in profile.Cutouts)
|
||||||
continue;
|
AddOffsetPolygon(result, cutout.OffsetInward(spacing), tolerance);
|
||||||
|
|
||||||
var polygon = offsetEntity.ToPolygonWithTolerance(tolerance);
|
|
||||||
polygon.RemoveSelfIntersections();
|
|
||||||
|
|
||||||
if (polygon.Vertices.Count < 2)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var pts = new PointF[polygon.Vertices.Count];
|
|
||||||
|
|
||||||
for (var j = 0; j < pts.Length; j++)
|
|
||||||
pts[j] = new PointF((float)polygon.Vertices[j].X, (float)polygon.Vertices[j].Y);
|
|
||||||
|
|
||||||
result.Add(pts);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddOffsetPolygon(List<PointF[]> result, Shape offsetEntity, double tolerance)
|
||||||
|
{
|
||||||
|
if (offsetEntity == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var polygon = offsetEntity.ToPolygonWithTolerance(tolerance);
|
||||||
|
polygon.RemoveSelfIntersections();
|
||||||
|
|
||||||
|
if (polygon.Vertices.Count < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pts = new PointF[polygon.Vertices.Count];
|
||||||
|
|
||||||
|
for (var j = 0; j < pts.Length; j++)
|
||||||
|
pts[j] = new PointF((float)polygon.Vertices[j].X, (float)polygon.Vertices[j].Y);
|
||||||
|
|
||||||
|
result.Add(pts);
|
||||||
|
}
|
||||||
|
|
||||||
private void RebuildOffsetPath(Matrix matrix)
|
private void RebuildOffsetPath(Matrix matrix)
|
||||||
{
|
{
|
||||||
OffsetPath?.Dispose();
|
OffsetPath?.Dispose();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
|
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Data\OpenNest.Data.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||||
|
|||||||
Reference in New Issue
Block a user