Compare commits
29 Commits
a34811bb6d
...
2db8c49838
| 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": {
|
||||
"opennest": {
|
||||
"command": "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/OpenNest.Mcp.exe",
|
||||
"args": []
|
||||
"command": "cmd",
|
||||
"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)
|
||||
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 drawing = new Drawing(name);
|
||||
drawing.Program = pgm;
|
||||
|
||||
@@ -255,7 +255,8 @@ static class NestConsole
|
||||
return null;
|
||||
}
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(geometry);
|
||||
var normalized = ShapeProfile.NormalizeEntities(geometry);
|
||||
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||
|
||||
if (pgm == null)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace OpenNest.Bending
|
||||
{
|
||||
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 EndPoint { get; set; }
|
||||
public BendDirection Direction { get; set; }
|
||||
@@ -29,6 +39,52 @@ namespace OpenNest.Bending
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
var dir = Direction.ToString();
|
||||
|
||||
@@ -108,7 +108,10 @@ namespace OpenNest.Converters
|
||||
if (line.StartPoint != lastpt)
|
||||
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;
|
||||
return lastpt;
|
||||
|
||||
@@ -598,6 +598,41 @@ namespace OpenNest.Geometry
|
||||
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>
|
||||
/// Gets the closest point on the shape to the given point.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
@@ -41,5 +42,52 @@ namespace OpenNest.Geometry
|
||||
public Shape Perimeter { 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)
|
||||
{
|
||||
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 totalSpacing = spacing + chordTolerance;
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
// Add chord tolerance to compensate for inscribed polygon chords
|
||||
// being inside the actual offset arcs.
|
||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||
AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||
chordTolerance, part.Location);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
|
||||
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||
polygon.RemoveSelfIntersections();
|
||||
polygon.Offset(part.Location);
|
||||
lines.AddRange(polygon.ToLines());
|
||||
}
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
AddOffsetLines(lines, cutout.OffsetInward(totalSpacing),
|
||||
chordTolerance, part.Location);
|
||||
|
||||
return lines;
|
||||
}
|
||||
@@ -66,21 +60,17 @@ namespace OpenNest
|
||||
public static List<Line> GetOffsetPartLines(Part part, double spacing, PushDirection facingDirection, double chordTolerance = 0.001)
|
||||
{
|
||||
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 totalSpacing = spacing + chordTolerance;
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||
chordTolerance, part.Location, facingDirection);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
|
||||
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||
polygon.RemoveSelfIntersections();
|
||||
polygon.Offset(part.Location);
|
||||
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
||||
}
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
AddOffsetDirectionalLines(lines, cutout.OffsetInward(totalSpacing),
|
||||
chordTolerance, part.Location, facingDirection);
|
||||
|
||||
return lines;
|
||||
}
|
||||
@@ -104,21 +94,17 @@ namespace OpenNest
|
||||
public static List<Line> GetOffsetPartLines(Part part, double spacing, Vector facingDirection, double chordTolerance = 0.001)
|
||||
{
|
||||
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 totalSpacing = spacing + chordTolerance;
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||
chordTolerance, part.Location, facingDirection);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
|
||||
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||
polygon.RemoveSelfIntersections();
|
||||
polygon.Offset(part.Location);
|
||||
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
||||
}
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
AddOffsetDirectionalLines(lines, cutout.OffsetInward(totalSpacing),
|
||||
chordTolerance, part.Location, facingDirection);
|
||||
|
||||
return lines;
|
||||
}
|
||||
@@ -189,5 +175,41 @@ namespace OpenNest
|
||||
|
||||
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 MaxAspectRatio { get; set; } = 5.0;
|
||||
public double MinUtilization { get; set; } = 0.3;
|
||||
public double UtilizationOverride { get; set; } = 0.75;
|
||||
|
||||
public void Apply(List<BestFitResult> results)
|
||||
{
|
||||
@@ -25,7 +26,7 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
var aspect = result.LongestSide / result.ShortestSide;
|
||||
|
||||
if (aspect > MaxAspectRatio)
|
||||
if (aspect > MaxAspectRatio && result.Utilization < UtilizationOverride)
|
||||
{
|
||||
result.Keep = false;
|
||||
result.Reason = string.Format("Aspect ratio {0:F1} exceeds max {1}", aspect, MaxAspectRatio);
|
||||
|
||||
@@ -22,18 +22,11 @@ namespace OpenNest.Engine.BestFit
|
||||
if (perimeter == null)
|
||||
return new PolygonExtractionResult(null, Vector.Zero);
|
||||
|
||||
// Inflate by half-spacing if spacing is non-zero.
|
||||
// Detect winding direction to choose the correct outward offset side.
|
||||
var outwardSide = OffsetSide.Right;
|
||||
if (halfSpacing > 0)
|
||||
{
|
||||
var testPoly = perimeter.ToPolygon();
|
||||
if (testPoly.Vertices.Count >= 3 && testPoly.RotationDirection() == RotationType.CW)
|
||||
outwardSide = OffsetSide.Left;
|
||||
}
|
||||
// Ensure CW winding for correct outward offset direction.
|
||||
definedShape.NormalizeWinding();
|
||||
|
||||
var inflated = halfSpacing > 0
|
||||
? (perimeter.OffsetEntity(halfSpacing, outwardSide) as Shape ?? perimeter)
|
||||
? (perimeter.OffsetOutward(halfSpacing) ?? perimeter)
|
||||
: perimeter;
|
||||
|
||||
// Convert to polygon with circumscribed arcs for tight nesting.
|
||||
|
||||
@@ -3,6 +3,7 @@ using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
@@ -349,6 +350,21 @@ namespace OpenNest.Engine.Fill
|
||||
if (copyDistance <= Tolerance.Epsilon)
|
||||
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})");
|
||||
|
||||
// Build all columns.
|
||||
|
||||
@@ -287,6 +287,65 @@ namespace OpenNest.Engine.Fill
|
||||
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>
|
||||
/// Creates a seed pattern containing a single part positioned at the work area origin.
|
||||
/// 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);
|
||||
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 (row.Count <= pattern.Parts.Count)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -321,9 +321,19 @@ namespace OpenNest.Engine.Fill
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
var remnantEngine = NestEngineRegistry.Create(plate);
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
var parts = remnantEngine.Fill(item, remnantBox, null, token);
|
||||
var filler = new FillLinear(remnantBox, partSpacing);
|
||||
List<Part> parts = null;
|
||||
|
||||
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 " +
|
||||
$"{remnantBox.Width:F2}x{remnantBox.Length:F2}");
|
||||
|
||||
@@ -244,28 +244,29 @@ public class StripeFiller
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear");
|
||||
try
|
||||
var filler = new FillLinear(remnantBox, spacing);
|
||||
List<Part> best = null;
|
||||
|
||||
foreach (var angle in new[] { 0.0, Angle.HalfPI })
|
||||
{
|
||||
var engine = CreateRemnantEngine(_context.Plate);
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
var parts = engine.Fill(item, remnantBox, _context.Progress, _context.Token);
|
||||
_context.Token.ThrowIfCancellationRequested();
|
||||
var result = FillHelpers.FillWithDirectionPreference(
|
||||
dir => filler.Fill(drawing, angle, dir),
|
||||
null, _comparer, remnantBox);
|
||||
|
||||
Debug.WriteLine($"[StripeFiller] Remnant engine ({engine.Name}): {parts?.Count ?? 0} parts, " +
|
||||
$"winner={engine.WinnerPhase}");
|
||||
|
||||
if (parts != null && parts.Count > 0)
|
||||
{
|
||||
FillResultCache.Store(drawing, remnantBox, spacing, parts);
|
||||
return parts;
|
||||
}
|
||||
|
||||
return null;
|
||||
if (result != null && result.Count > (best?.Count ?? 0))
|
||||
best = result;
|
||||
}
|
||||
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(
|
||||
|
||||
@@ -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.Engine\OpenNest.Engine.csproj" />
|
||||
<PackageReference Include="ACadSharp" Version="3.1.32" />
|
||||
<PackageReference Include="ClosedXML" Version="0.104.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ModelContextProtocol.Server;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Shapes;
|
||||
using System.ComponentModel;
|
||||
@@ -103,7 +104,8 @@ namespace OpenNest.Mcp.Tools
|
||||
if (geometry.Count == 0)
|
||||
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)
|
||||
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>
|
||||
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Data\OpenNest.Data.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.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));
|
||||
drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(entities);
|
||||
var normalized = ShapeProfile.NormalizeEntities(entities);
|
||||
drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(normalized);
|
||||
drawing.UpdateArea();
|
||||
drawing.Color = PartColors[colorIndex % PartColors.Length];
|
||||
colorIndex++;
|
||||
|
||||
@@ -30,6 +30,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PostProcessors", "PostProce
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Posts.Cincinnati", "OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj", "{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Data", "OpenNest.Data\OpenNest.Data.csproj", "{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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|x86.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -112,8 +112,6 @@ namespace OpenNest.Controls
|
||||
DrawEntity(e.Graphics, entity, pen);
|
||||
}
|
||||
|
||||
DrawEtchMarks(e.Graphics);
|
||||
|
||||
if (SimplifierPreview != null)
|
||||
{
|
||||
// Draw tolerance zone (offset lines each side of original geometry)
|
||||
@@ -240,46 +238,6 @@ namespace OpenNest.Controls
|
||||
private static bool IsEtchLayer(Layer layer) =>
|
||||
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)
|
||||
{
|
||||
if (Bends == null || Bends.Count == 0)
|
||||
|
||||
@@ -584,6 +584,7 @@ namespace OpenNest.Controls
|
||||
|
||||
part.Draw(g, (i + 1).ToString());
|
||||
DrawBendLines(g, part.BasePart);
|
||||
DrawEtchMarks(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)
|
||||
{
|
||||
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>();
|
||||
}
|
||||
|
||||
Bend.UpdateEtchEntities(result.Entities, bends);
|
||||
|
||||
var item = new FileListItem
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(file),
|
||||
@@ -245,6 +247,9 @@ namespace OpenNest.Forms
|
||||
bend.SourceEntity.IsVisible = true;
|
||||
|
||||
item.Bends.RemoveAt(index);
|
||||
Bend.UpdateEtchEntities(item.Entities, item.Bends);
|
||||
entityView1.Entities.Clear();
|
||||
entityView1.Entities.AddRange(item.Entities);
|
||||
entityView1.Bends = item.Bends;
|
||||
entityView1.SelectedBendIndex = -1;
|
||||
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();
|
||||
if (entities.Count == 0) return;
|
||||
|
||||
var shape = new ShapeProfile(entities);
|
||||
SetRotation(shape.Perimeter, RotationType.CW);
|
||||
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 normalized = ShapeProfile.NormalizeEntities(entities);
|
||||
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||
var originOffset = Vector.Zero;
|
||||
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
|
||||
{
|
||||
@@ -395,6 +392,9 @@ namespace OpenNest.Forms
|
||||
|
||||
line.IsVisible = false;
|
||||
item.Bends.Add(bend);
|
||||
Bend.UpdateEtchEntities(item.Entities, item.Bends);
|
||||
entityView1.Entities.Clear();
|
||||
entityView1.Entities.AddRange(item.Entities);
|
||||
entityView1.Bends = item.Bends;
|
||||
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||
entityView1.Invalidate();
|
||||
@@ -560,18 +560,8 @@ namespace OpenNest.Forms
|
||||
if (item.Bends != null)
|
||||
drawing.Bends.AddRange(item.Bends);
|
||||
|
||||
var shape = new ShapeProfile(entities);
|
||||
|
||||
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 normalized = ShapeProfile.NormalizeEntities(entities);
|
||||
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||
var firstCode = pgm[0];
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -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
+23
-5
@@ -32,6 +32,7 @@
|
||||
mnuFile = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileNew = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileOpen = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileImportBom = new System.Windows.Forms.ToolStripMenuItem();
|
||||
toolStripMenuItem1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
mnuFileSave = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileSaveAs = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -78,6 +79,7 @@
|
||||
mnuSetOffsetIncrement = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuSetRotationIncrement = new System.Windows.Forms.ToolStripMenuItem();
|
||||
toolStripMenuItem15 = new System.Windows.Forms.ToolStripSeparator();
|
||||
mnuToolsMachineConfig = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuToolsOptions = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNest = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -171,7 +173,7 @@
|
||||
//
|
||||
// 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.Size = new System.Drawing.Size(37, 20);
|
||||
mnuFile.Text = "&File";
|
||||
@@ -186,14 +188,21 @@
|
||||
mnuFileNew.Click += New_Click;
|
||||
//
|
||||
// mnuFileOpen
|
||||
//
|
||||
//
|
||||
mnuFileOpen.Image = Properties.Resources.doc_open;
|
||||
mnuFileOpen.Name = "mnuFileOpen";
|
||||
mnuFileOpen.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O;
|
||||
mnuFileOpen.Size = new System.Drawing.Size(146, 22);
|
||||
mnuFileOpen.Text = "Open";
|
||||
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.Name = "toolStripMenuItem1";
|
||||
@@ -395,7 +404,7 @@
|
||||
//
|
||||
// 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.Size = new System.Drawing.Size(47, 20);
|
||||
mnuTools.Text = "&Tools";
|
||||
@@ -520,8 +529,15 @@
|
||||
toolStripMenuItem15.Name = "toolStripMenuItem15";
|
||||
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.Name = "mnuToolsOptions";
|
||||
mnuToolsOptions.Size = new System.Drawing.Size(214, 22);
|
||||
mnuToolsOptions.Text = "Options";
|
||||
@@ -1125,6 +1141,7 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFile;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileNew;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileOpen;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileImportBom;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem1;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileSave;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileSaveAs;
|
||||
@@ -1145,6 +1162,7 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawOffset;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem5;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuTools;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuToolsMachineConfig;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuToolsOptions;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNest;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestEdit;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using OpenNest.Actions;
|
||||
using OpenNest.Collections;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Fill;
|
||||
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)
|
||||
{
|
||||
if (activeForm != null)
|
||||
@@ -732,6 +740,17 @@ namespace OpenNest.Forms
|
||||
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)
|
||||
{
|
||||
if (activeForm == null) return;
|
||||
|
||||
+24
-20
@@ -178,32 +178,36 @@ namespace OpenNest
|
||||
{
|
||||
var result = new List<PointF[]>();
|
||||
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)
|
||||
{
|
||||
var offsetEntity = shape.OffsetOutward(spacing);
|
||||
AddOffsetPolygon(result, profile.Perimeter.OffsetOutward(spacing), tolerance);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
|
||||
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);
|
||||
}
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
AddOffsetPolygon(result, cutout.OffsetInward(spacing), tolerance);
|
||||
|
||||
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)
|
||||
{
|
||||
OffsetPath?.Dispose();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.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.Gpu\OpenNest.Gpu.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user