merge: resolve .gitignore conflict, keep both entries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -201,3 +201,6 @@ FakesAssemblies/
|
|||||||
|
|
||||||
# Git worktrees
|
# Git worktrees
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ if (!keepParts)
|
|||||||
plate.Parts.Clear();
|
plate.Parts.Clear();
|
||||||
|
|
||||||
Console.WriteLine($"Nest: {nest.Name}");
|
Console.WriteLine($"Nest: {nest.Name}");
|
||||||
Console.WriteLine($"Plate: {plateIndex} ({plate.Size.Width:F1} x {plate.Size.Height:F1}), spacing={plate.PartSpacing:F2}");
|
Console.WriteLine($"Plate: {plateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}");
|
||||||
Console.WriteLine($"Drawing: {drawing.Name}");
|
Console.WriteLine($"Drawing: {drawing.Name}");
|
||||||
|
|
||||||
if (!keepParts)
|
if (!keepParts)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class AssignmentParameters
|
||||||
|
{
|
||||||
|
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
|
||||||
|
public string Preference { get; set; } = "ILAT";
|
||||||
|
public double MinGeometryLength { get; set; } = 0.01;
|
||||||
|
}
|
||||||
|
}
|
||||||
206
OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
Normal file
206
OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class ContourCuttingStrategy
|
||||||
|
{
|
||||||
|
public CuttingParameters Parameters { get; set; }
|
||||||
|
|
||||||
|
public Program Apply(Program partProgram, Plate plate)
|
||||||
|
{
|
||||||
|
var exitPoint = GetExitPoint(plate);
|
||||||
|
var entities = partProgram.ToGeometry();
|
||||||
|
var profile = new ShapeProfile(entities);
|
||||||
|
|
||||||
|
// Find closest point on perimeter from exit point
|
||||||
|
var perimeterPoint = profile.Perimeter.ClosestPointTo(exitPoint, out var perimeterEntity);
|
||||||
|
|
||||||
|
// Chain cutouts by nearest-neighbor from perimeter point, then reverse
|
||||||
|
// so farthest cutouts are cut first, nearest-to-perimeter cut last
|
||||||
|
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
|
||||||
|
orderedCutouts.Reverse();
|
||||||
|
|
||||||
|
// Build output program: cutouts first (farthest to nearest), perimeter last
|
||||||
|
var result = new Program();
|
||||||
|
var currentPoint = exitPoint;
|
||||||
|
|
||||||
|
foreach (var cutout in orderedCutouts)
|
||||||
|
{
|
||||||
|
var contourType = DetectContourType(cutout);
|
||||||
|
var closestPt = cutout.ClosestPointTo(currentPoint, out var entity);
|
||||||
|
var normal = ComputeNormal(closestPt, entity, contourType);
|
||||||
|
var winding = DetermineWinding(cutout);
|
||||||
|
|
||||||
|
var leadIn = SelectLeadIn(contourType);
|
||||||
|
var leadOut = SelectLeadOut(contourType);
|
||||||
|
|
||||||
|
result.Codes.AddRange(leadIn.Generate(closestPt, normal, winding));
|
||||||
|
var reindexed = cutout.ReindexAt(closestPt, entity);
|
||||||
|
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
|
||||||
|
// TODO: MicrotabLeadOut — trim last cutting move by GapSize
|
||||||
|
result.Codes.AddRange(leadOut.Generate(closestPt, normal, winding));
|
||||||
|
|
||||||
|
currentPoint = closestPt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perimeter last
|
||||||
|
{
|
||||||
|
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
||||||
|
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
||||||
|
var winding = DetermineWinding(profile.Perimeter);
|
||||||
|
|
||||||
|
var leadIn = SelectLeadIn(ContourType.External);
|
||||||
|
var leadOut = SelectLeadOut(ContourType.External);
|
||||||
|
|
||||||
|
result.Codes.AddRange(leadIn.Generate(perimeterPt, normal, winding));
|
||||||
|
var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
|
||||||
|
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
|
||||||
|
// TODO: MicrotabLeadOut — trim last cutting move by GapSize
|
||||||
|
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector GetExitPoint(Plate plate)
|
||||||
|
{
|
||||||
|
var w = plate.Size.Width;
|
||||||
|
var l = plate.Size.Length;
|
||||||
|
|
||||||
|
return plate.Quadrant switch
|
||||||
|
{
|
||||||
|
1 => new Vector(w, l), // Q1 origin BottomLeft -> exit TopRight
|
||||||
|
2 => new Vector(0, l), // Q2 origin BottomRight -> exit TopLeft
|
||||||
|
3 => new Vector(0, 0), // Q3 origin TopRight -> exit BottomLeft
|
||||||
|
4 => new Vector(w, 0), // Q4 origin TopLeft -> exit BottomRight
|
||||||
|
_ => new Vector(w, l)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Shape> SequenceCutouts(List<Shape> cutouts, Vector startPoint)
|
||||||
|
{
|
||||||
|
var remaining = new List<Shape>(cutouts);
|
||||||
|
var ordered = new List<Shape>();
|
||||||
|
var currentPoint = startPoint;
|
||||||
|
|
||||||
|
while (remaining.Count > 0)
|
||||||
|
{
|
||||||
|
var nearest = remaining[0];
|
||||||
|
var nearestPt = nearest.ClosestPointTo(currentPoint);
|
||||||
|
var nearestDist = nearestPt.DistanceTo(currentPoint);
|
||||||
|
|
||||||
|
for (var i = 1; i < remaining.Count; i++)
|
||||||
|
{
|
||||||
|
var pt = remaining[i].ClosestPointTo(currentPoint);
|
||||||
|
var dist = pt.DistanceTo(currentPoint);
|
||||||
|
if (dist < nearestDist)
|
||||||
|
{
|
||||||
|
nearest = remaining[i];
|
||||||
|
nearestPt = pt;
|
||||||
|
nearestDist = dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ordered.Add(nearest);
|
||||||
|
remaining.Remove(nearest);
|
||||||
|
currentPoint = nearestPt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContourType DetectContourType(Shape cutout)
|
||||||
|
{
|
||||||
|
if (cutout.Entities.Count == 1 && cutout.Entities[0] is Circle)
|
||||||
|
return ContourType.ArcCircle;
|
||||||
|
|
||||||
|
return ContourType.Internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double ComputeNormal(Vector point, Entity entity, ContourType contourType)
|
||||||
|
{
|
||||||
|
double normal;
|
||||||
|
|
||||||
|
if (entity is Line line)
|
||||||
|
{
|
||||||
|
// Perpendicular to line direction
|
||||||
|
var tangent = line.EndPoint.AngleFrom(line.StartPoint);
|
||||||
|
normal = tangent + Math.Angle.HalfPI;
|
||||||
|
}
|
||||||
|
else if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
// Radial direction from center to point
|
||||||
|
normal = point.AngleFrom(arc.Center);
|
||||||
|
}
|
||||||
|
else if (entity is Circle circle)
|
||||||
|
{
|
||||||
|
normal = point.AngleFrom(circle.Center);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
normal = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For internal contours, flip the normal (point into scrap)
|
||||||
|
if (contourType == ContourType.Internal || contourType == ContourType.ArcCircle)
|
||||||
|
normal += System.Math.PI;
|
||||||
|
|
||||||
|
return Math.Angle.NormalizeRad(normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RotationType DetermineWinding(Shape shape)
|
||||||
|
{
|
||||||
|
// Use signed area: positive = CCW, negative = CW
|
||||||
|
var area = shape.Area();
|
||||||
|
return area >= 0 ? RotationType.CCW : RotationType.CW;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LeadIn SelectLeadIn(ContourType contourType)
|
||||||
|
{
|
||||||
|
return contourType switch
|
||||||
|
{
|
||||||
|
ContourType.ArcCircle => Parameters.ArcCircleLeadIn ?? Parameters.InternalLeadIn,
|
||||||
|
ContourType.Internal => Parameters.InternalLeadIn,
|
||||||
|
_ => Parameters.ExternalLeadIn
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private LeadOut SelectLeadOut(ContourType contourType)
|
||||||
|
{
|
||||||
|
return contourType switch
|
||||||
|
{
|
||||||
|
ContourType.ArcCircle => Parameters.ArcCircleLeadOut ?? Parameters.InternalLeadOut,
|
||||||
|
ContourType.Internal => Parameters.InternalLeadOut,
|
||||||
|
_ => Parameters.ExternalLeadOut
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint)
|
||||||
|
{
|
||||||
|
var moves = new List<ICode>();
|
||||||
|
|
||||||
|
foreach (var entity in shape.Entities)
|
||||||
|
{
|
||||||
|
if (entity is Line line)
|
||||||
|
{
|
||||||
|
moves.Add(new LinearMove(line.EndPoint));
|
||||||
|
}
|
||||||
|
else if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW));
|
||||||
|
}
|
||||||
|
else if (entity is Circle circle)
|
||||||
|
{
|
||||||
|
moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new System.InvalidOperationException($"Unsupported entity type: {entity.Type}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return moves;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
OpenNest.Core/CNC/CuttingStrategy/ContourType.cs
Normal file
9
OpenNest.Core/CNC/CuttingStrategy/ContourType.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public enum ContourType
|
||||||
|
{
|
||||||
|
External,
|
||||||
|
Internal,
|
||||||
|
ArcCircle
|
||||||
|
}
|
||||||
|
}
|
||||||
30
OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs
Normal file
30
OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class CuttingParameters
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string MachineName { get; set; }
|
||||||
|
public string MaterialName { get; set; }
|
||||||
|
public string Grade { get; set; }
|
||||||
|
public double Thickness { get; set; }
|
||||||
|
|
||||||
|
public double Kerf { get; set; }
|
||||||
|
public double PartSpacing { get; set; }
|
||||||
|
|
||||||
|
public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn();
|
||||||
|
public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut();
|
||||||
|
|
||||||
|
public LeadIn InternalLeadIn { get; set; } = new LineLeadIn { Length = 0.125, ApproachAngle = 90 };
|
||||||
|
public LeadOut InternalLeadOut { get; set; } = new NoLeadOut();
|
||||||
|
|
||||||
|
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
|
||||||
|
public LeadOut ArcCircleLeadOut { get; set; } = new NoLeadOut();
|
||||||
|
|
||||||
|
public Tab TabConfig { get; set; }
|
||||||
|
public bool TabsEnabled { get; set; }
|
||||||
|
|
||||||
|
public SequenceParameters Sequencing { get; set; } = new SequenceParameters();
|
||||||
|
public AssignmentParameters Assignment { get; set; } = new AssignmentParameters();
|
||||||
|
}
|
||||||
|
}
|
||||||
36
OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs
Normal file
36
OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class ArcLeadIn : LeadIn
|
||||||
|
{
|
||||||
|
public double Radius { get; set; }
|
||||||
|
|
||||||
|
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
|
var arcCenter = new Vector(
|
||||||
|
contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle),
|
||||||
|
contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle));
|
||||||
|
|
||||||
|
return new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(piercePoint),
|
||||||
|
new ArcMove(contourStartPoint, arcCenter, winding)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||||
|
{
|
||||||
|
var arcCenterX = contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle);
|
||||||
|
var arcCenterY = contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle);
|
||||||
|
|
||||||
|
return new Vector(
|
||||||
|
arcCenterX + Radius * System.Math.Cos(contourNormalAngle),
|
||||||
|
arcCenterY + Radius * System.Math.Sin(contourNormalAngle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs
Normal file
49
OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class CleanHoleLeadIn : LeadIn
|
||||||
|
{
|
||||||
|
public double LineLength { get; set; }
|
||||||
|
public double ArcRadius { get; set; }
|
||||||
|
public double Kerf { get; set; }
|
||||||
|
|
||||||
|
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
|
var arcCenterX = contourStartPoint.X + ArcRadius * System.Math.Cos(contourNormalAngle);
|
||||||
|
var arcCenterY = contourStartPoint.Y + ArcRadius * System.Math.Sin(contourNormalAngle);
|
||||||
|
var arcCenter = new Vector(arcCenterX, arcCenterY);
|
||||||
|
|
||||||
|
var lineAngle = contourNormalAngle + Angle.ToRadians(135.0);
|
||||||
|
var arcStart = new Vector(
|
||||||
|
arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
|
||||||
|
arcCenterY + ArcRadius * System.Math.Sin(lineAngle));
|
||||||
|
|
||||||
|
return new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(piercePoint),
|
||||||
|
new LinearMove(arcStart),
|
||||||
|
new ArcMove(contourStartPoint, arcCenter, winding)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||||
|
{
|
||||||
|
var arcCenterX = contourStartPoint.X + ArcRadius * System.Math.Cos(contourNormalAngle);
|
||||||
|
var arcCenterY = contourStartPoint.Y + ArcRadius * System.Math.Sin(contourNormalAngle);
|
||||||
|
|
||||||
|
var lineAngle = contourNormalAngle + Angle.ToRadians(135.0);
|
||||||
|
var arcStartX = arcCenterX + ArcRadius * System.Math.Cos(lineAngle);
|
||||||
|
var arcStartY = arcCenterY + ArcRadius * System.Math.Sin(lineAngle);
|
||||||
|
|
||||||
|
return new Vector(
|
||||||
|
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
||||||
|
arcStartY + LineLength * System.Math.Sin(lineAngle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs
Normal file
13
OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public abstract class LeadIn
|
||||||
|
{
|
||||||
|
public abstract List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW);
|
||||||
|
|
||||||
|
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs
Normal file
49
OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class LineArcLeadIn : LeadIn
|
||||||
|
{
|
||||||
|
public double LineLength { get; set; }
|
||||||
|
public double ApproachAngle { get; set; } = 135.0;
|
||||||
|
public double ArcRadius { get; set; }
|
||||||
|
|
||||||
|
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
|
var arcCenterX = contourStartPoint.X + ArcRadius * System.Math.Cos(contourNormalAngle);
|
||||||
|
var arcCenterY = contourStartPoint.Y + ArcRadius * System.Math.Sin(contourNormalAngle);
|
||||||
|
var arcCenter = new Vector(arcCenterX, arcCenterY);
|
||||||
|
|
||||||
|
var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
||||||
|
var arcStart = new Vector(
|
||||||
|
arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
|
||||||
|
arcCenterY + ArcRadius * System.Math.Sin(lineAngle));
|
||||||
|
|
||||||
|
return new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(piercePoint),
|
||||||
|
new LinearMove(arcStart),
|
||||||
|
new ArcMove(contourStartPoint, arcCenter, winding)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||||
|
{
|
||||||
|
var arcCenterX = contourStartPoint.X + ArcRadius * System.Math.Cos(contourNormalAngle);
|
||||||
|
var arcCenterY = contourStartPoint.Y + ArcRadius * System.Math.Sin(contourNormalAngle);
|
||||||
|
|
||||||
|
var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
||||||
|
var arcStartX = arcCenterX + ArcRadius * System.Math.Cos(lineAngle);
|
||||||
|
var arcStartY = arcCenterY + ArcRadius * System.Math.Sin(lineAngle);
|
||||||
|
|
||||||
|
return new Vector(
|
||||||
|
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
||||||
|
arcStartY + LineLength * System.Math.Sin(lineAngle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs
Normal file
32
OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class LineLeadIn : LeadIn
|
||||||
|
{
|
||||||
|
public double Length { get; set; }
|
||||||
|
public double ApproachAngle { get; set; } = 90.0;
|
||||||
|
|
||||||
|
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
|
return new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(piercePoint),
|
||||||
|
new LinearMove(contourStartPoint)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||||
|
{
|
||||||
|
var approachAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
||||||
|
return new Vector(
|
||||||
|
contourStartPoint.X + Length * System.Math.Cos(approachAngle),
|
||||||
|
contourStartPoint.Y + Length * System.Math.Sin(approachAngle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs
Normal file
44
OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class LineLineLeadIn : LeadIn
|
||||||
|
{
|
||||||
|
public double Length1 { get; set; }
|
||||||
|
public double ApproachAngle1 { get; set; } = 90.0;
|
||||||
|
public double Length2 { get; set; }
|
||||||
|
public double ApproachAngle2 { get; set; } = 90.0;
|
||||||
|
|
||||||
|
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
|
var secondAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle1);
|
||||||
|
var midPoint = new Vector(
|
||||||
|
contourStartPoint.X + Length2 * System.Math.Cos(secondAngle),
|
||||||
|
contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle));
|
||||||
|
|
||||||
|
return new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(piercePoint),
|
||||||
|
new LinearMove(midPoint),
|
||||||
|
new LinearMove(contourStartPoint)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||||
|
{
|
||||||
|
var secondAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle1);
|
||||||
|
var midX = contourStartPoint.X + Length2 * System.Math.Cos(secondAngle);
|
||||||
|
var midY = contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle);
|
||||||
|
|
||||||
|
var firstAngle = secondAngle + Angle.ToRadians(ApproachAngle2);
|
||||||
|
return new Vector(
|
||||||
|
midX + Length1 * System.Math.Cos(firstAngle),
|
||||||
|
midY + Length1 * System.Math.Sin(firstAngle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs
Normal file
22
OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class NoLeadIn : LeadIn
|
||||||
|
{
|
||||||
|
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
return new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(contourStartPoint)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||||
|
{
|
||||||
|
return contourStartPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs
Normal file
27
OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class ArcLeadOut : LeadOut
|
||||||
|
{
|
||||||
|
public double Radius { get; set; }
|
||||||
|
|
||||||
|
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
var arcCenterX = contourEndPoint.X + Radius * System.Math.Cos(contourNormalAngle);
|
||||||
|
var arcCenterY = contourEndPoint.Y + Radius * System.Math.Sin(contourNormalAngle);
|
||||||
|
var arcCenter = new Vector(arcCenterX, arcCenterY);
|
||||||
|
|
||||||
|
var endPoint = new Vector(
|
||||||
|
arcCenterX + Radius * System.Math.Cos(contourNormalAngle + System.Math.PI / 2),
|
||||||
|
arcCenterY + Radius * System.Math.Sin(contourNormalAngle + System.Math.PI / 2));
|
||||||
|
|
||||||
|
return new List<ICode>
|
||||||
|
{
|
||||||
|
new ArcMove(endPoint, arcCenter, winding)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs
Normal file
11
OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public abstract class LeadOut
|
||||||
|
{
|
||||||
|
public abstract List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs
Normal file
26
OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class LineLeadOut : LeadOut
|
||||||
|
{
|
||||||
|
public double Length { get; set; }
|
||||||
|
public double ApproachAngle { get; set; } = 90.0;
|
||||||
|
|
||||||
|
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
var overcutAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
||||||
|
var endPoint = new Vector(
|
||||||
|
contourEndPoint.X + Length * System.Math.Cos(overcutAngle),
|
||||||
|
contourEndPoint.Y + Length * System.Math.Sin(overcutAngle));
|
||||||
|
|
||||||
|
return new List<ICode>
|
||||||
|
{
|
||||||
|
new LinearMove(endPoint)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class MicrotabLeadOut : LeadOut
|
||||||
|
{
|
||||||
|
public double GapSize { get; set; } = 0.03;
|
||||||
|
|
||||||
|
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
return new List<ICode>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs
Normal file
14
OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class NoLeadOut : LeadOut
|
||||||
|
{
|
||||||
|
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
return new List<ICode>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
OpenNest.Core/CNC/CuttingStrategy/SequenceParameters.cs
Normal file
27
OpenNest.Core/CNC/CuttingStrategy/SequenceParameters.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
// Values match PEP Technology's numbering scheme (value 6 intentionally skipped)
|
||||||
|
public enum SequenceMethod
|
||||||
|
{
|
||||||
|
RightSide = 1,
|
||||||
|
LeastCode = 2,
|
||||||
|
Advanced = 3,
|
||||||
|
BottomSide = 4,
|
||||||
|
EdgeStart = 5,
|
||||||
|
LeftSide = 7,
|
||||||
|
RightSideAlt = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SequenceParameters
|
||||||
|
{
|
||||||
|
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
|
||||||
|
public double SmallCutoutWidth { get; set; } = 1.5;
|
||||||
|
public double SmallCutoutHeight { get; set; } = 1.5;
|
||||||
|
public double MediumCutoutWidth { get; set; } = 8.0;
|
||||||
|
public double MediumCutoutHeight { get; set; } = 8.0;
|
||||||
|
public double DistanceMediumSmall { get; set; }
|
||||||
|
public bool AlternateRowsColumns { get; set; } = true;
|
||||||
|
public bool AlternateCutoutsWithinRowColumn { get; set; } = true;
|
||||||
|
public double MinDistanceBetweenRowsColumns { get; set; } = 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs
Normal file
34
OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class BreakerTab : Tab
|
||||||
|
{
|
||||||
|
public double BreakerDepth { get; set; }
|
||||||
|
public double BreakerLeadInLength { get; set; }
|
||||||
|
public double BreakerAngle { get; set; }
|
||||||
|
|
||||||
|
public override List<ICode> Generate(
|
||||||
|
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
var codes = new List<ICode>();
|
||||||
|
|
||||||
|
if (TabLeadOut != null)
|
||||||
|
codes.AddRange(TabLeadOut.Generate(tabStartPoint, contourNormalAngle, winding));
|
||||||
|
|
||||||
|
var scoreAngle = contourNormalAngle + System.Math.PI;
|
||||||
|
var scoreEnd = new Vector(
|
||||||
|
tabStartPoint.X + BreakerDepth * System.Math.Cos(scoreAngle),
|
||||||
|
tabStartPoint.Y + BreakerDepth * System.Math.Sin(scoreAngle));
|
||||||
|
codes.Add(new LinearMove(scoreEnd));
|
||||||
|
codes.Add(new RapidMove(tabEndPoint));
|
||||||
|
|
||||||
|
if (TabLeadIn != null)
|
||||||
|
codes.AddRange(TabLeadIn.Generate(tabEndPoint, contourNormalAngle, winding));
|
||||||
|
|
||||||
|
return codes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs
Normal file
20
OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class MachineTab : Tab
|
||||||
|
{
|
||||||
|
public int MachineTabId { get; set; }
|
||||||
|
|
||||||
|
public override List<ICode> Generate(
|
||||||
|
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
return new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(tabEndPoint)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs
Normal file
36
OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public class NormalTab : Tab
|
||||||
|
{
|
||||||
|
public double CutoutMinWidth { get; set; }
|
||||||
|
public double CutoutMinHeight { get; set; }
|
||||||
|
public double CutoutMaxWidth { get; set; }
|
||||||
|
public double CutoutMaxHeight { get; set; }
|
||||||
|
|
||||||
|
public override List<ICode> Generate(
|
||||||
|
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
|
{
|
||||||
|
var codes = new List<ICode>();
|
||||||
|
|
||||||
|
if (TabLeadOut != null)
|
||||||
|
codes.AddRange(TabLeadOut.Generate(tabStartPoint, contourNormalAngle, winding));
|
||||||
|
|
||||||
|
codes.Add(new RapidMove(tabEndPoint));
|
||||||
|
|
||||||
|
if (TabLeadIn != null)
|
||||||
|
codes.AddRange(TabLeadIn.Generate(tabEndPoint, contourNormalAngle, winding));
|
||||||
|
|
||||||
|
return codes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AppliesToCutout(double cutoutWidth, double cutoutHeight)
|
||||||
|
{
|
||||||
|
return cutoutWidth >= CutoutMinWidth && cutoutWidth <= CutoutMaxWidth
|
||||||
|
&& cutoutHeight >= CutoutMinHeight && cutoutHeight <= CutoutMaxHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs
Normal file
16
OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public abstract class Tab
|
||||||
|
{
|
||||||
|
public double Size { get; set; } = 0.03;
|
||||||
|
public LeadIn TabLeadIn { get; set; }
|
||||||
|
public LeadOut TabLeadOut { get; set; }
|
||||||
|
|
||||||
|
public abstract List<ICode> Generate(
|
||||||
|
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -155,6 +155,28 @@ namespace OpenNest.Geometry
|
|||||||
Center.Y + Radius * System.Math.Sin(EndAngle));
|
Center.Y + Radius * System.Math.Sin(EndAngle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Splits the arc at the given point, returning two sub-arcs.
|
||||||
|
/// Either half may be null if the split point coincides with an endpoint.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="point">The point at which to split the arc.</param>
|
||||||
|
/// <returns>A tuple of (first, second) sub-arcs.</returns>
|
||||||
|
public (Arc first, Arc second) SplitAt(Vector point)
|
||||||
|
{
|
||||||
|
if (point.DistanceTo(StartPoint()) < Tolerance.Epsilon)
|
||||||
|
return (null, new Arc(Center, Radius, StartAngle, EndAngle, IsReversed));
|
||||||
|
|
||||||
|
if (point.DistanceTo(EndPoint()) < Tolerance.Epsilon)
|
||||||
|
return (new Arc(Center, Radius, StartAngle, EndAngle, IsReversed), null);
|
||||||
|
|
||||||
|
var splitAngle = Angle.NormalizeRad(Center.AngleTo(point));
|
||||||
|
|
||||||
|
var firstArc = new Arc(Center, Radius, StartAngle, splitAngle, IsReversed);
|
||||||
|
var secondArc = new Arc(Center, Radius, splitAngle, EndAngle, IsReversed);
|
||||||
|
|
||||||
|
return (firstArc, secondArc);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true if the given arc has the same center point and radius as this.
|
/// Returns true if the given arc has the same center point and radius as this.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -388,7 +410,7 @@ namespace OpenNest.Geometry
|
|||||||
boundingBox.X = minX;
|
boundingBox.X = minX;
|
||||||
boundingBox.Y = minY;
|
boundingBox.Y = minY;
|
||||||
boundingBox.Width = maxX - minX;
|
boundingBox.Width = maxX - minX;
|
||||||
boundingBox.Height = maxY - minY;
|
boundingBox.Length = maxY - minY;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace OpenNest.Geometry
|
|||||||
double minX = boxes[0].X;
|
double minX = boxes[0].X;
|
||||||
double minY = boxes[0].Y;
|
double minY = boxes[0].Y;
|
||||||
double maxX = boxes[0].X + boxes[0].Width;
|
double maxX = boxes[0].X + boxes[0].Width;
|
||||||
double maxY = boxes[0].Y + boxes[0].Height;
|
double maxY = boxes[0].Y + boxes[0].Length;
|
||||||
|
|
||||||
foreach (var box in boxes)
|
foreach (var box in boxes)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
Location = new Vector(x, y);
|
Location = new Vector(x, y);
|
||||||
Width = w;
|
Width = w;
|
||||||
Height = h;
|
Length = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector Location;
|
public Vector Location;
|
||||||
|
|
||||||
public Vector Center
|
public Vector Center
|
||||||
{
|
{
|
||||||
get { return new Vector(X + Width * 0.5, Y + Height * 0.5); }
|
get { return new Vector(X + Width * 0.5, Y + Length * 0.5); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public Size Size;
|
public Size Size;
|
||||||
@@ -45,10 +45,10 @@ namespace OpenNest.Geometry
|
|||||||
set { Size.Width = value; }
|
set { Size.Width = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Height
|
public double Length
|
||||||
{
|
{
|
||||||
get { return Size.Height; }
|
get { return Size.Length; }
|
||||||
set { Size.Height = value; }
|
set { Size.Length = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MoveTo(double x, double y)
|
public void MoveTo(double x, double y)
|
||||||
@@ -86,7 +86,7 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public double Top
|
public double Top
|
||||||
{
|
{
|
||||||
get { return Y + Height; }
|
get { return Y + Length; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Bottom
|
public double Bottom
|
||||||
@@ -96,12 +96,12 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public double Area()
|
public double Area()
|
||||||
{
|
{
|
||||||
return Width * Height;
|
return Width * Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Perimeter()
|
public double Perimeter()
|
||||||
{
|
{
|
||||||
return Width * 2 + Height * 2;
|
return Width * 2 + Length * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Intersects(Box box)
|
public bool Intersects(Box box)
|
||||||
@@ -197,12 +197,12 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public Box Offset(double d)
|
public Box Offset(double d)
|
||||||
{
|
{
|
||||||
return new Box(X - d, Y - d, Width + d * 2, Height + d * 2);
|
return new Box(X - d, Y - d, Width + d * 2, Length + d * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format("[Box: X={0}, Y={1}, Width={2}, Height={3}]", X, Y, Width, Height);
|
return string.Format("[Box: X={0}, Y={1}, Width={2}, Length={3}]", X, Y, Width, Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
var x = large.Left;
|
var x = large.Left;
|
||||||
var y = large.Bottom;
|
var y = large.Bottom;
|
||||||
var w = small.Left - x;
|
var w = small.Left - x;
|
||||||
var h = large.Height;
|
var h = large.Length;
|
||||||
|
|
||||||
return new Box(x, y, w, h);
|
return new Box(x, y, w, h);
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
var x = small.Right;
|
var x = small.Right;
|
||||||
var y = large.Bottom;
|
var y = large.Bottom;
|
||||||
var w = large.Right - x;
|
var w = large.Right - x;
|
||||||
var h = large.Height;
|
var h = large.Length;
|
||||||
|
|
||||||
return new Box(x, y, w, h);
|
return new Box(x, y, w, h);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ namespace OpenNest.Geometry
|
|||||||
boundingBox.X = Center.X - Radius;
|
boundingBox.X = Center.X - Radius;
|
||||||
boundingBox.Y = Center.Y - Radius;
|
boundingBox.Y = Center.Y - Radius;
|
||||||
boundingBox.Width = Diameter;
|
boundingBox.Width = Diameter;
|
||||||
boundingBox.Height = Diameter;
|
boundingBox.Length = Diameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
|
|||||||
@@ -381,12 +381,12 @@ namespace OpenNest.Geometry
|
|||||||
if (StartPoint.Y < EndPoint.Y)
|
if (StartPoint.Y < EndPoint.Y)
|
||||||
{
|
{
|
||||||
boundingBox.Y = StartPoint.Y;
|
boundingBox.Y = StartPoint.Y;
|
||||||
boundingBox.Height = EndPoint.Y - StartPoint.Y;
|
boundingBox.Length = EndPoint.Y - StartPoint.Y;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
boundingBox.Y = EndPoint.Y;
|
boundingBox.Y = EndPoint.Y;
|
||||||
boundingBox.Height = StartPoint.Y - EndPoint.Y;
|
boundingBox.Length = StartPoint.Y - EndPoint.Y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +414,25 @@ namespace OpenNest.Geometry
|
|||||||
return OffsetEntity(distance, side);
|
return OffsetEntity(distance, side);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Splits the line at the given point, returning two sub-lines.
|
||||||
|
/// Either half may be null if the split point coincides with an endpoint.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="point">The point at which to split the line.</param>
|
||||||
|
/// <returns>A tuple of (first, second) sub-lines.</returns>
|
||||||
|
public (Line first, Line second) SplitAt(Vector point)
|
||||||
|
{
|
||||||
|
var first = point.DistanceTo(StartPoint) < Tolerance.Epsilon
|
||||||
|
? null
|
||||||
|
: new Line(StartPoint, point);
|
||||||
|
|
||||||
|
var second = point.DistanceTo(EndPoint) < Tolerance.Epsilon
|
||||||
|
? null
|
||||||
|
: new Line(point, EndPoint);
|
||||||
|
|
||||||
|
return (first, second);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the closest point on the line to the given point.
|
/// Gets the closest point on the line to the given point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ namespace OpenNest.Geometry
|
|||||||
boundingBox.X = minX;
|
boundingBox.X = minX;
|
||||||
boundingBox.Y = minY;
|
boundingBox.Y = minY;
|
||||||
boundingBox.Width = maxX - minX;
|
boundingBox.Width = maxX - minX;
|
||||||
boundingBox.Height = maxY - minY;
|
boundingBox.Length = maxY - minY;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
|
|||||||
@@ -201,6 +201,68 @@ namespace OpenNest.Geometry
|
|||||||
return closestPt;
|
return closestPt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a new shape with entities reordered so that the given point on
|
||||||
|
/// the given entity becomes the new start point of the contour.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="point">The point on the entity to reindex at.</param>
|
||||||
|
/// <param name="entity">The entity containing the point.</param>
|
||||||
|
/// <returns>A new reindexed shape.</returns>
|
||||||
|
public Shape ReindexAt(Vector point, Entity entity)
|
||||||
|
{
|
||||||
|
// Circle case: return a new shape with just the circle
|
||||||
|
if (entity is Circle)
|
||||||
|
{
|
||||||
|
var result = new Shape();
|
||||||
|
result.Entities.Add(entity);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = Entities.IndexOf(entity);
|
||||||
|
if (i < 0)
|
||||||
|
throw new ArgumentException("Entity not found in shape", nameof(entity));
|
||||||
|
|
||||||
|
// Split the entity at the point
|
||||||
|
Entity firstHalf = null;
|
||||||
|
Entity secondHalf = null;
|
||||||
|
|
||||||
|
if (entity is Line line)
|
||||||
|
{
|
||||||
|
var (f, s) = line.SplitAt(point);
|
||||||
|
firstHalf = f;
|
||||||
|
secondHalf = s;
|
||||||
|
}
|
||||||
|
else if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
var (f, s) = arc.SplitAt(point);
|
||||||
|
firstHalf = f;
|
||||||
|
secondHalf = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build reindexed entity list
|
||||||
|
var entities = new List<Entity>();
|
||||||
|
|
||||||
|
// secondHalf of split entity (if not null)
|
||||||
|
if (secondHalf != null)
|
||||||
|
entities.Add(secondHalf);
|
||||||
|
|
||||||
|
// Entities after the split index (wrapping)
|
||||||
|
for (var j = i + 1; j < Entities.Count; j++)
|
||||||
|
entities.Add(Entities[j]);
|
||||||
|
|
||||||
|
// Entities before the split index (wrapping)
|
||||||
|
for (var j = 0; j < i; j++)
|
||||||
|
entities.Add(Entities[j]);
|
||||||
|
|
||||||
|
// firstHalf of split entity (if not null)
|
||||||
|
if (firstHalf != null)
|
||||||
|
entities.Add(firstHalf);
|
||||||
|
|
||||||
|
var reindexed = new Shape();
|
||||||
|
reindexed.Entities.AddRange(entities);
|
||||||
|
return reindexed;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts the shape to a polygon.
|
/// Converts the shape to a polygon.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -399,7 +461,7 @@ namespace OpenNest.Geometry
|
|||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
{
|
{
|
||||||
var offsetShape = new Shape();
|
var offsetShape = new Shape();
|
||||||
var definedShape = new DefinedShape(this);
|
var definedShape = new ShapeProfile(this);
|
||||||
|
|
||||||
Entity lastEntity = null;
|
Entity lastEntity = null;
|
||||||
Entity lastOffsetEntity = null;
|
Entity lastOffsetEntity = null;
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
public class DefinedShape
|
public class ShapeProfile
|
||||||
{
|
{
|
||||||
public DefinedShape(Shape shape)
|
public ShapeProfile(Shape shape)
|
||||||
{
|
{
|
||||||
Update(shape.Entities);
|
Update(shape.Entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefinedShape(List<Entity> entities)
|
public ShapeProfile(List<Entity> entities)
|
||||||
{
|
{
|
||||||
Update(entities);
|
Update(entities);
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
public struct Size
|
public struct Size
|
||||||
{
|
{
|
||||||
public Size(double width, double height)
|
public Size(double width, double length)
|
||||||
{
|
{
|
||||||
Height = height;
|
Length = length;
|
||||||
Width = width;
|
Width = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Height;
|
public double Length;
|
||||||
|
|
||||||
public double Width;
|
public double Width;
|
||||||
|
|
||||||
@@ -21,10 +21,10 @@ namespace OpenNest.Geometry
|
|||||||
if (a.Length > 2)
|
if (a.Length > 2)
|
||||||
throw new FormatException("Invalid size format.");
|
throw new FormatException("Invalid size format.");
|
||||||
|
|
||||||
var height = double.Parse(a[0]);
|
var length = double.Parse(a[0]);
|
||||||
var width = double.Parse(a[1]);
|
var width = double.Parse(a[1]);
|
||||||
|
|
||||||
return new Size(width, height);
|
return new Size(width, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryParse(string s, out Size size)
|
public static bool TryParse(string s, out Size size)
|
||||||
@@ -44,12 +44,12 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format("{0} x {1}", Height, Width);
|
return string.Format("{0} x {1}", Length, Width);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ToString(int decimalPlaces)
|
public string ToString(int decimalPlaces)
|
||||||
{
|
{
|
||||||
return string.Format("{0} x {1}", System.Math.Round(Height, decimalPlaces), System.Math.Round(Width, decimalPlaces));
|
return string.Format("{0} x {1}", System.Math.Round(Length, decimalPlaces), System.Math.Round(Width, decimalPlaces));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ namespace OpenNest
|
|||||||
var part = new Part(BaseDrawing, clonedProgram,
|
var part = new Part(BaseDrawing, clonedProgram,
|
||||||
location + offset,
|
location + offset,
|
||||||
new Box(BoundingBox.X + offset.X, BoundingBox.Y + offset.Y,
|
new Box(BoundingBox.X + offset.X, BoundingBox.Y + offset.Y,
|
||||||
BoundingBox.Width, BoundingBox.Height));
|
BoundingBox.Width, BoundingBox.Length));
|
||||||
|
|
||||||
return part;
|
return part;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
const double oneAndHalfPI = System.Math.PI * 1.5;
|
const double oneAndHalfPI = System.Math.PI * 1.5;
|
||||||
|
|
||||||
Size = new Size(Size.Height, Size.Width);
|
Size = new Size(Size.Length, Size.Width);
|
||||||
|
|
||||||
if (rotationDirection == RotationType.CW)
|
if (rotationDirection == RotationType.CW)
|
||||||
{
|
{
|
||||||
@@ -128,7 +128,7 @@ namespace OpenNest
|
|||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
Offset(0, Size.Height);
|
Offset(0, Size.Length);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
@@ -136,7 +136,7 @@ namespace OpenNest
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
Offset(0, -Size.Height);
|
Offset(0, -Size.Length);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
@@ -165,7 +165,7 @@ namespace OpenNest
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
Offset(0, Size.Height);
|
Offset(0, Size.Length);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
@@ -173,7 +173,7 @@ namespace OpenNest
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
Offset(0, -Size.Height);
|
Offset(0, -Size.Length);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -200,19 +200,19 @@ namespace OpenNest
|
|||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
centerpt = new Vector(Size.Width * 0.5, Size.Height * 0.5);
|
centerpt = new Vector(Size.Width * 0.5, Size.Length * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
centerpt = new Vector(-Size.Width * 0.5, Size.Height * 0.5);
|
centerpt = new Vector(-Size.Width * 0.5, Size.Length * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
centerpt = new Vector(-Size.Width * 0.5, -Size.Height * 0.5);
|
centerpt = new Vector(-Size.Width * 0.5, -Size.Length * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
centerpt = new Vector(Size.Width * 0.5, -Size.Height * 0.5);
|
centerpt = new Vector(Size.Width * 0.5, -Size.Length * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -308,12 +308,12 @@ namespace OpenNest
|
|||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
plateBox.X = (float)-Size.Width;
|
plateBox.X = (float)-Size.Width;
|
||||||
plateBox.Y = (float)-Size.Height;
|
plateBox.Y = (float)-Size.Length;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
plateBox.X = 0;
|
plateBox.X = 0;
|
||||||
plateBox.Y = (float)-Size.Height;
|
plateBox.Y = (float)-Size.Length;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -321,7 +321,7 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
|
|
||||||
plateBox.Width = Size.Width;
|
plateBox.Width = Size.Width;
|
||||||
plateBox.Height = Size.Height;
|
plateBox.Length = Size.Length;
|
||||||
|
|
||||||
if (!includeParts)
|
if (!includeParts)
|
||||||
return plateBox;
|
return plateBox;
|
||||||
@@ -341,7 +341,7 @@ namespace OpenNest
|
|||||||
? partsBox.Right - boundingBox.X
|
? partsBox.Right - boundingBox.X
|
||||||
: plateBox.Right - boundingBox.X;
|
: plateBox.Right - boundingBox.X;
|
||||||
|
|
||||||
boundingBox.Height = partsBox.Top > plateBox.Top
|
boundingBox.Length = partsBox.Top > plateBox.Top
|
||||||
? partsBox.Top - boundingBox.Y
|
? partsBox.Top - boundingBox.Y
|
||||||
: plateBox.Top - boundingBox.Y;
|
: plateBox.Top - boundingBox.Y;
|
||||||
|
|
||||||
@@ -359,7 +359,7 @@ namespace OpenNest
|
|||||||
box.X += EdgeSpacing.Left;
|
box.X += EdgeSpacing.Left;
|
||||||
box.Y += EdgeSpacing.Bottom;
|
box.Y += EdgeSpacing.Bottom;
|
||||||
box.Width -= EdgeSpacing.Left + EdgeSpacing.Right;
|
box.Width -= EdgeSpacing.Left + EdgeSpacing.Right;
|
||||||
box.Height -= EdgeSpacing.Top + EdgeSpacing.Bottom;
|
box.Length -= EdgeSpacing.Top + EdgeSpacing.Bottom;
|
||||||
|
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
@@ -383,28 +383,28 @@ namespace OpenNest
|
|||||||
var bounds = Parts.GetBoundingBox();
|
var bounds = Parts.GetBoundingBox();
|
||||||
|
|
||||||
double width;
|
double width;
|
||||||
double height;
|
double length;
|
||||||
|
|
||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
||||||
height = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
length = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
||||||
height = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
length = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
||||||
height = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
length = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
||||||
height = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
length = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -413,7 +413,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
Size = new Size(
|
Size = new Size(
|
||||||
Helper.RoundUpToNearest(width, roundingFactor),
|
Helper.RoundUpToNearest(width, roundingFactor),
|
||||||
Helper.RoundUpToNearest(height, roundingFactor));
|
Helper.RoundUpToNearest(length, roundingFactor));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -422,7 +422,7 @@ namespace OpenNest
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public double Area()
|
public double Area()
|
||||||
{
|
{
|
||||||
return Size.Width * Size.Height;
|
return Size.Width * Size.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -503,7 +503,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
if (maxRight < work.Right)
|
if (maxRight < work.Right)
|
||||||
{
|
{
|
||||||
var strip = new Box(maxRight, work.Bottom, work.Right - maxRight, work.Height);
|
var strip = new Box(maxRight, work.Bottom, work.Right - maxRight, work.Length);
|
||||||
if (strip.Area() > 1.0)
|
if (strip.Area() > 1.0)
|
||||||
results.Add(strip);
|
results.Add(strip);
|
||||||
}
|
}
|
||||||
@@ -548,7 +548,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
if (minLeft > work.Left)
|
if (minLeft > work.Left)
|
||||||
{
|
{
|
||||||
var strip = new Box(work.Left, work.Bottom, minLeft - work.Left, work.Height);
|
var strip = new Box(work.Left, work.Bottom, minLeft - work.Left, work.Length);
|
||||||
if (strip.Area() > 1.0)
|
if (strip.Area() > 1.0)
|
||||||
results.Add(strip);
|
results.Add(strip);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
var combinedBox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
var combinedBox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||||
bestArea = combinedBox.Area();
|
bestArea = combinedBox.Area();
|
||||||
bestWidth = combinedBox.Width;
|
bestWidth = combinedBox.Width;
|
||||||
bestHeight = combinedBox.Height;
|
bestHeight = combinedBox.Length;
|
||||||
bestRotation = 0;
|
bestRotation = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,15 +64,15 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
if (isHorizontalPush)
|
if (isHorizontalPush)
|
||||||
{
|
{
|
||||||
perpMin = -(bbox2.Height + spacing);
|
perpMin = -(bbox2.Length + spacing);
|
||||||
perpMax = bbox1.Height + bbox2.Height + spacing;
|
perpMax = bbox1.Length + bbox2.Length + spacing;
|
||||||
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
|
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
perpMin = -(bbox2.Width + spacing);
|
perpMin = -(bbox2.Width + spacing);
|
||||||
perpMax = bbox1.Width + bbox2.Width + spacing;
|
perpMax = bbox1.Width + bbox2.Width + spacing;
|
||||||
pushStartOffset = bbox1.Height + bbox2.Height + spacing * 2;
|
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-compute part1's offset lines (half-spacing outward)
|
// Pre-compute part1's offset lines (half-spacing outward)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace OpenNest.Engine.BestFit.Tiling
|
|||||||
public TileResult Evaluate(BestFitResult bestFit, Plate plate)
|
public TileResult Evaluate(BestFitResult bestFit, Plate plate)
|
||||||
{
|
{
|
||||||
var plateWidth = plate.Size.Width - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right;
|
var plateWidth = plate.Size.Width - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right;
|
||||||
var plateHeight = plate.Size.Height - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom;
|
var plateHeight = plate.Size.Length - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom;
|
||||||
|
|
||||||
var result1 = TryTile(bestFit, plateWidth, plateHeight, false);
|
var result1 = TryTile(bestFit, plateWidth, plateHeight, false);
|
||||||
var result2 = TryTile(bestFit, plateWidth, plateHeight, true);
|
var result2 = TryTile(bestFit, plateWidth, plateHeight, true);
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ namespace OpenNest.CirclePacking
|
|||||||
Bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
|
Bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
|
||||||
Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon);
|
Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon);
|
||||||
|
|
||||||
var rows = System.Math.Floor((Bin.Height + Tolerance.Epsilon) / (item.Diameter));
|
var rows = System.Math.Floor((Bin.Length + Tolerance.Epsilon) / (item.Diameter));
|
||||||
|
|
||||||
var diameter = item.Diameter;
|
var diameter = item.Diameter;
|
||||||
var remaining = Bin.Height - diameter * rows;
|
var remaining = Bin.Length - diameter * rows;
|
||||||
var radius = diameter * 0.5;
|
var radius = diameter * 0.5;
|
||||||
|
|
||||||
if (remaining < radius)
|
if (remaining < radius)
|
||||||
@@ -47,7 +47,7 @@ namespace OpenNest.CirclePacking
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var yoffset = (Bin.Height - diameter) / (2 * rows - 1);
|
var yoffset = (Bin.Length - diameter) / (2 * rows - 1);
|
||||||
var xoffset = Trigonometry.Base(yoffset, diameter);
|
var xoffset = Trigonometry.Base(yoffset, diameter);
|
||||||
|
|
||||||
var yodd = Bin.Y + yoffset;
|
var yodd = Bin.Y + yoffset;
|
||||||
|
|||||||
@@ -71,12 +71,12 @@ namespace OpenNest.CirclePacking
|
|||||||
Bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
|
Bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
|
||||||
Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon);
|
Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon);
|
||||||
|
|
||||||
var count = System.Math.Floor((bin.Height + Tolerance.Epsilon) / item.Diameter);
|
var count = System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Diameter);
|
||||||
|
|
||||||
if (count == 0)
|
if (count == 0)
|
||||||
return bin;
|
return bin;
|
||||||
|
|
||||||
var yoffset = (bin.Height - item.Diameter) / (count - 1);
|
var yoffset = (bin.Length - item.Diameter) / (count - 1);
|
||||||
var xoffset = Trigonometry.Base(yoffset * 0.5, item.Diameter);
|
var xoffset = Trigonometry.Base(yoffset * 0.5, item.Diameter);
|
||||||
|
|
||||||
int column = 0;
|
int column = 0;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace OpenNest
|
|||||||
public FillLinear(Box workArea, double partSpacing)
|
public FillLinear(Box workArea, double partSpacing)
|
||||||
{
|
{
|
||||||
PartSpacing = partSpacing;
|
PartSpacing = partSpacing;
|
||||||
WorkArea = new Box(workArea.X, workArea.Y, workArea.Width, workArea.Height);
|
WorkArea = new Box(workArea.X, workArea.Y, workArea.Width, workArea.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Box WorkArea { get; }
|
public Box WorkArea { get; }
|
||||||
@@ -34,7 +34,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
private static double GetDimension(Box box, NestDirection direction)
|
private static double GetDimension(Box box, NestDirection direction)
|
||||||
{
|
{
|
||||||
return direction == NestDirection.Horizontal ? box.Width : box.Height;
|
return direction == NestDirection.Horizontal ? box.Width : box.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double GetStart(Box box, NestDirection direction)
|
private static double GetStart(Box box, NestDirection direction)
|
||||||
@@ -321,7 +321,7 @@ namespace OpenNest
|
|||||||
template.Offset(WorkArea.Location - template.BoundingBox.Location);
|
template.Offset(WorkArea.Location - template.BoundingBox.Location);
|
||||||
|
|
||||||
if (template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
|
if (template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
|
||||||
template.BoundingBox.Height > WorkArea.Height + Tolerance.Epsilon)
|
template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
|
||||||
return pattern;
|
return pattern;
|
||||||
|
|
||||||
pattern.Parts.Add(template);
|
pattern.Parts.Add(template);
|
||||||
@@ -472,7 +472,7 @@ namespace OpenNest
|
|||||||
if (width <= Tolerance.Epsilon)
|
if (width <= Tolerance.Epsilon)
|
||||||
return new List<Part>();
|
return new List<Part>();
|
||||||
|
|
||||||
remainingStrip = new Box(left, WorkArea.Y, width, WorkArea.Height);
|
remainingStrip = new Box(left, WorkArea.Y, width, WorkArea.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build rotation set: always try cardinal orientations (0° and 90°),
|
// Build rotation set: always try cardinal orientations (0° and 90°),
|
||||||
@@ -601,7 +601,7 @@ namespace OpenNest
|
|||||||
var basePattern = pattern.Clone(offset);
|
var basePattern = pattern.Clone(offset);
|
||||||
|
|
||||||
if (basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
|
if (basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
|
||||||
basePattern.BoundingBox.Height > WorkArea.Height + Tolerance.Epsilon)
|
basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
|
||||||
return new List<Part>();
|
return new List<Part>();
|
||||||
|
|
||||||
return FillRecursive(basePattern, primaryAxis, depth: 0);
|
return FillRecursive(basePattern, primaryAxis, depth: 0);
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ namespace OpenNest
|
|||||||
if (maxRight < workArea.Right)
|
if (maxRight < workArea.Right)
|
||||||
{
|
{
|
||||||
var width = workArea.Right - maxRight;
|
var width = workArea.Right - maxRight;
|
||||||
var height = workArea.Height;
|
var height = workArea.Length;
|
||||||
|
|
||||||
if (System.Math.Min(width, height) >= MinRemnantDimension)
|
if (System.Math.Min(width, height) >= MinRemnantDimension)
|
||||||
largest = System.Math.Max(largest, width * height);
|
largest = System.Math.Max(largest, width * height);
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ namespace OpenNest
|
|||||||
|
|
||||||
testPart.UpdateBounds();
|
testPart.UpdateBounds();
|
||||||
|
|
||||||
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Height);
|
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
|
||||||
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Height);
|
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
||||||
|
|
||||||
if (workAreaShortSide < partLongestSide)
|
if (workAreaShortSide < partLongestSide)
|
||||||
{
|
{
|
||||||
@@ -113,7 +113,7 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bestLinearScore = best != null ? FillScore.Compute(best, workArea) : default;
|
var bestLinearScore = best != null ? FillScore.Compute(best, workArea) : default;
|
||||||
Debug.WriteLine($"[FindBestFill] Linear: {bestLinearScore.Count} parts, density={bestLinearScore.Density:P1} | WorkArea: {workArea.Width:F1}x{workArea.Height:F1} | Angles: {angles.Count}");
|
Debug.WriteLine($"[FindBestFill] Linear: {bestLinearScore.Count} parts, density={bestLinearScore.Density:P1} | WorkArea: {workArea.Width:F1}x{workArea.Length:F1} | Angles: {angles.Count}");
|
||||||
|
|
||||||
// Try rectangle best-fit (mixes orientations to fill remnant strips).
|
// Try rectangle best-fit (mixes orientations to fill remnant strips).
|
||||||
var rectResult = FillRectangleBestFit(item, workArea);
|
var rectResult = FillRectangleBestFit(item, workArea);
|
||||||
@@ -143,7 +143,7 @@ namespace OpenNest
|
|||||||
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
|
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
|
||||||
var best = FillPattern(engine, groupParts, angles, workArea);
|
var best = FillPattern(engine, groupParts, angles, workArea);
|
||||||
|
|
||||||
Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}");
|
Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
|
||||||
|
|
||||||
if (groupParts.Count == 1)
|
if (groupParts.Count == 1)
|
||||||
{
|
{
|
||||||
@@ -213,7 +213,7 @@ namespace OpenNest
|
|||||||
private List<Part> FillWithPairs(NestItem item, Box workArea)
|
private List<Part> FillWithPairs(NestItem item, Box workArea)
|
||||||
{
|
{
|
||||||
var bestFits = BestFitCache.GetOrCompute(
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
item.Drawing, Plate.Size.Width, Plate.Size.Height,
|
item.Drawing, Plate.Size.Width, Plate.Size.Length,
|
||||||
Plate.PartSpacing);
|
Plate.PartSpacing);
|
||||||
|
|
||||||
var candidates = SelectPairCandidates(bestFits, workArea);
|
var candidates = SelectPairCandidates(bestFits, workArea);
|
||||||
@@ -260,8 +260,8 @@ namespace OpenNest
|
|||||||
var kept = bestFits.Where(r => r.Keep).ToList();
|
var kept = bestFits.Where(r => r.Keep).ToList();
|
||||||
var top = kept.Take(50).ToList();
|
var top = kept.Take(50).ToList();
|
||||||
|
|
||||||
var workShortSide = System.Math.Min(workArea.Width, workArea.Height);
|
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
||||||
var plateShortSide = System.Math.Min(Plate.Size.Width, Plate.Size.Height);
|
var plateShortSide = System.Math.Min(Plate.Size.Width, Plate.Size.Length);
|
||||||
|
|
||||||
// When the work area is significantly narrower than the plate,
|
// When the work area is significantly narrower than the plate,
|
||||||
// include all pairs that fit the narrow dimension.
|
// include all pairs that fit the narrow dimension.
|
||||||
@@ -356,7 +356,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
var refDim = horizontal
|
var refDim = horizontal
|
||||||
? sorted.Max(p => p.BoundingBox.Width)
|
? sorted.Max(p => p.BoundingBox.Width)
|
||||||
: sorted.Max(p => p.BoundingBox.Height);
|
: sorted.Max(p => p.BoundingBox.Length);
|
||||||
var gapThreshold = refDim * 0.5;
|
var gapThreshold = refDim * 0.5;
|
||||||
|
|
||||||
var clusters = new List<List<Part>>();
|
var clusters = new List<List<Part>>();
|
||||||
@@ -425,7 +425,7 @@ namespace OpenNest
|
|||||||
if (stripWidth <= 0)
|
if (stripWidth <= 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
stripBox = new Box(stripLeft, workArea.Y, stripWidth, workArea.Height);
|
stripBox = new Box(stripLeft, workArea.Y, stripWidth, workArea.Length);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -438,7 +438,7 @@ namespace OpenNest
|
|||||||
stripBox = new Box(workArea.X, stripBottom, workArea.Width, stripHeight);
|
stripBox = new Box(workArea.X, stripBottom, workArea.Width, stripHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.WriteLine($"[TryStripRefill] Strip: {stripBox.Width:F1}x{stripBox.Height:F1} at ({stripBox.X:F1},{stripBox.Y:F1})");
|
Debug.WriteLine($"[TryStripRefill] Strip: {stripBox.Width:F1}x{stripBox.Length:F1} at ({stripBox.X:F1},{stripBox.Y:F1})");
|
||||||
|
|
||||||
var stripParts = FindBestFill(item, stripBox);
|
var stripParts = FindBestFill(item, stripBox);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace OpenNest.RectanglePacking
|
|||||||
};
|
};
|
||||||
|
|
||||||
bin.Width += partSpacing;
|
bin.Width += partSpacing;
|
||||||
bin.Height += partSpacing;
|
bin.Length += partSpacing;
|
||||||
|
|
||||||
return bin;
|
return bin;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ namespace OpenNest.RectanglePacking
|
|||||||
var box = item.Drawing.Program.BoundingBox();
|
var box = item.Drawing.Program.BoundingBox();
|
||||||
|
|
||||||
box.Width += partSpacing;
|
box.Width += partSpacing;
|
||||||
box.Height += partSpacing;
|
box.Length += partSpacing;
|
||||||
|
|
||||||
return new Item
|
return new Item
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ namespace OpenNest.RectanglePacking
|
|||||||
int normalColumns = 0;
|
int normalColumns = 0;
|
||||||
int rotateColumns = 0;
|
int rotateColumns = 0;
|
||||||
|
|
||||||
if (!BestCombination.FindFrom2(item.Width, item.Height, bin.Width, out normalColumns, out rotateColumns))
|
if (!BestCombination.FindFrom2(item.Width, item.Length, bin.Width, out normalColumns, out rotateColumns))
|
||||||
return bin;
|
return bin;
|
||||||
|
|
||||||
var normalRows = (int)System.Math.Floor((bin.Height + Tolerance.Epsilon) / item.Height);
|
var normalRows = (int)System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Length);
|
||||||
var rotateRows = (int)System.Math.Floor((bin.Height + Tolerance.Epsilon) / item.Width);
|
var rotateRows = (int)System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Width);
|
||||||
|
|
||||||
item.Location = bin.Location;
|
item.Location = bin.Location;
|
||||||
|
|
||||||
@@ -69,17 +69,17 @@ namespace OpenNest.RectanglePacking
|
|||||||
int normalRows = 0;
|
int normalRows = 0;
|
||||||
int rotateRows = 0;
|
int rotateRows = 0;
|
||||||
|
|
||||||
if (!BestCombination.FindFrom2(item.Height, item.Width, Bin.Height, out normalRows, out rotateRows))
|
if (!BestCombination.FindFrom2(item.Length, item.Width, Bin.Length, out normalRows, out rotateRows))
|
||||||
return bin;
|
return bin;
|
||||||
|
|
||||||
var normalColumns = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
|
var normalColumns = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
|
||||||
var rotateColumns = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Height);
|
var rotateColumns = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Length);
|
||||||
|
|
||||||
item.Location = bin.Location;
|
item.Location = bin.Location;
|
||||||
|
|
||||||
bin.Items.AddRange(FillGrid(item, normalRows, normalColumns, int.MaxValue));
|
bin.Items.AddRange(FillGrid(item, normalRows, normalColumns, int.MaxValue));
|
||||||
|
|
||||||
item.Location.Y += item.Height * normalRows;
|
item.Location.Y += item.Length * normalRows;
|
||||||
item.Rotate();
|
item.Rotate();
|
||||||
|
|
||||||
bin.Items.AddRange(FillGrid(item, rotateRows, rotateColumns, int.MaxValue));
|
bin.Items.AddRange(FillGrid(item, rotateRows, rotateColumns, int.MaxValue));
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace OpenNest.RectanglePacking
|
|||||||
for (var j = 0; j < innerCount; j++)
|
for (var j = 0; j < innerCount; j++)
|
||||||
{
|
{
|
||||||
var x = (columnMajor ? i : j) * item.Width + item.X;
|
var x = (columnMajor ? i : j) * item.Width + item.X;
|
||||||
var y = (columnMajor ? j : i) * item.Height + item.Y;
|
var y = (columnMajor ? j : i) * item.Length + item.Y;
|
||||||
|
|
||||||
var clone = item.Clone() as Item;
|
var clone = item.Clone() as Item;
|
||||||
clone.Location = new Vector(x, y);
|
clone.Location = new Vector(x, y);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace OpenNest.RectanglePacking
|
|||||||
|
|
||||||
public override void Fill(Item item)
|
public override void Fill(Item item)
|
||||||
{
|
{
|
||||||
var ycount = (int)System.Math.Floor((Bin.Height + Tolerance.Epsilon) / item.Height);
|
var ycount = (int)System.Math.Floor((Bin.Length + Tolerance.Epsilon) / item.Length);
|
||||||
var xcount = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
|
var xcount = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
|
||||||
|
|
||||||
for (int i = 0; i < xcount; i++)
|
for (int i = 0; i < xcount; i++)
|
||||||
@@ -24,7 +24,7 @@ namespace OpenNest.RectanglePacking
|
|||||||
|
|
||||||
for (int j = 0; j < ycount; j++)
|
for (int j = 0; j < ycount; j++)
|
||||||
{
|
{
|
||||||
var y = item.Height * j + Bin.Y;
|
var y = item.Length * j + Bin.Y;
|
||||||
|
|
||||||
var addedItem = item.Clone() as Item;
|
var addedItem = item.Clone() as Item;
|
||||||
addedItem.Location = new Vector(x, y);
|
addedItem.Location = new Vector(x, y);
|
||||||
@@ -36,7 +36,7 @@ namespace OpenNest.RectanglePacking
|
|||||||
|
|
||||||
public override void Fill(Item item, int maxCount)
|
public override void Fill(Item item, int maxCount)
|
||||||
{
|
{
|
||||||
var ycount = (int)System.Math.Floor((Bin.Height + Tolerance.Epsilon) / item.Height);
|
var ycount = (int)System.Math.Floor((Bin.Length + Tolerance.Epsilon) / item.Length);
|
||||||
var xcount = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
|
var xcount = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
|
||||||
var count = ycount * xcount;
|
var count = ycount * xcount;
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ namespace OpenNest.RectanglePacking
|
|||||||
columns = (int)System.Math.Ceiling((double)maxCount / rows);
|
columns = (int)System.Math.Ceiling((double)maxCount / rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bin.Items.AddRange(FillGrid(item, rows, columns, maxCount, columnMajor: item.Width > item.Height));
|
Bin.Items.AddRange(FillGrid(item, rows, columns, maxCount, columnMajor: item.Width > item.Length));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace OpenNest.RectanglePacking
|
|||||||
|
|
||||||
public void Rotate()
|
public void Rotate()
|
||||||
{
|
{
|
||||||
Generic.Swap(ref Size.Width, ref Size.Height);
|
Generic.Swap(ref Size.Width, ref Size.Length);
|
||||||
IsRotated = !IsRotated;
|
IsRotated = !IsRotated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ namespace OpenNest.RectanglePacking
|
|||||||
double minX = items[0].X;
|
double minX = items[0].X;
|
||||||
double minY = items[0].Y;
|
double minY = items[0].Y;
|
||||||
double maxX = items[0].X + items[0].Width;
|
double maxX = items[0].X + items[0].Width;
|
||||||
double maxY = items[0].Y + items[0].Height;
|
double maxY = items[0].Y + items[0].Length;
|
||||||
|
|
||||||
foreach (var box in items)
|
foreach (var box in items)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ namespace OpenNest.RectanglePacking
|
|||||||
|
|
||||||
public override void Pack(List<Item> items)
|
public override void Pack(List<Item> items)
|
||||||
{
|
{
|
||||||
items = items.OrderBy(i => -i.Height).ToList();
|
items = items.OrderBy(i => -i.Length).ToList();
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
if (item.Height > Bin.Height)
|
if (item.Length > Bin.Length)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var level = FindLevel(item);
|
var level = FindLevel(item);
|
||||||
@@ -36,7 +36,7 @@ namespace OpenNest.RectanglePacking
|
|||||||
{
|
{
|
||||||
foreach (var level in levels)
|
foreach (var level in levels)
|
||||||
{
|
{
|
||||||
if (level.Height < item.Height)
|
if (level.Height < item.Length)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (level.RemainingWidth < item.Width)
|
if (level.RemainingWidth < item.Width)
|
||||||
@@ -58,12 +58,12 @@ namespace OpenNest.RectanglePacking
|
|||||||
|
|
||||||
var remaining = Bin.Top - y;
|
var remaining = Bin.Top - y;
|
||||||
|
|
||||||
if (remaining < item.Height)
|
if (remaining < item.Length)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var level = new Level(Bin);
|
var level = new Level(Bin);
|
||||||
level.Y = y;
|
level.Y = y;
|
||||||
level.Height = item.Height;
|
level.Height = item.Length;
|
||||||
|
|
||||||
levels.Add(level);
|
levels.Add(level);
|
||||||
|
|
||||||
|
|||||||
@@ -145,29 +145,29 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
pt1 = new XYZ(0, 0, 0);
|
pt1 = new XYZ(0, 0, 0);
|
||||||
pt2 = new XYZ(0, plate.Size.Height, 0);
|
pt2 = new XYZ(0, plate.Size.Length, 0);
|
||||||
pt3 = new XYZ(plate.Size.Width, plate.Size.Height, 0);
|
pt3 = new XYZ(plate.Size.Width, plate.Size.Length, 0);
|
||||||
pt4 = new XYZ(plate.Size.Width, 0, 0);
|
pt4 = new XYZ(plate.Size.Width, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
pt1 = new XYZ(0, 0, 0);
|
pt1 = new XYZ(0, 0, 0);
|
||||||
pt2 = new XYZ(0, plate.Size.Height, 0);
|
pt2 = new XYZ(0, plate.Size.Length, 0);
|
||||||
pt3 = new XYZ(-plate.Size.Width, plate.Size.Height, 0);
|
pt3 = new XYZ(-plate.Size.Width, plate.Size.Length, 0);
|
||||||
pt4 = new XYZ(-plate.Size.Width, 0, 0);
|
pt4 = new XYZ(-plate.Size.Width, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
pt1 = new XYZ(0, 0, 0);
|
pt1 = new XYZ(0, 0, 0);
|
||||||
pt2 = new XYZ(0, -plate.Size.Height, 0);
|
pt2 = new XYZ(0, -plate.Size.Length, 0);
|
||||||
pt3 = new XYZ(-plate.Size.Width, -plate.Size.Height, 0);
|
pt3 = new XYZ(-plate.Size.Width, -plate.Size.Length, 0);
|
||||||
pt4 = new XYZ(-plate.Size.Width, 0, 0);
|
pt4 = new XYZ(-plate.Size.Width, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
pt1 = new XYZ(0, 0, 0);
|
pt1 = new XYZ(0, 0, 0);
|
||||||
pt2 = new XYZ(0, -plate.Size.Height, 0);
|
pt2 = new XYZ(0, -plate.Size.Length, 0);
|
||||||
pt3 = new XYZ(plate.Size.Width, -plate.Size.Height, 0);
|
pt3 = new XYZ(plate.Size.Width, -plate.Size.Length, 0);
|
||||||
pt4 = new XYZ(plate.Size.Width, 0, 0);
|
pt4 = new XYZ(plate.Size.Width, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
126
OpenNest.IO/NestFormat.cs
Normal file
126
OpenNest.IO/NestFormat.cs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public static class NestFormat
|
||||||
|
{
|
||||||
|
public static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public record NestDto
|
||||||
|
{
|
||||||
|
public int Version { get; init; } = 2;
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Units { get; init; } = "Inches";
|
||||||
|
public string Customer { get; init; } = "";
|
||||||
|
public string DateCreated { get; init; } = "";
|
||||||
|
public string DateLastModified { get; init; } = "";
|
||||||
|
public string Notes { get; init; } = "";
|
||||||
|
public PlateDefaultsDto PlateDefaults { get; init; } = new();
|
||||||
|
public List<DrawingDto> Drawings { get; init; } = new();
|
||||||
|
public List<PlateDto> Plates { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PlateDefaultsDto
|
||||||
|
{
|
||||||
|
public SizeDto Size { get; init; } = new();
|
||||||
|
public double Thickness { get; init; }
|
||||||
|
public int Quadrant { get; init; } = 1;
|
||||||
|
public double PartSpacing { get; init; }
|
||||||
|
public MaterialDto Material { get; init; } = new();
|
||||||
|
public SpacingDto EdgeSpacing { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DrawingDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Customer { get; init; } = "";
|
||||||
|
public ColorDto Color { get; init; } = new();
|
||||||
|
public QuantityDto Quantity { get; init; } = new();
|
||||||
|
public int Priority { get; init; }
|
||||||
|
public ConstraintsDto Constraints { get; init; } = new();
|
||||||
|
public MaterialDto Material { get; init; } = new();
|
||||||
|
public SourceDto Source { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PlateDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public SizeDto Size { get; init; } = new();
|
||||||
|
public double Thickness { get; init; }
|
||||||
|
public int Quadrant { get; init; } = 1;
|
||||||
|
public int Quantity { get; init; } = 1;
|
||||||
|
public double PartSpacing { get; init; }
|
||||||
|
public MaterialDto Material { get; init; } = new();
|
||||||
|
public SpacingDto EdgeSpacing { get; init; } = new();
|
||||||
|
public List<PartDto> Parts { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PartDto
|
||||||
|
{
|
||||||
|
public int DrawingId { get; init; }
|
||||||
|
public double X { get; init; }
|
||||||
|
public double Y { get; init; }
|
||||||
|
public double Rotation { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SizeDto
|
||||||
|
{
|
||||||
|
public double Width { get; init; }
|
||||||
|
public double Length { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record MaterialDto
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Grade { get; init; } = "";
|
||||||
|
public double Density { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SpacingDto
|
||||||
|
{
|
||||||
|
public double Left { get; init; }
|
||||||
|
public double Top { get; init; }
|
||||||
|
public double Right { get; init; }
|
||||||
|
public double Bottom { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ColorDto
|
||||||
|
{
|
||||||
|
public int A { get; init; } = 255;
|
||||||
|
public int R { get; init; }
|
||||||
|
public int G { get; init; }
|
||||||
|
public int B { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record QuantityDto
|
||||||
|
{
|
||||||
|
public int Required { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ConstraintsDto
|
||||||
|
{
|
||||||
|
public double StepAngle { get; init; }
|
||||||
|
public double StartAngle { get; init; }
|
||||||
|
public double EndAngle { get; init; }
|
||||||
|
public bool Allow180Equivalent { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SourceDto
|
||||||
|
{
|
||||||
|
public string Path { get; init; } = "";
|
||||||
|
public OffsetDto Offset { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record OffsetDto
|
||||||
|
{
|
||||||
|
public double X { get; init; }
|
||||||
|
public double Y { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +1,28 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.Json;
|
||||||
using System.Xml;
|
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
namespace OpenNest.IO
|
namespace OpenNest.IO
|
||||||
{
|
{
|
||||||
public sealed class NestReader
|
public sealed class NestReader
|
||||||
{
|
{
|
||||||
private ZipArchive zipArchive;
|
private readonly Stream stream;
|
||||||
private Dictionary<int, Plate> plateDict;
|
private readonly ZipArchive zipArchive;
|
||||||
private Dictionary<int, Drawing> drawingDict;
|
|
||||||
private Dictionary<int, Program> programDict;
|
|
||||||
private Dictionary<int, Program> plateProgramDict;
|
|
||||||
private Stream stream;
|
|
||||||
private Nest nest;
|
|
||||||
|
|
||||||
private NestReader()
|
|
||||||
{
|
|
||||||
plateDict = new Dictionary<int, Plate>();
|
|
||||||
drawingDict = new Dictionary<int, Drawing>();
|
|
||||||
programDict = new Dictionary<int, Program>();
|
|
||||||
plateProgramDict = new Dictionary<int, Program>();
|
|
||||||
nest = new Nest();
|
|
||||||
}
|
|
||||||
|
|
||||||
public NestReader(string file)
|
public NestReader(string file)
|
||||||
: this()
|
|
||||||
{
|
{
|
||||||
stream = new FileStream(file, FileMode.Open, FileAccess.Read);
|
stream = new FileStream(file, FileMode.Open, FileAccess.Read);
|
||||||
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
||||||
}
|
}
|
||||||
|
|
||||||
public NestReader(Stream stream)
|
public NestReader(Stream stream)
|
||||||
: this()
|
|
||||||
{
|
{
|
||||||
this.stream = stream;
|
this.stream = stream;
|
||||||
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
||||||
@@ -47,52 +30,12 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
public Nest Read()
|
public Nest Read()
|
||||||
{
|
{
|
||||||
const string plateExtensionPattern = "plate-\\d\\d\\d";
|
var nestJson = ReadEntry("nest.json");
|
||||||
const string programExtensionPattern = "program-\\d\\d\\d";
|
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
|
||||||
|
|
||||||
foreach (var entry in zipArchive.Entries)
|
var programs = ReadPrograms(dto.Drawings.Count);
|
||||||
{
|
var drawingMap = BuildDrawings(dto, programs);
|
||||||
var memstream = new MemoryStream();
|
var nest = BuildNest(dto, drawingMap);
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
|
||||||
entryStream.CopyTo(memstream);
|
|
||||||
}
|
|
||||||
|
|
||||||
memstream.Position = 0;
|
|
||||||
|
|
||||||
switch (entry.FullName)
|
|
||||||
{
|
|
||||||
case "info":
|
|
||||||
ReadNestInfo(memstream);
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case "drawing-info":
|
|
||||||
ReadDrawingInfo(memstream);
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case "plate-info":
|
|
||||||
ReadPlateInfo(memstream);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Regex.IsMatch(entry.FullName, programExtensionPattern))
|
|
||||||
{
|
|
||||||
ReadProgram(memstream, entry.FullName);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Regex.IsMatch(entry.FullName, plateExtensionPattern))
|
|
||||||
{
|
|
||||||
ReadPlate(memstream, entry.FullName);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LinkProgramsToDrawings();
|
|
||||||
LinkPartsToPlates();
|
|
||||||
|
|
||||||
AddPlatesToNest();
|
|
||||||
AddDrawingsToNest();
|
|
||||||
|
|
||||||
zipArchive.Dispose();
|
zipArchive.Dispose();
|
||||||
stream.Close();
|
stream.Close();
|
||||||
@@ -100,374 +43,114 @@ namespace OpenNest.IO
|
|||||||
return nest;
|
return nest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReadNestInfo(Stream stream)
|
private string ReadEntry(string name)
|
||||||
{
|
{
|
||||||
var reader = XmlReader.Create(stream);
|
var entry = zipArchive.GetEntry(name)
|
||||||
var spacing = new Spacing();
|
?? throw new InvalidDataException($"Nest file is missing required entry '{name}'.");
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
using var reader = new StreamReader(entryStream);
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
while (reader.Read())
|
private Dictionary<int, Program> ReadPrograms(int count)
|
||||||
|
{
|
||||||
|
var programs = new Dictionary<int, Program>();
|
||||||
|
for (var i = 1; i <= count; i++)
|
||||||
{
|
{
|
||||||
if (!reader.IsStartElement())
|
var entry = zipArchive.GetEntry($"programs/program-{i}");
|
||||||
continue;
|
if (entry == null) continue;
|
||||||
|
|
||||||
switch (reader.Name)
|
using var entryStream = entry.Open();
|
||||||
|
var memStream = new MemoryStream();
|
||||||
|
entryStream.CopyTo(memStream);
|
||||||
|
memStream.Position = 0;
|
||||||
|
|
||||||
|
var reader = new ProgramReader(memStream);
|
||||||
|
programs[i] = reader.Read();
|
||||||
|
}
|
||||||
|
return programs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, Drawing> BuildDrawings(NestDto dto, Dictionary<int, Program> programs)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<int, Drawing>();
|
||||||
|
foreach (var d in dto.Drawings)
|
||||||
|
{
|
||||||
|
var drawing = new Drawing(d.Name);
|
||||||
|
drawing.Customer = d.Customer;
|
||||||
|
drawing.Color = Color.FromArgb(d.Color.A, d.Color.R, d.Color.G, d.Color.B);
|
||||||
|
drawing.Quantity.Required = d.Quantity.Required;
|
||||||
|
drawing.Priority = d.Priority;
|
||||||
|
drawing.Constraints.StepAngle = d.Constraints.StepAngle;
|
||||||
|
drawing.Constraints.StartAngle = d.Constraints.StartAngle;
|
||||||
|
drawing.Constraints.EndAngle = d.Constraints.EndAngle;
|
||||||
|
drawing.Constraints.Allow180Equivalent = d.Constraints.Allow180Equivalent;
|
||||||
|
drawing.Material = new Material(d.Material.Name, d.Material.Grade, d.Material.Density);
|
||||||
|
drawing.Source.Path = d.Source.Path;
|
||||||
|
drawing.Source.Offset = new Vector(d.Source.Offset.X, d.Source.Offset.Y);
|
||||||
|
|
||||||
|
if (programs.TryGetValue(d.Id, out var pgm))
|
||||||
|
drawing.Program = pgm;
|
||||||
|
|
||||||
|
map[d.Id] = drawing;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Nest BuildNest(NestDto dto, Dictionary<int, Drawing> drawingMap)
|
||||||
|
{
|
||||||
|
var nest = new Nest();
|
||||||
|
nest.Name = dto.Name;
|
||||||
|
|
||||||
|
Units units;
|
||||||
|
if (Enum.TryParse(dto.Units, true, out units))
|
||||||
|
nest.Units = units;
|
||||||
|
|
||||||
|
nest.Customer = dto.Customer;
|
||||||
|
nest.DateCreated = DateTime.Parse(dto.DateCreated);
|
||||||
|
nest.DateLastModified = DateTime.Parse(dto.DateLastModified);
|
||||||
|
nest.Notes = dto.Notes;
|
||||||
|
|
||||||
|
// Plate defaults
|
||||||
|
var pd = dto.PlateDefaults;
|
||||||
|
nest.PlateDefaults.Size = new OpenNest.Geometry.Size(pd.Size.Width, pd.Size.Length);
|
||||||
|
nest.PlateDefaults.Thickness = pd.Thickness;
|
||||||
|
nest.PlateDefaults.Quadrant = pd.Quadrant;
|
||||||
|
nest.PlateDefaults.PartSpacing = pd.PartSpacing;
|
||||||
|
nest.PlateDefaults.Material = new Material(pd.Material.Name, pd.Material.Grade, pd.Material.Density);
|
||||||
|
nest.PlateDefaults.EdgeSpacing = new Spacing(pd.EdgeSpacing.Left, pd.EdgeSpacing.Bottom, pd.EdgeSpacing.Right, pd.EdgeSpacing.Top);
|
||||||
|
|
||||||
|
// Drawings
|
||||||
|
foreach (var d in drawingMap.OrderBy(k => k.Key))
|
||||||
|
nest.Drawings.Add(d.Value);
|
||||||
|
|
||||||
|
// Plates
|
||||||
|
foreach (var p in dto.Plates.OrderBy(p => p.Id))
|
||||||
|
{
|
||||||
|
var plate = new Plate();
|
||||||
|
plate.Size = new OpenNest.Geometry.Size(p.Size.Width, p.Size.Length);
|
||||||
|
plate.Thickness = p.Thickness;
|
||||||
|
plate.Quadrant = p.Quadrant;
|
||||||
|
plate.Quantity = p.Quantity;
|
||||||
|
plate.PartSpacing = p.PartSpacing;
|
||||||
|
plate.Material = new Material(p.Material.Name, p.Material.Grade, p.Material.Density);
|
||||||
|
plate.EdgeSpacing = new Spacing(p.EdgeSpacing.Left, p.EdgeSpacing.Bottom, p.EdgeSpacing.Right, p.EdgeSpacing.Top);
|
||||||
|
|
||||||
|
foreach (var partDto in p.Parts)
|
||||||
{
|
{
|
||||||
case "Nest":
|
if (!drawingMap.TryGetValue(partDto.DrawingId, out var dwg))
|
||||||
nest.Name = reader["name"];
|
continue;
|
||||||
break;
|
|
||||||
|
|
||||||
case "Units":
|
var part = new Part(dwg);
|
||||||
Units units;
|
part.Rotate(partDto.Rotation);
|
||||||
TryParseEnum<Units>(reader.ReadString(), out units);
|
part.Offset(new Vector(partDto.X, partDto.Y));
|
||||||
nest.Units = units;
|
plate.Parts.Add(part);
|
||||||
break;
|
|
||||||
|
|
||||||
case "Customer":
|
|
||||||
nest.Customer = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "DateCreated":
|
|
||||||
nest.DateCreated = DateTime.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "DateLastModified":
|
|
||||||
nest.DateLastModified = DateTime.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Notes":
|
|
||||||
nest.Notes = Uri.UnescapeDataString(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Size":
|
|
||||||
nest.PlateDefaults.Size = OpenNest.Geometry.Size.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Thickness":
|
|
||||||
nest.PlateDefaults.Thickness = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Quadrant":
|
|
||||||
nest.PlateDefaults.Quadrant = int.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "PartSpacing":
|
|
||||||
nest.PlateDefaults.PartSpacing = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Name":
|
|
||||||
nest.PlateDefaults.Material.Name = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Grade":
|
|
||||||
nest.PlateDefaults.Material.Grade = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Density":
|
|
||||||
nest.PlateDefaults.Material.Density = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Left":
|
|
||||||
spacing.Left = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Right":
|
|
||||||
spacing.Right = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Top":
|
|
||||||
spacing.Top = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Bottom":
|
|
||||||
spacing.Bottom = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nest.Plates.Add(plate);
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.Close();
|
return nest;
|
||||||
nest.PlateDefaults.EdgeSpacing = spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadDrawingInfo(Stream stream)
|
|
||||||
{
|
|
||||||
var reader = XmlReader.Create(stream);
|
|
||||||
Drawing drawing = null;
|
|
||||||
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
if (!reader.IsStartElement())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
switch (reader.Name)
|
|
||||||
{
|
|
||||||
case "Drawing":
|
|
||||||
var id = int.Parse(reader["id"]);
|
|
||||||
var name = reader["name"];
|
|
||||||
|
|
||||||
drawingDict.Add(id, (drawing = new Drawing(name)));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Customer":
|
|
||||||
drawing.Customer = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Color":
|
|
||||||
{
|
|
||||||
var parts = reader.ReadString().Split(',');
|
|
||||||
|
|
||||||
if (parts.Length == 3)
|
|
||||||
{
|
|
||||||
byte r = byte.Parse(parts[0]);
|
|
||||||
byte g = byte.Parse(parts[1]);
|
|
||||||
byte b = byte.Parse(parts[2]);
|
|
||||||
|
|
||||||
drawing.Color = Color.FromArgb(r, g, b);
|
|
||||||
}
|
|
||||||
else if (parts.Length == 4)
|
|
||||||
{
|
|
||||||
byte a = byte.Parse(parts[0]);
|
|
||||||
byte r = byte.Parse(parts[1]);
|
|
||||||
byte g = byte.Parse(parts[2]);
|
|
||||||
byte b = byte.Parse(parts[3]);
|
|
||||||
|
|
||||||
drawing.Color = Color.FromArgb(a, r, g, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Required":
|
|
||||||
drawing.Quantity.Required = int.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Name":
|
|
||||||
drawing.Material.Name = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Grade":
|
|
||||||
drawing.Material.Grade = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Density":
|
|
||||||
drawing.Material.Density = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Path":
|
|
||||||
drawing.Source.Path = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Offset":
|
|
||||||
{
|
|
||||||
var parts = reader.ReadString().Split(',');
|
|
||||||
|
|
||||||
if (parts.Length != 2)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
drawing.Source.Offset = new Vector(double.Parse(parts[0]), double.Parse(parts[1]));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadPlateInfo(Stream stream)
|
|
||||||
{
|
|
||||||
var reader = XmlReader.Create(stream);
|
|
||||||
var spacing = new Spacing();
|
|
||||||
Plate plate = null;
|
|
||||||
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
if (!reader.IsStartElement())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
switch (reader.Name)
|
|
||||||
{
|
|
||||||
case "Plate":
|
|
||||||
var id = int.Parse(reader["id"]);
|
|
||||||
|
|
||||||
if (plate != null)
|
|
||||||
plate.EdgeSpacing = spacing;
|
|
||||||
|
|
||||||
plateDict.Add(id, (plate = new Plate()));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Size":
|
|
||||||
plate.Size = OpenNest.Geometry.Size.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Qty":
|
|
||||||
plate.Quantity = int.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Thickness":
|
|
||||||
plate.Thickness = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Quadrant":
|
|
||||||
plate.Quadrant = int.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "PartSpacing":
|
|
||||||
plate.PartSpacing = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Name":
|
|
||||||
plate.Material.Name = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Grade":
|
|
||||||
plate.Material.Grade = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Density":
|
|
||||||
plate.Material.Density = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Left":
|
|
||||||
spacing.Left = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Right":
|
|
||||||
spacing.Right = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Top":
|
|
||||||
spacing.Top = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Bottom":
|
|
||||||
spacing.Bottom = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plate != null)
|
|
||||||
plate.EdgeSpacing = spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadProgram(Stream stream, string name)
|
|
||||||
{
|
|
||||||
var id = GetProgramId(name);
|
|
||||||
var reader = new ProgramReader(stream);
|
|
||||||
var pgm = reader.Read();
|
|
||||||
programDict.Add(id, pgm);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadPlate(Stream stream, string name)
|
|
||||||
{
|
|
||||||
var id = GetPlateId(name);
|
|
||||||
var reader = new ProgramReader(stream);
|
|
||||||
var pgm = reader.Read();
|
|
||||||
plateProgramDict.Add(id, pgm);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LinkProgramsToDrawings()
|
|
||||||
{
|
|
||||||
foreach (var drawingItem in drawingDict)
|
|
||||||
{
|
|
||||||
Program pgm;
|
|
||||||
|
|
||||||
if (programDict.TryGetValue(drawingItem.Key, out pgm))
|
|
||||||
drawingItem.Value.Program = pgm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LinkPartsToPlates()
|
|
||||||
{
|
|
||||||
foreach (var plateProgram in plateProgramDict)
|
|
||||||
{
|
|
||||||
var parts = CreateParts(plateProgram.Value);
|
|
||||||
|
|
||||||
Plate plate;
|
|
||||||
|
|
||||||
if (!plateDict.TryGetValue(plateProgram.Key, out plate))
|
|
||||||
plate = new Plate();
|
|
||||||
|
|
||||||
plate.Parts.AddRange(parts);
|
|
||||||
plateDict[plateProgram.Key] = plate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddPlatesToNest()
|
|
||||||
{
|
|
||||||
var plates = plateDict.OrderBy(i => i.Key).Select(i => i.Value).ToList();
|
|
||||||
nest.Plates.AddRange(plates);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddDrawingsToNest()
|
|
||||||
{
|
|
||||||
var drawings = drawingDict.OrderBy(i => i.Key).Select(i => i.Value).ToList();
|
|
||||||
drawings.ForEach(d => nest.Drawings.Add(d));
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Part> CreateParts(Program pgm)
|
|
||||||
{
|
|
||||||
var parts = new List<Part>();
|
|
||||||
var pos = Vector.Zero;
|
|
||||||
|
|
||||||
for (int i = 0; i < pgm.Codes.Count; i++)
|
|
||||||
{
|
|
||||||
var code = pgm.Codes[i];
|
|
||||||
|
|
||||||
switch (code.Type)
|
|
||||||
{
|
|
||||||
case CodeType.RapidMove:
|
|
||||||
pos = ((RapidMove)code).EndPoint;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CodeType.SubProgramCall:
|
|
||||||
var subpgm = (SubProgramCall)code;
|
|
||||||
var dwg = drawingDict[subpgm.Id];
|
|
||||||
var part = new Part(dwg);
|
|
||||||
part.Rotate(Angle.ToRadians(subpgm.Rotation));
|
|
||||||
part.Offset(pos);
|
|
||||||
parts.Add(part);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetPlateId(string name)
|
|
||||||
{
|
|
||||||
return int.Parse(name.Replace("plate-", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetProgramId(string name)
|
|
||||||
{
|
|
||||||
return int.Parse(name.Replace("program-", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T ParseEnum<T>(string value)
|
|
||||||
{
|
|
||||||
return (T)Enum.Parse(typeof(T), value, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryParseEnum<T>(string value, out T e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
e = ParseEnum<T>(value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
e = ParseEnum<T>(typeof(T).GetEnumValues().GetValue(0).ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum NestInfoSection
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
DefaultPlate,
|
|
||||||
Material,
|
|
||||||
EdgeSpacing,
|
|
||||||
Source
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,22 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml;
|
using System.Text.Json;
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
namespace OpenNest.IO
|
namespace OpenNest.IO
|
||||||
{
|
{
|
||||||
public sealed class NestWriter
|
public sealed class NestWriter
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Number of decimal places the output is round to.
|
|
||||||
/// This number must have more decimal places than Tolerance.Epsilon
|
|
||||||
/// </summary>
|
|
||||||
private const int OutputPrecision = 10;
|
private const int OutputPrecision = 10;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fixed-point format string that avoids scientific notation.
|
|
||||||
/// ProgramReader treats 'E' as a code letter, so "6.66E-08" would be
|
|
||||||
/// split into X:"6.66" and E:"-08", corrupting the parsed value.
|
|
||||||
/// </summary>
|
|
||||||
private const string CoordinateFormat = "0.##########";
|
private const string CoordinateFormat = "0.##########";
|
||||||
|
|
||||||
private readonly Nest nest;
|
private readonly Nest nest;
|
||||||
private ZipArchive zipArchive;
|
|
||||||
private Dictionary<int, Drawing> drawingDict;
|
private Dictionary<int, Drawing> drawingDict;
|
||||||
|
|
||||||
public NestWriter(Nest nest)
|
public NestWriter(Nest nest)
|
||||||
@@ -37,27 +27,21 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
public bool Write(string file)
|
public bool Write(string file)
|
||||||
{
|
{
|
||||||
this.nest.DateLastModified = DateTime.Now;
|
nest.DateLastModified = DateTime.Now;
|
||||||
|
|
||||||
SetDrawingIds();
|
SetDrawingIds();
|
||||||
|
|
||||||
using (var fileStream = new FileStream(file, FileMode.Create))
|
using var fileStream = new FileStream(file, FileMode.Create);
|
||||||
using (zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create))
|
using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create);
|
||||||
{
|
|
||||||
AddNestInfo();
|
WriteNestJson(zipArchive);
|
||||||
AddPlates();
|
WritePrograms(zipArchive);
|
||||||
AddPlateInfo();
|
|
||||||
AddDrawings();
|
|
||||||
AddDrawingInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetDrawingIds()
|
private void SetDrawingIds()
|
||||||
{
|
{
|
||||||
int id = 1;
|
var id = 1;
|
||||||
|
|
||||||
foreach (var drawing in nest.Drawings)
|
foreach (var drawing in nest.Drawings)
|
||||||
{
|
{
|
||||||
drawingDict.Add(id, drawing);
|
drawingDict.Add(id, drawing);
|
||||||
@@ -65,241 +49,156 @@ namespace OpenNest.IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddNestInfo()
|
private void WriteNestJson(ZipArchive zipArchive)
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream();
|
var dto = BuildNestDto();
|
||||||
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
|
var json = JsonSerializer.Serialize(dto, JsonOptions);
|
||||||
{
|
|
||||||
Indent = true
|
|
||||||
});
|
|
||||||
|
|
||||||
writer.WriteStartDocument();
|
var entry = zipArchive.CreateEntry("nest.json");
|
||||||
writer.WriteStartElement("Nest");
|
using var stream = entry.Open();
|
||||||
writer.WriteAttributeString("name", nest.Name);
|
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||||
|
writer.Write(json);
|
||||||
writer.WriteElementString("Units", nest.Units.ToString());
|
|
||||||
writer.WriteElementString("Customer", nest.Customer);
|
|
||||||
writer.WriteElementString("DateCreated", nest.DateCreated.ToString());
|
|
||||||
writer.WriteElementString("DateLastModified", nest.DateLastModified.ToString());
|
|
||||||
|
|
||||||
writer.WriteStartElement("DefaultPlate");
|
|
||||||
writer.WriteElementString("Size", nest.PlateDefaults.Size.ToString());
|
|
||||||
writer.WriteElementString("Thickness", nest.PlateDefaults.Thickness.ToString());
|
|
||||||
writer.WriteElementString("Quadrant", nest.PlateDefaults.Quadrant.ToString());
|
|
||||||
writer.WriteElementString("PartSpacing", nest.PlateDefaults.PartSpacing.ToString());
|
|
||||||
|
|
||||||
writer.WriteStartElement("Material");
|
|
||||||
writer.WriteElementString("Name", nest.PlateDefaults.Material.Name);
|
|
||||||
writer.WriteElementString("Grade", nest.PlateDefaults.Material.Grade);
|
|
||||||
writer.WriteElementString("Density", nest.PlateDefaults.Material.Density.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteStartElement("EdgeSpacing");
|
|
||||||
writer.WriteElementString("Left", nest.PlateDefaults.EdgeSpacing.Left.ToString());
|
|
||||||
writer.WriteElementString("Top", nest.PlateDefaults.EdgeSpacing.Top.ToString());
|
|
||||||
writer.WriteElementString("Right", nest.PlateDefaults.EdgeSpacing.Right.ToString());
|
|
||||||
writer.WriteElementString("Bottom", nest.PlateDefaults.EdgeSpacing.Bottom.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteElementString("Notes", Uri.EscapeDataString(nest.Notes));
|
|
||||||
|
|
||||||
writer.WriteEndElement(); // DefaultPlate
|
|
||||||
writer.WriteEndElement(); // Nest
|
|
||||||
|
|
||||||
writer.WriteEndDocument();
|
|
||||||
|
|
||||||
writer.Flush();
|
|
||||||
writer.Close();
|
|
||||||
|
|
||||||
stream.Position = 0;
|
|
||||||
|
|
||||||
var entry = zipArchive.CreateEntry("info");
|
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
|
||||||
stream.CopyTo(entryStream);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPlates()
|
private NestDto BuildNestDto()
|
||||||
{
|
{
|
||||||
int num = 1;
|
return new NestDto
|
||||||
|
|
||||||
foreach (var plate in nest.Plates)
|
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream();
|
Version = 2,
|
||||||
var name = string.Format("plate-{0}", num.ToString().PadLeft(3, '0'));
|
Name = nest.Name ?? "",
|
||||||
|
Units = nest.Units.ToString(),
|
||||||
|
Customer = nest.Customer ?? "",
|
||||||
|
DateCreated = nest.DateCreated.ToString("o"),
|
||||||
|
DateLastModified = nest.DateLastModified.ToString("o"),
|
||||||
|
Notes = nest.Notes ?? "",
|
||||||
|
PlateDefaults = BuildPlateDefaultsDto(),
|
||||||
|
Drawings = BuildDrawingDtos(),
|
||||||
|
Plates = BuildPlateDtos()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
WritePlate(stream, plate);
|
private PlateDefaultsDto BuildPlateDefaultsDto()
|
||||||
|
{
|
||||||
var entry = zipArchive.CreateEntry(name);
|
var pd = nest.PlateDefaults;
|
||||||
using (var entryStream = entry.Open())
|
return new PlateDefaultsDto
|
||||||
|
{
|
||||||
|
Size = new SizeDto { Width = pd.Size.Width, Length = pd.Size.Length },
|
||||||
|
Thickness = pd.Thickness,
|
||||||
|
Quadrant = pd.Quadrant,
|
||||||
|
PartSpacing = pd.PartSpacing,
|
||||||
|
Material = new MaterialDto
|
||||||
{
|
{
|
||||||
stream.CopyTo(entryStream);
|
Name = pd.Material.Name ?? "",
|
||||||
|
Grade = pd.Material.Grade ?? "",
|
||||||
|
Density = pd.Material.Density
|
||||||
|
},
|
||||||
|
EdgeSpacing = new SpacingDto
|
||||||
|
{
|
||||||
|
Left = pd.EdgeSpacing.Left,
|
||||||
|
Top = pd.EdgeSpacing.Top,
|
||||||
|
Right = pd.EdgeSpacing.Right,
|
||||||
|
Bottom = pd.EdgeSpacing.Bottom
|
||||||
}
|
}
|
||||||
|
};
|
||||||
num++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPlateInfo()
|
private List<DrawingDto> BuildDrawingDtos()
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream();
|
var list = new List<DrawingDto>();
|
||||||
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
{
|
{
|
||||||
Indent = true
|
var d = kvp.Value;
|
||||||
});
|
list.Add(new DrawingDto
|
||||||
|
{
|
||||||
|
Id = kvp.Key,
|
||||||
|
Name = d.Name ?? "",
|
||||||
|
Customer = d.Customer ?? "",
|
||||||
|
Color = new ColorDto { A = d.Color.A, R = d.Color.R, G = d.Color.G, B = d.Color.B },
|
||||||
|
Quantity = new QuantityDto { Required = d.Quantity.Required },
|
||||||
|
Priority = d.Priority,
|
||||||
|
Constraints = new ConstraintsDto
|
||||||
|
{
|
||||||
|
StepAngle = d.Constraints.StepAngle,
|
||||||
|
StartAngle = d.Constraints.StartAngle,
|
||||||
|
EndAngle = d.Constraints.EndAngle,
|
||||||
|
Allow180Equivalent = d.Constraints.Allow180Equivalent
|
||||||
|
},
|
||||||
|
Material = new MaterialDto
|
||||||
|
{
|
||||||
|
Name = d.Material.Name ?? "",
|
||||||
|
Grade = d.Material.Grade ?? "",
|
||||||
|
Density = d.Material.Density
|
||||||
|
},
|
||||||
|
Source = new SourceDto
|
||||||
|
{
|
||||||
|
Path = d.Source.Path ?? "",
|
||||||
|
Offset = new OffsetDto { X = d.Source.Offset.X, Y = d.Source.Offset.Y }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
writer.WriteStartDocument();
|
private List<PlateDto> BuildPlateDtos()
|
||||||
writer.WriteStartElement("Plates");
|
{
|
||||||
writer.WriteAttributeString("count", nest.Plates.Count.ToString());
|
var list = new List<PlateDto>();
|
||||||
|
for (var i = 0; i < nest.Plates.Count; i++)
|
||||||
for (int i = 0; i < nest.Plates.Count; ++i)
|
|
||||||
{
|
{
|
||||||
var plate = nest.Plates[i];
|
var plate = nest.Plates[i];
|
||||||
|
var parts = new List<PartDto>();
|
||||||
writer.WriteStartElement("Plate");
|
foreach (var part in plate.Parts)
|
||||||
writer.WriteAttributeString("id", (i + 1).ToString());
|
|
||||||
|
|
||||||
writer.WriteElementString("Quadrant", plate.Quadrant.ToString());
|
|
||||||
writer.WriteElementString("Thickness", plate.Thickness.ToString());
|
|
||||||
writer.WriteElementString("Size", plate.Size.ToString());
|
|
||||||
writer.WriteElementString("Qty", plate.Quantity.ToString());
|
|
||||||
writer.WriteElementString("PartSpacing", plate.PartSpacing.ToString());
|
|
||||||
|
|
||||||
writer.WriteStartElement("Material");
|
|
||||||
writer.WriteElementString("Name", plate.Material.Name);
|
|
||||||
writer.WriteElementString("Grade", plate.Material.Grade);
|
|
||||||
writer.WriteElementString("Density", plate.Material.Density.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteStartElement("EdgeSpacing");
|
|
||||||
writer.WriteElementString("Left", plate.EdgeSpacing.Left.ToString());
|
|
||||||
writer.WriteElementString("Top", plate.EdgeSpacing.Top.ToString());
|
|
||||||
writer.WriteElementString("Right", plate.EdgeSpacing.Right.ToString());
|
|
||||||
writer.WriteElementString("Bottom", plate.EdgeSpacing.Bottom.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteEndElement(); // Plate
|
|
||||||
writer.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndElement(); // Plates
|
|
||||||
writer.WriteEndDocument();
|
|
||||||
|
|
||||||
writer.Flush();
|
|
||||||
writer.Close();
|
|
||||||
|
|
||||||
stream.Position = 0;
|
|
||||||
|
|
||||||
var entry = zipArchive.CreateEntry("plate-info");
|
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
|
||||||
stream.CopyTo(entryStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddDrawings()
|
|
||||||
{
|
|
||||||
int num = 1;
|
|
||||||
|
|
||||||
foreach (var dwg in nest.Drawings)
|
|
||||||
{
|
|
||||||
var stream = new MemoryStream();
|
|
||||||
var name = string.Format("program-{0}", num.ToString().PadLeft(3, '0'));
|
|
||||||
|
|
||||||
WriteDrawing(stream, dwg);
|
|
||||||
|
|
||||||
var entry = zipArchive.CreateEntry(name);
|
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
{
|
||||||
stream.CopyTo(entryStream);
|
var match = drawingDict.Where(dwg => dwg.Value == part.BaseDrawing).FirstOrDefault();
|
||||||
|
parts.Add(new PartDto
|
||||||
|
{
|
||||||
|
DrawingId = match.Key,
|
||||||
|
X = part.Location.X,
|
||||||
|
Y = part.Location.Y,
|
||||||
|
Rotation = part.Rotation
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
num++;
|
list.Add(new PlateDto
|
||||||
|
{
|
||||||
|
Id = i + 1,
|
||||||
|
Size = new SizeDto { Width = plate.Size.Width, Length = plate.Size.Length },
|
||||||
|
Thickness = plate.Thickness,
|
||||||
|
Quadrant = plate.Quadrant,
|
||||||
|
Quantity = plate.Quantity,
|
||||||
|
PartSpacing = plate.PartSpacing,
|
||||||
|
Material = new MaterialDto
|
||||||
|
{
|
||||||
|
Name = plate.Material.Name ?? "",
|
||||||
|
Grade = plate.Material.Grade ?? "",
|
||||||
|
Density = plate.Material.Density
|
||||||
|
},
|
||||||
|
EdgeSpacing = new SpacingDto
|
||||||
|
{
|
||||||
|
Left = plate.EdgeSpacing.Left,
|
||||||
|
Top = plate.EdgeSpacing.Top,
|
||||||
|
Right = plate.EdgeSpacing.Right,
|
||||||
|
Bottom = plate.EdgeSpacing.Bottom
|
||||||
|
},
|
||||||
|
Parts = parts
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddDrawingInfo()
|
private void WritePrograms(ZipArchive zipArchive)
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream();
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
|
|
||||||
{
|
{
|
||||||
Indent = true
|
var name = $"programs/program-{kvp.Key}";
|
||||||
});
|
var stream = new MemoryStream();
|
||||||
|
WriteDrawing(stream, kvp.Value);
|
||||||
|
|
||||||
writer.WriteStartDocument();
|
var entry = zipArchive.CreateEntry(name);
|
||||||
writer.WriteStartElement("Drawings");
|
using var entryStream = entry.Open();
|
||||||
writer.WriteAttributeString("count", nest.Drawings.Count.ToString());
|
|
||||||
|
|
||||||
int id = 1;
|
|
||||||
|
|
||||||
foreach (var drawing in nest.Drawings)
|
|
||||||
{
|
|
||||||
writer.WriteStartElement("Drawing");
|
|
||||||
writer.WriteAttributeString("id", id.ToString());
|
|
||||||
writer.WriteAttributeString("name", drawing.Name);
|
|
||||||
|
|
||||||
writer.WriteElementString("Customer", drawing.Customer);
|
|
||||||
writer.WriteElementString("Color", string.Format("{0}, {1}, {2}, {3}", drawing.Color.A, drawing.Color.R, drawing.Color.G, drawing.Color.B));
|
|
||||||
|
|
||||||
writer.WriteStartElement("Quantity");
|
|
||||||
writer.WriteElementString("Required", drawing.Quantity.Required.ToString());
|
|
||||||
writer.WriteElementString("Nested", drawing.Quantity.Nested.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteStartElement("Material");
|
|
||||||
writer.WriteElementString("Name", drawing.Material.Name);
|
|
||||||
writer.WriteElementString("Grade", drawing.Material.Grade);
|
|
||||||
writer.WriteElementString("Density", drawing.Material.Density.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteStartElement("Source");
|
|
||||||
writer.WriteElementString("Path", drawing.Source.Path);
|
|
||||||
writer.WriteElementString("Offset", string.Format("{0}, {1}",
|
|
||||||
drawing.Source.Offset.X,
|
|
||||||
drawing.Source.Offset.Y));
|
|
||||||
writer.WriteEndElement(); // Source
|
|
||||||
|
|
||||||
writer.WriteEndElement(); // Drawing
|
|
||||||
|
|
||||||
id++;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndElement(); // Drawings
|
|
||||||
writer.WriteEndDocument();
|
|
||||||
|
|
||||||
writer.Flush();
|
|
||||||
writer.Close();
|
|
||||||
|
|
||||||
stream.Position = 0;
|
|
||||||
|
|
||||||
var entry = zipArchive.CreateEntry("drawing-info");
|
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
|
||||||
stream.CopyTo(entryStream);
|
stream.CopyTo(entryStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WritePlate(Stream stream, Plate plate)
|
|
||||||
{
|
|
||||||
var writer = new StreamWriter(stream);
|
|
||||||
writer.AutoFlush = true;
|
|
||||||
writer.WriteLine("G90");
|
|
||||||
|
|
||||||
foreach (var part in plate.Parts)
|
|
||||||
{
|
|
||||||
var match = drawingDict.Where(dwg => dwg.Value == part.BaseDrawing).FirstOrDefault();
|
|
||||||
var id = match.Key;
|
|
||||||
|
|
||||||
writer.WriteLine("G00X{0}Y{1}",
|
|
||||||
part.Location.X.ToString(CoordinateFormat),
|
|
||||||
part.Location.Y.ToString(CoordinateFormat));
|
|
||||||
writer.WriteLine("G65P{0}R{1}", id, Angle.ToDegrees(part.Rotation));
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.Position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteDrawing(Stream stream, Drawing drawing)
|
private void WriteDrawing(Stream stream, Drawing drawing)
|
||||||
{
|
{
|
||||||
var program = drawing.Program;
|
var program = drawing.Program;
|
||||||
@@ -308,7 +207,7 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
|
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
|
||||||
|
|
||||||
for (int i = 0; i < drawing.Program.Length; ++i)
|
for (var i = 0; i < drawing.Program.Length; ++i)
|
||||||
{
|
{
|
||||||
var code = drawing.Program[i];
|
var code = drawing.Program[i];
|
||||||
writer.WriteLine(GetCodeString(code));
|
writer.WriteLine(GetCodeString(code));
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ namespace OpenNest.Mcp.Tools
|
|||||||
{
|
{
|
||||||
var plate = nest.Plates[i];
|
var plate = nest.Plates[i];
|
||||||
var work = plate.WorkArea();
|
var work = plate.WorkArea();
|
||||||
sb.AppendLine($" Plate {i}: {plate.Size.Width:F1} x {plate.Size.Height:F1}, " +
|
sb.AppendLine($" Plate {i}: {plate.Size.Width:F1} x {plate.Size.Length:F1}, " +
|
||||||
$"parts={plate.Parts.Count}, " +
|
$"parts={plate.Parts.Count}, " +
|
||||||
$"utilization={plate.Utilization():P1}, " +
|
$"utilization={plate.Utilization():P1}, " +
|
||||||
$"work area={work.Width:F1} x {work.Height:F1}");
|
$"work area={work.Width:F1} x {work.Length:F1}");
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.AppendLine($"Drawings: {nest.Drawings.Count}");
|
sb.AppendLine($"Drawings: {nest.Drawings.Count}");
|
||||||
@@ -51,7 +51,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
foreach (var dwg in nest.Drawings)
|
foreach (var dwg in nest.Drawings)
|
||||||
{
|
{
|
||||||
var bbox = dwg.Program.BoundingBox();
|
var bbox = dwg.Program.BoundingBox();
|
||||||
sb.AppendLine($" {dwg.Name}: bbox={bbox.Width:F2} x {bbox.Height:F2}, " +
|
sb.AppendLine($" {dwg.Name}: bbox={bbox.Width:F2} x {bbox.Length:F2}, " +
|
||||||
$"required={dwg.Quantity.Required}, nested={dwg.Quantity.Nested}");
|
$"required={dwg.Quantity.Required}, nested={dwg.Quantity.Nested}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
_session.Drawings.Add(drawing);
|
_session.Drawings.Add(drawing);
|
||||||
|
|
||||||
var bbox = pgm.BoundingBox();
|
var bbox = pgm.BoundingBox();
|
||||||
return $"Imported drawing '{drawingName}': bbox={bbox.Width:F2} x {bbox.Height:F2}";
|
return $"Imported drawing '{drawingName}': bbox={bbox.Width:F2} x {bbox.Length:F2}";
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool(Name = "create_drawing")]
|
[McpServerTool(Name = "create_drawing")]
|
||||||
@@ -134,7 +134,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
_session.Drawings.Add(drawing);
|
_session.Drawings.Add(drawing);
|
||||||
|
|
||||||
var bbox = pgm.BoundingBox();
|
var bbox = pgm.BoundingBox();
|
||||||
return $"Created drawing '{name}': bbox={bbox.Width:F2} x {bbox.Height:F2}";
|
return $"Created drawing '{name}': bbox={bbox.Width:F2} x {bbox.Length:F2}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CncProgram CreateRectangle(double width, double height)
|
private static CncProgram CreateRectangle(double width, double height)
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ namespace OpenNest.Mcp.Tools
|
|||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine($"Plate {plateIndex}:");
|
sb.AppendLine($"Plate {plateIndex}:");
|
||||||
sb.AppendLine($" Size: {plate.Size.Width:F1} x {plate.Size.Height:F1}");
|
sb.AppendLine($" Size: {plate.Size.Width:F1} x {plate.Size.Length:F1}");
|
||||||
sb.AppendLine($" Quadrant: {plate.Quadrant}");
|
sb.AppendLine($" Quadrant: {plate.Quadrant}");
|
||||||
sb.AppendLine($" Thickness: {plate.Thickness:F2}");
|
sb.AppendLine($" Thickness: {plate.Thickness:F2}");
|
||||||
sb.AppendLine($" Material: {plate.Material.Name}");
|
sb.AppendLine($" Material: {plate.Material.Name}");
|
||||||
sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}");
|
sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}");
|
||||||
sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}");
|
sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}");
|
||||||
sb.AppendLine($" Work area: {work.X:F1},{work.Y:F1} {work.Width:F1}x{work.Height:F1}");
|
sb.AppendLine($" Work area: {work.X:F1},{work.Y:F1} {work.Width:F1}x{work.Length:F1}");
|
||||||
sb.AppendLine($" Parts: {plate.Parts.Count}");
|
sb.AppendLine($" Parts: {plate.Parts.Count}");
|
||||||
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
|
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
|
||||||
sb.AppendLine($" Quantity: {plate.Quantity}");
|
sb.AppendLine($" Quantity: {plate.Quantity}");
|
||||||
@@ -57,7 +57,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
for (var i = 0; i < remnants.Count; i++)
|
for (var i = 0; i < remnants.Count; i++)
|
||||||
{
|
{
|
||||||
var r = remnants[i];
|
var r = remnants[i];
|
||||||
sb.AppendLine($" Remnant {i}: ({r.X:F1},{r.Y:F1}) {r.Width:F1}x{r.Height:F1}, area={r.Area():F1}");
|
sb.AppendLine($" Remnant {i}: ({r.X:F1},{r.Y:F1}) {r.Width:F1}x{r.Length:F1}, area={r.Area():F1}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
@@ -90,7 +90,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
sb.AppendLine($" [{i}] {part.BaseDrawing.Name}: " +
|
sb.AppendLine($" [{i}] {part.BaseDrawing.Name}: " +
|
||||||
$"loc=({part.Location.X:F2},{part.Location.Y:F2}), " +
|
$"loc=({part.Location.X:F2},{part.Location.Y:F2}), " +
|
||||||
$"rot={rotDeg:F1} deg, " +
|
$"rot={rotDeg:F1} deg, " +
|
||||||
$"bbox=({bbox.X:F2},{bbox.Y:F2} {bbox.Width:F2}x{bbox.Height:F2})");
|
$"bbox=({bbox.X:F2},{bbox.Y:F2} {bbox.Width:F2}x{bbox.Length:F2})");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plate.Parts.Count > limit)
|
if (plate.Parts.Count > limit)
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
var added = plate.Parts.Count - countBefore;
|
var added = plate.Parts.Count - countBefore;
|
||||||
totalAdded += added;
|
totalAdded += added;
|
||||||
|
|
||||||
sb.AppendLine($" Remnant {i}: ({remnant.X:F1},{remnant.Y:F1} {remnant.Width:F1}x{remnant.Height:F1}) -> {added} parts {(success ? "" : "(no fit)")}");
|
sb.AppendLine($" Remnant {i}: ({remnant.X:F1},{remnant.Y:F1} {remnant.Width:F1}x{remnant.Length:F1}) -> {added} parts {(success ? "" : "(no fit)")}");
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.AppendLine($"Total parts added: {totalAdded}");
|
sb.AppendLine($"Total parts added: {totalAdded}");
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ namespace OpenNest.Mcp.Tools
|
|||||||
var work = plate.WorkArea();
|
var work = plate.WorkArea();
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine($"Created plate {index}: {plate.Size.Width:F1} x {plate.Size.Height:F1}");
|
sb.AppendLine($"Created plate {index}: {plate.Size.Width:F1} x {plate.Size.Length:F1}");
|
||||||
sb.AppendLine($" Quadrant: {plate.Quadrant}");
|
sb.AppendLine($" Quadrant: {plate.Quadrant}");
|
||||||
sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}");
|
sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}");
|
||||||
sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}");
|
sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}");
|
||||||
sb.AppendLine($" Work area: {work.Width:F1} x {work.Height:F1}");
|
sb.AppendLine($" Work area: {work.Width:F1} x {work.Length:F1}");
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ namespace OpenNest.Actions
|
|||||||
var location = plateView.PointWorldToGraph(SelectedArea.Location);
|
var location = plateView.PointWorldToGraph(SelectedArea.Location);
|
||||||
var size = new SizeF(
|
var size = new SizeF(
|
||||||
plateView.LengthWorldToGui(SelectedArea.Width),
|
plateView.LengthWorldToGui(SelectedArea.Width),
|
||||||
plateView.LengthWorldToGui(SelectedArea.Height));
|
plateView.LengthWorldToGui(SelectedArea.Length));
|
||||||
|
|
||||||
var rect = new System.Drawing.RectangleF(location.X, location.Y - size.Height, size.Width, size.Height);
|
var rect = new System.Drawing.RectangleF(location.X, location.Y - size.Height, size.Width, size.Height);
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
public virtual void ZoomToArea(Box box, bool redraw = true)
|
public virtual void ZoomToArea(Box box, bool redraw = true)
|
||||||
{
|
{
|
||||||
ZoomToArea(box.X, box.Y, box.Width, box.Height, redraw);
|
ZoomToArea(box.X, box.Y, box.Width, box.Length, redraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void ZoomToArea(double x, double y, double width, double height, bool redraw = true)
|
public virtual void ZoomToArea(double x, double y, double width, double height, bool redraw = true)
|
||||||
|
|||||||
@@ -384,13 +384,13 @@ namespace OpenNest.Controls
|
|||||||
var plateRect = new RectangleF
|
var plateRect = new RectangleF
|
||||||
{
|
{
|
||||||
Width = LengthWorldToGui(Plate.Size.Width),
|
Width = LengthWorldToGui(Plate.Size.Width),
|
||||||
Height = LengthWorldToGui(Plate.Size.Height)
|
Height = LengthWorldToGui(Plate.Size.Length)
|
||||||
};
|
};
|
||||||
|
|
||||||
var edgeSpacingRect = new RectangleF
|
var edgeSpacingRect = new RectangleF
|
||||||
{
|
{
|
||||||
Width = LengthWorldToGui(Plate.Size.Width - Plate.EdgeSpacing.Left - Plate.EdgeSpacing.Right),
|
Width = LengthWorldToGui(Plate.Size.Width - Plate.EdgeSpacing.Left - Plate.EdgeSpacing.Right),
|
||||||
Height = LengthWorldToGui(Plate.Size.Height - Plate.EdgeSpacing.Top - Plate.EdgeSpacing.Bottom)
|
Height = LengthWorldToGui(Plate.Size.Length - Plate.EdgeSpacing.Top - Plate.EdgeSpacing.Bottom)
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (Plate.Quadrant)
|
switch (Plate.Quadrant)
|
||||||
@@ -410,17 +410,17 @@ namespace OpenNest.Controls
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
plateRect.Location = PointWorldToGraph(-Plate.Size.Width, -Plate.Size.Height);
|
plateRect.Location = PointWorldToGraph(-Plate.Size.Width, -Plate.Size.Length);
|
||||||
edgeSpacingRect.Location = PointWorldToGraph(
|
edgeSpacingRect.Location = PointWorldToGraph(
|
||||||
Plate.EdgeSpacing.Left - Plate.Size.Width,
|
Plate.EdgeSpacing.Left - Plate.Size.Width,
|
||||||
Plate.EdgeSpacing.Bottom - Plate.Size.Height);
|
Plate.EdgeSpacing.Bottom - Plate.Size.Length);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
plateRect.Location = PointWorldToGraph(0, -Plate.Size.Height);
|
plateRect.Location = PointWorldToGraph(0, -Plate.Size.Length);
|
||||||
edgeSpacingRect.Location = PointWorldToGraph(
|
edgeSpacingRect.Location = PointWorldToGraph(
|
||||||
Plate.EdgeSpacing.Left,
|
Plate.EdgeSpacing.Left,
|
||||||
Plate.EdgeSpacing.Bottom - Plate.Size.Height);
|
Plate.EdgeSpacing.Bottom - Plate.Size.Length);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -590,7 +590,7 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
Location = PointWorldToGraph(box.Location),
|
Location = PointWorldToGraph(box.Location),
|
||||||
Width = LengthWorldToGui(box.Width),
|
Width = LengthWorldToGui(box.Width),
|
||||||
Height = LengthWorldToGui(box.Height)
|
Height = LengthWorldToGui(box.Length)
|
||||||
};
|
};
|
||||||
|
|
||||||
g.DrawRectangle(ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height);
|
g.DrawRectangle(ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ namespace OpenNest.Forms
|
|||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
var results = BestFitCache.GetOrCompute(
|
var results = BestFitCache.GetOrCompute(
|
||||||
drawing, plate.Size.Width, plate.Size.Height, plate.PartSpacing);
|
drawing, plate.Size.Width, plate.Size.Length, plate.PartSpacing);
|
||||||
|
|
||||||
var findMs = sw.ElapsedMilliseconds;
|
var findMs = sw.ElapsedMilliseconds;
|
||||||
var total = results.Count;
|
var total = results.Count;
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ namespace OpenNest.Forms
|
|||||||
drawing.Source.Path = item.Path;
|
drawing.Source.Path = item.Path;
|
||||||
drawing.Quantity.Required = item.Quantity;
|
drawing.Quantity.Required = item.Quantity;
|
||||||
|
|
||||||
var shape = new DefinedShape(entities);
|
var shape = new ShapeProfile(entities);
|
||||||
|
|
||||||
SetRotation(shape.Perimeter, RotationType.CW);
|
SetRotation(shape.Perimeter, RotationType.CW);
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ namespace OpenNest.Forms
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TopSpacing + BottomSpacing >= size.Height)
|
if (TopSpacing + BottomSpacing >= size.Length)
|
||||||
{
|
{
|
||||||
applyButton.Enabled = false;
|
applyButton.Enabled = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ namespace OpenNest.Forms
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TopSpacing + BottomSpacing >= size.Height)
|
if (TopSpacing + BottomSpacing >= size.Length)
|
||||||
{
|
{
|
||||||
applyButton.Enabled = false;
|
applyButton.Enabled = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ namespace OpenNest.Forms
|
|||||||
UpdateStatus();
|
UpdateStatus();
|
||||||
UpdateGpuStatus();
|
UpdateGpuStatus();
|
||||||
|
|
||||||
if (GpuEvaluatorFactory.GpuAvailable)
|
//if (GpuEvaluatorFactory.GpuAvailable)
|
||||||
BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
|
// BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetNestName(DateTime date, int id)
|
private string GetNestName(DateTime date, int id)
|
||||||
|
|||||||
1218
docs/superpowers/plans/2026-03-12-cutting-strategy.md
Normal file
1218
docs/superpowers/plans/2026-03-12-cutting-strategy.md
Normal file
File diff suppressed because it is too large
Load Diff
767
docs/superpowers/plans/2026-03-12-nest-file-format-v2.md
Normal file
767
docs/superpowers/plans/2026-03-12-nest-file-format-v2.md
Normal file
@@ -0,0 +1,767 @@
|
|||||||
|
# Nest File Format v2 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace the XML+G-code nest file format with a single `nest.json` metadata file plus `programs/` folder inside the ZIP archive.
|
||||||
|
|
||||||
|
**Architecture:** Add a `NestFormat` static class containing DTO records and shared JSON options. Rewrite `NestWriter` to serialize DTOs to JSON and write programs under `programs/`. Rewrite `NestReader` to deserialize JSON and read programs from `programs/`. Public API unchanged.
|
||||||
|
|
||||||
|
**Tech Stack:** `System.Text.Json` (built into .NET 8, no new packages needed)
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-03-12-nest-file-format-v2-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| Action | File | Responsibility |
|
||||||
|
|--------|------|----------------|
|
||||||
|
| Create | `OpenNest.IO/NestFormat.cs` | DTO records for JSON serialization + shared `JsonSerializerOptions` |
|
||||||
|
| Rewrite | `OpenNest.IO/NestWriter.cs` | Serialize nest to JSON + write programs to `programs/` folder |
|
||||||
|
| Rewrite | `OpenNest.IO/NestReader.cs` | Deserialize JSON + read programs from `programs/` folder |
|
||||||
|
|
||||||
|
No other files change. `ProgramReader.cs`, `DxfImporter.cs`, `DxfExporter.cs`, `Extensions.cs`, all domain model classes, and all caller sites remain untouched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 1: DTO Records and JSON Options
|
||||||
|
|
||||||
|
### Task 1: Create NestFormat.cs with DTO records
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `OpenNest.IO/NestFormat.cs`
|
||||||
|
|
||||||
|
These DTOs are the JSON shape — flat records that map 1:1 with the spec's JSON schema. They live in `OpenNest.IO` because they're serialization concerns, not domain model.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `NestFormat.cs`**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public static class NestFormat
|
||||||
|
{
|
||||||
|
public static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public record NestDto
|
||||||
|
{
|
||||||
|
public int Version { get; init; } = 2;
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Units { get; init; } = "Inches";
|
||||||
|
public string Customer { get; init; } = "";
|
||||||
|
public string DateCreated { get; init; } = "";
|
||||||
|
public string DateLastModified { get; init; } = "";
|
||||||
|
public string Notes { get; init; } = "";
|
||||||
|
public PlateDefaultsDto PlateDefaults { get; init; } = new();
|
||||||
|
public List<DrawingDto> Drawings { get; init; } = new();
|
||||||
|
public List<PlateDto> Plates { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PlateDefaultsDto
|
||||||
|
{
|
||||||
|
public SizeDto Size { get; init; } = new();
|
||||||
|
public double Thickness { get; init; }
|
||||||
|
public int Quadrant { get; init; } = 1;
|
||||||
|
public double PartSpacing { get; init; }
|
||||||
|
public MaterialDto Material { get; init; } = new();
|
||||||
|
public SpacingDto EdgeSpacing { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DrawingDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Customer { get; init; } = "";
|
||||||
|
public ColorDto Color { get; init; } = new();
|
||||||
|
public QuantityDto Quantity { get; init; } = new();
|
||||||
|
public int Priority { get; init; }
|
||||||
|
public ConstraintsDto Constraints { get; init; } = new();
|
||||||
|
public MaterialDto Material { get; init; } = new();
|
||||||
|
public SourceDto Source { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PlateDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public SizeDto Size { get; init; } = new();
|
||||||
|
public double Thickness { get; init; }
|
||||||
|
public int Quadrant { get; init; } = 1;
|
||||||
|
public int Quantity { get; init; } = 1;
|
||||||
|
public double PartSpacing { get; init; }
|
||||||
|
public MaterialDto Material { get; init; } = new();
|
||||||
|
public SpacingDto EdgeSpacing { get; init; } = new();
|
||||||
|
public List<PartDto> Parts { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PartDto
|
||||||
|
{
|
||||||
|
public int DrawingId { get; init; }
|
||||||
|
public double X { get; init; }
|
||||||
|
public double Y { get; init; }
|
||||||
|
public double Rotation { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SizeDto
|
||||||
|
{
|
||||||
|
public double Width { get; init; }
|
||||||
|
public double Height { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record MaterialDto
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Grade { get; init; } = "";
|
||||||
|
public double Density { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SpacingDto
|
||||||
|
{
|
||||||
|
public double Left { get; init; }
|
||||||
|
public double Top { get; init; }
|
||||||
|
public double Right { get; init; }
|
||||||
|
public double Bottom { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ColorDto
|
||||||
|
{
|
||||||
|
public int A { get; init; } = 255;
|
||||||
|
public int R { get; init; }
|
||||||
|
public int G { get; init; }
|
||||||
|
public int B { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record QuantityDto
|
||||||
|
{
|
||||||
|
public int Required { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ConstraintsDto
|
||||||
|
{
|
||||||
|
public double StepAngle { get; init; }
|
||||||
|
public double StartAngle { get; init; }
|
||||||
|
public double EndAngle { get; init; }
|
||||||
|
public bool Allow180Equivalent { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SourceDto
|
||||||
|
{
|
||||||
|
public string Path { get; init; } = "";
|
||||||
|
public OffsetDto Offset { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record OffsetDto
|
||||||
|
{
|
||||||
|
public double X { get; init; }
|
||||||
|
public double Y { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build to verify DTOs compile**
|
||||||
|
|
||||||
|
Run: `dotnet build OpenNest.IO/OpenNest.IO.csproj`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add OpenNest.IO/NestFormat.cs
|
||||||
|
git commit -m "feat: add NestFormat DTOs for JSON nest file format v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 2: Rewrite NestWriter
|
||||||
|
|
||||||
|
### Task 2: Rewrite NestWriter to use JSON serialization
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Rewrite: `OpenNest.IO/NestWriter.cs`
|
||||||
|
|
||||||
|
The writer keeps the same public API: `NestWriter(Nest nest)` constructor and `bool Write(string file)`. Internally it builds a `NestDto` from the domain model, serializes it to `nest.json`, and writes each drawing's program to `programs/program-N`.
|
||||||
|
|
||||||
|
The G-code writing methods (`WriteDrawing`, `GetCodeString`, `GetLayerString`) are preserved exactly — they write program G-code to streams, which is unchanged. The `WritePlate` method and all XML methods (`AddNestInfo`, `AddPlateInfo`, `AddDrawingInfo`) are removed.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Rewrite `NestWriter.cs`**
|
||||||
|
|
||||||
|
Replace the entire file. Key changes:
|
||||||
|
- Remove `using System.Xml`
|
||||||
|
- Add `using System.Text.Json`
|
||||||
|
- Remove `AddNestInfo()`, `AddPlateInfo()`, `AddDrawingInfo()`, `AddPlates()`, `WritePlate()` methods
|
||||||
|
- Add `BuildNestDto()` method that maps domain model → DTOs
|
||||||
|
- `Write()` now serializes `NestDto` to `nest.json` and writes programs to `programs/program-N`
|
||||||
|
- Keep `WriteDrawing()`, `GetCodeString()`, `GetLayerString()` exactly as-is
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public sealed class NestWriter
|
||||||
|
{
|
||||||
|
private const int OutputPrecision = 10;
|
||||||
|
private const string CoordinateFormat = "0.##########";
|
||||||
|
|
||||||
|
private readonly Nest nest;
|
||||||
|
private Dictionary<int, Drawing> drawingDict;
|
||||||
|
|
||||||
|
public NestWriter(Nest nest)
|
||||||
|
{
|
||||||
|
this.drawingDict = new Dictionary<int, Drawing>();
|
||||||
|
this.nest = nest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Write(string file)
|
||||||
|
{
|
||||||
|
nest.DateLastModified = DateTime.Now;
|
||||||
|
SetDrawingIds();
|
||||||
|
|
||||||
|
using var fileStream = new FileStream(file, FileMode.Create);
|
||||||
|
using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create);
|
||||||
|
|
||||||
|
WriteNestJson(zipArchive);
|
||||||
|
WritePrograms(zipArchive);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDrawingIds()
|
||||||
|
{
|
||||||
|
var id = 1;
|
||||||
|
foreach (var drawing in nest.Drawings)
|
||||||
|
{
|
||||||
|
drawingDict.Add(id, drawing);
|
||||||
|
id++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteNestJson(ZipArchive zipArchive)
|
||||||
|
{
|
||||||
|
var dto = BuildNestDto();
|
||||||
|
var json = JsonSerializer.Serialize(dto, JsonOptions);
|
||||||
|
|
||||||
|
var entry = zipArchive.CreateEntry("nest.json");
|
||||||
|
using var stream = entry.Open();
|
||||||
|
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||||
|
writer.Write(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NestDto BuildNestDto()
|
||||||
|
{
|
||||||
|
return new NestDto
|
||||||
|
{
|
||||||
|
Version = 2,
|
||||||
|
Name = nest.Name ?? "",
|
||||||
|
Units = nest.Units.ToString(),
|
||||||
|
Customer = nest.Customer ?? "",
|
||||||
|
DateCreated = nest.DateCreated.ToString("o"),
|
||||||
|
DateLastModified = nest.DateLastModified.ToString("o"),
|
||||||
|
Notes = nest.Notes ?? "",
|
||||||
|
PlateDefaults = BuildPlateDefaultsDto(),
|
||||||
|
Drawings = BuildDrawingDtos(),
|
||||||
|
Plates = BuildPlateDtos()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlateDefaultsDto BuildPlateDefaultsDto()
|
||||||
|
{
|
||||||
|
var pd = nest.PlateDefaults;
|
||||||
|
return new PlateDefaultsDto
|
||||||
|
{
|
||||||
|
Size = new SizeDto { Width = pd.Size.Width, Height = pd.Size.Height },
|
||||||
|
Thickness = pd.Thickness,
|
||||||
|
Quadrant = pd.Quadrant,
|
||||||
|
PartSpacing = pd.PartSpacing,
|
||||||
|
Material = new MaterialDto
|
||||||
|
{
|
||||||
|
Name = pd.Material.Name ?? "",
|
||||||
|
Grade = pd.Material.Grade ?? "",
|
||||||
|
Density = pd.Material.Density
|
||||||
|
},
|
||||||
|
EdgeSpacing = new SpacingDto
|
||||||
|
{
|
||||||
|
Left = pd.EdgeSpacing.Left,
|
||||||
|
Top = pd.EdgeSpacing.Top,
|
||||||
|
Right = pd.EdgeSpacing.Right,
|
||||||
|
Bottom = pd.EdgeSpacing.Bottom
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DrawingDto> BuildDrawingDtos()
|
||||||
|
{
|
||||||
|
var list = new List<DrawingDto>();
|
||||||
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
|
{
|
||||||
|
var d = kvp.Value;
|
||||||
|
list.Add(new DrawingDto
|
||||||
|
{
|
||||||
|
Id = kvp.Key,
|
||||||
|
Name = d.Name ?? "",
|
||||||
|
Customer = d.Customer ?? "",
|
||||||
|
Color = new ColorDto { A = d.Color.A, R = d.Color.R, G = d.Color.G, B = d.Color.B },
|
||||||
|
Quantity = new QuantityDto { Required = d.Quantity.Required },
|
||||||
|
Priority = d.Priority,
|
||||||
|
Constraints = new ConstraintsDto
|
||||||
|
{
|
||||||
|
StepAngle = d.Constraints.StepAngle,
|
||||||
|
StartAngle = d.Constraints.StartAngle,
|
||||||
|
EndAngle = d.Constraints.EndAngle,
|
||||||
|
Allow180Equivalent = d.Constraints.Allow180Equivalent
|
||||||
|
},
|
||||||
|
Material = new MaterialDto
|
||||||
|
{
|
||||||
|
Name = d.Material.Name ?? "",
|
||||||
|
Grade = d.Material.Grade ?? "",
|
||||||
|
Density = d.Material.Density
|
||||||
|
},
|
||||||
|
Source = new SourceDto
|
||||||
|
{
|
||||||
|
Path = d.Source.Path ?? "",
|
||||||
|
Offset = new OffsetDto { X = d.Source.Offset.X, Y = d.Source.Offset.Y }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PlateDto> BuildPlateDtos()
|
||||||
|
{
|
||||||
|
var list = new List<PlateDto>();
|
||||||
|
for (var i = 0; i < nest.Plates.Count; i++)
|
||||||
|
{
|
||||||
|
var plate = nest.Plates[i];
|
||||||
|
var parts = new List<PartDto>();
|
||||||
|
foreach (var part in plate.Parts)
|
||||||
|
{
|
||||||
|
var match = drawingDict.Where(dwg => dwg.Value == part.BaseDrawing).FirstOrDefault();
|
||||||
|
parts.Add(new PartDto
|
||||||
|
{
|
||||||
|
DrawingId = match.Key,
|
||||||
|
X = part.Location.X,
|
||||||
|
Y = part.Location.Y,
|
||||||
|
Rotation = part.Rotation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new PlateDto
|
||||||
|
{
|
||||||
|
Id = i + 1,
|
||||||
|
Size = new SizeDto { Width = plate.Size.Width, Height = plate.Size.Height },
|
||||||
|
Thickness = plate.Thickness,
|
||||||
|
Quadrant = plate.Quadrant,
|
||||||
|
Quantity = plate.Quantity,
|
||||||
|
PartSpacing = plate.PartSpacing,
|
||||||
|
Material = new MaterialDto
|
||||||
|
{
|
||||||
|
Name = plate.Material.Name ?? "",
|
||||||
|
Grade = plate.Material.Grade ?? "",
|
||||||
|
Density = plate.Material.Density
|
||||||
|
},
|
||||||
|
EdgeSpacing = new SpacingDto
|
||||||
|
{
|
||||||
|
Left = plate.EdgeSpacing.Left,
|
||||||
|
Top = plate.EdgeSpacing.Top,
|
||||||
|
Right = plate.EdgeSpacing.Right,
|
||||||
|
Bottom = plate.EdgeSpacing.Bottom
|
||||||
|
},
|
||||||
|
Parts = parts
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WritePrograms(ZipArchive zipArchive)
|
||||||
|
{
|
||||||
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
|
{
|
||||||
|
var name = $"programs/program-{kvp.Key}";
|
||||||
|
var stream = new MemoryStream();
|
||||||
|
WriteDrawing(stream, kvp.Value);
|
||||||
|
|
||||||
|
var entry = zipArchive.CreateEntry(name);
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
stream.CopyTo(entryStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteDrawing(Stream stream, Drawing drawing)
|
||||||
|
{
|
||||||
|
var program = drawing.Program;
|
||||||
|
var writer = new StreamWriter(stream);
|
||||||
|
writer.AutoFlush = true;
|
||||||
|
|
||||||
|
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
|
||||||
|
|
||||||
|
for (var i = 0; i < drawing.Program.Length; ++i)
|
||||||
|
{
|
||||||
|
var code = drawing.Program[i];
|
||||||
|
writer.WriteLine(GetCodeString(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCodeString(ICode code)
|
||||||
|
{
|
||||||
|
switch (code.Type)
|
||||||
|
{
|
||||||
|
case CodeType.ArcMove:
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var arcMove = (ArcMove)code;
|
||||||
|
|
||||||
|
var x = System.Math.Round(arcMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat);
|
||||||
|
var y = System.Math.Round(arcMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat);
|
||||||
|
var i = System.Math.Round(arcMove.CenterPoint.X, OutputPrecision).ToString(CoordinateFormat);
|
||||||
|
var j = System.Math.Round(arcMove.CenterPoint.Y, OutputPrecision).ToString(CoordinateFormat);
|
||||||
|
|
||||||
|
if (arcMove.Rotation == RotationType.CW)
|
||||||
|
sb.Append(string.Format("G02X{0}Y{1}I{2}J{3}", x, y, i, j));
|
||||||
|
else
|
||||||
|
sb.Append(string.Format("G03X{0}Y{1}I{2}J{3}", x, y, i, j));
|
||||||
|
|
||||||
|
if (arcMove.Layer != LayerType.Cut)
|
||||||
|
sb.Append(GetLayerString(arcMove.Layer));
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.Comment:
|
||||||
|
{
|
||||||
|
var comment = (Comment)code;
|
||||||
|
return ":" + comment.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.LinearMove:
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var linearMove = (LinearMove)code;
|
||||||
|
|
||||||
|
sb.Append(string.Format("G01X{0}Y{1}",
|
||||||
|
System.Math.Round(linearMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat),
|
||||||
|
System.Math.Round(linearMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat)));
|
||||||
|
|
||||||
|
if (linearMove.Layer != LayerType.Cut)
|
||||||
|
sb.Append(GetLayerString(linearMove.Layer));
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.RapidMove:
|
||||||
|
{
|
||||||
|
var rapidMove = (RapidMove)code;
|
||||||
|
|
||||||
|
return string.Format("G00X{0}Y{1}",
|
||||||
|
System.Math.Round(rapidMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat),
|
||||||
|
System.Math.Round(rapidMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.SetFeedrate:
|
||||||
|
{
|
||||||
|
var setFeedrate = (Feedrate)code;
|
||||||
|
return "F" + setFeedrate.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.SetKerf:
|
||||||
|
{
|
||||||
|
var setKerf = (Kerf)code;
|
||||||
|
|
||||||
|
switch (setKerf.Value)
|
||||||
|
{
|
||||||
|
case KerfType.None: return "G40";
|
||||||
|
case KerfType.Left: return "G41";
|
||||||
|
case KerfType.Right: return "G42";
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.SubProgramCall:
|
||||||
|
{
|
||||||
|
var subProgramCall = (SubProgramCall)code;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLayerString(LayerType layer)
|
||||||
|
{
|
||||||
|
switch (layer)
|
||||||
|
{
|
||||||
|
case LayerType.Display:
|
||||||
|
return ":DISPLAY";
|
||||||
|
|
||||||
|
case LayerType.Leadin:
|
||||||
|
return ":LEADIN";
|
||||||
|
|
||||||
|
case LayerType.Leadout:
|
||||||
|
return ":LEADOUT";
|
||||||
|
|
||||||
|
case LayerType.Scribe:
|
||||||
|
return ":SCRIBE";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build to verify NestWriter compiles**
|
||||||
|
|
||||||
|
Run: `dotnet build OpenNest.sln`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add OpenNest.IO/NestWriter.cs
|
||||||
|
git commit -m "feat: rewrite NestWriter to use JSON format v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 3: Rewrite NestReader
|
||||||
|
|
||||||
|
### Task 3: Rewrite NestReader to use JSON deserialization
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Rewrite: `OpenNest.IO/NestReader.cs`
|
||||||
|
|
||||||
|
The reader keeps the same public API: `NestReader(string file)`, `NestReader(Stream stream)`, and `Nest Read()`. Internally it reads `nest.json`, deserializes to `NestDto`, reads programs from `programs/program-N`, and assembles the domain model.
|
||||||
|
|
||||||
|
All XML parsing, plate G-code parsing, dictionary-linking (`LinkProgramsToDrawings`, `LinkPartsToPlates`), and the helper enums/methods are removed.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Rewrite `NestReader.cs`**
|
||||||
|
|
||||||
|
Replace the entire file:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public sealed class NestReader
|
||||||
|
{
|
||||||
|
private readonly Stream stream;
|
||||||
|
private readonly ZipArchive zipArchive;
|
||||||
|
|
||||||
|
public NestReader(string file)
|
||||||
|
{
|
||||||
|
stream = new FileStream(file, FileMode.Open, FileAccess.Read);
|
||||||
|
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NestReader(Stream stream)
|
||||||
|
{
|
||||||
|
this.stream = stream;
|
||||||
|
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Nest Read()
|
||||||
|
{
|
||||||
|
var nestJson = ReadEntry("nest.json");
|
||||||
|
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
|
||||||
|
|
||||||
|
var programs = ReadPrograms(dto.Drawings.Count);
|
||||||
|
var drawingMap = BuildDrawings(dto, programs);
|
||||||
|
var nest = BuildNest(dto, drawingMap);
|
||||||
|
|
||||||
|
zipArchive.Dispose();
|
||||||
|
stream.Close();
|
||||||
|
|
||||||
|
return nest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReadEntry(string name)
|
||||||
|
{
|
||||||
|
var entry = zipArchive.GetEntry(name)
|
||||||
|
?? throw new InvalidDataException($"Nest file is missing required entry '{name}'.");
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
using var reader = new StreamReader(entryStream);
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, Program> ReadPrograms(int count)
|
||||||
|
{
|
||||||
|
var programs = new Dictionary<int, Program>();
|
||||||
|
for (var i = 1; i <= count; i++)
|
||||||
|
{
|
||||||
|
var entry = zipArchive.GetEntry($"programs/program-{i}");
|
||||||
|
if (entry == null) continue;
|
||||||
|
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
var memStream = new MemoryStream();
|
||||||
|
entryStream.CopyTo(memStream);
|
||||||
|
memStream.Position = 0;
|
||||||
|
|
||||||
|
var reader = new ProgramReader(memStream);
|
||||||
|
programs[i] = reader.Read();
|
||||||
|
}
|
||||||
|
return programs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, Drawing> BuildDrawings(NestDto dto, Dictionary<int, Program> programs)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<int, Drawing>();
|
||||||
|
foreach (var d in dto.Drawings)
|
||||||
|
{
|
||||||
|
var drawing = new Drawing(d.Name);
|
||||||
|
drawing.Customer = d.Customer;
|
||||||
|
drawing.Color = Color.FromArgb(d.Color.A, d.Color.R, d.Color.G, d.Color.B);
|
||||||
|
drawing.Quantity.Required = d.Quantity.Required;
|
||||||
|
drawing.Priority = d.Priority;
|
||||||
|
drawing.Constraints.StepAngle = d.Constraints.StepAngle;
|
||||||
|
drawing.Constraints.StartAngle = d.Constraints.StartAngle;
|
||||||
|
drawing.Constraints.EndAngle = d.Constraints.EndAngle;
|
||||||
|
drawing.Constraints.Allow180Equivalent = d.Constraints.Allow180Equivalent;
|
||||||
|
drawing.Material = new Material(d.Material.Name, d.Material.Grade, d.Material.Density);
|
||||||
|
drawing.Source.Path = d.Source.Path;
|
||||||
|
drawing.Source.Offset = new Vector(d.Source.Offset.X, d.Source.Offset.Y);
|
||||||
|
|
||||||
|
if (programs.TryGetValue(d.Id, out var pgm))
|
||||||
|
drawing.Program = pgm;
|
||||||
|
|
||||||
|
map[d.Id] = drawing;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Nest BuildNest(NestDto dto, Dictionary<int, Drawing> drawingMap)
|
||||||
|
{
|
||||||
|
var nest = new Nest();
|
||||||
|
nest.Name = dto.Name;
|
||||||
|
|
||||||
|
Units units;
|
||||||
|
if (Enum.TryParse(dto.Units, true, out units))
|
||||||
|
nest.Units = units;
|
||||||
|
|
||||||
|
nest.Customer = dto.Customer;
|
||||||
|
nest.DateCreated = DateTime.Parse(dto.DateCreated);
|
||||||
|
nest.DateLastModified = DateTime.Parse(dto.DateLastModified);
|
||||||
|
nest.Notes = dto.Notes;
|
||||||
|
|
||||||
|
// Plate defaults
|
||||||
|
var pd = dto.PlateDefaults;
|
||||||
|
nest.PlateDefaults.Size = new Size(pd.Size.Width, pd.Size.Height);
|
||||||
|
nest.PlateDefaults.Thickness = pd.Thickness;
|
||||||
|
nest.PlateDefaults.Quadrant = pd.Quadrant;
|
||||||
|
nest.PlateDefaults.PartSpacing = pd.PartSpacing;
|
||||||
|
nest.PlateDefaults.Material = new Material(pd.Material.Name, pd.Material.Grade, pd.Material.Density);
|
||||||
|
nest.PlateDefaults.EdgeSpacing = new Spacing(pd.EdgeSpacing.Left, pd.EdgeSpacing.Bottom, pd.EdgeSpacing.Right, pd.EdgeSpacing.Top);
|
||||||
|
|
||||||
|
// Drawings
|
||||||
|
foreach (var d in drawingMap.OrderBy(k => k.Key))
|
||||||
|
nest.Drawings.Add(d.Value);
|
||||||
|
|
||||||
|
// Plates
|
||||||
|
foreach (var p in dto.Plates.OrderBy(p => p.Id))
|
||||||
|
{
|
||||||
|
var plate = new Plate();
|
||||||
|
plate.Size = new Size(p.Size.Width, p.Size.Height);
|
||||||
|
plate.Thickness = p.Thickness;
|
||||||
|
plate.Quadrant = p.Quadrant;
|
||||||
|
plate.Quantity = p.Quantity;
|
||||||
|
plate.PartSpacing = p.PartSpacing;
|
||||||
|
plate.Material = new Material(p.Material.Name, p.Material.Grade, p.Material.Density);
|
||||||
|
plate.EdgeSpacing = new Spacing(p.EdgeSpacing.Left, p.EdgeSpacing.Bottom, p.EdgeSpacing.Right, p.EdgeSpacing.Top);
|
||||||
|
|
||||||
|
foreach (var partDto in p.Parts)
|
||||||
|
{
|
||||||
|
if (!drawingMap.TryGetValue(partDto.DrawingId, out var dwg))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var part = new Part(dwg);
|
||||||
|
part.Rotate(partDto.Rotation);
|
||||||
|
part.Offset(new Vector(partDto.X, partDto.Y));
|
||||||
|
plate.Parts.Add(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
nest.Plates.Add(plate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build to verify NestReader compiles**
|
||||||
|
|
||||||
|
Run: `dotnet build OpenNest.sln`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add OpenNest.IO/NestReader.cs
|
||||||
|
git commit -m "feat: rewrite NestReader to use JSON format v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 4: Smoke Test
|
||||||
|
|
||||||
|
### Task 4: Manual smoke test via OpenNest.Console
|
||||||
|
|
||||||
|
**Files:** None modified — this is a verification step.
|
||||||
|
|
||||||
|
Use the `OpenNest.Console` project (or the MCP server) to verify round-trip: create a nest, save it, reload it, confirm data is intact.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Build the full solution**
|
||||||
|
|
||||||
|
Run: `dotnet build OpenNest.sln`
|
||||||
|
Expected: Build succeeded with no errors.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Round-trip test via MCP tools**
|
||||||
|
|
||||||
|
Use the OpenNest MCP tools to:
|
||||||
|
1. Create a drawing (e.g. a rectangle via `create_drawing`)
|
||||||
|
2. Create a plate via `create_plate`
|
||||||
|
3. Fill the plate via `fill_plate`
|
||||||
|
4. Save the nest via the console app or verify `get_plate_info` shows parts
|
||||||
|
5. If a nest file exists on disk, load it with `load_nest` and verify `get_plate_info` returns the same data
|
||||||
|
|
||||||
|
- [ ] **Step 3: Inspect the ZIP contents**
|
||||||
|
|
||||||
|
Unzip a saved nest file and verify:
|
||||||
|
- `nest.json` exists with correct structure
|
||||||
|
- `programs/program-1` (etc.) exist with G-code content
|
||||||
|
- No `info`, `drawing-info`, `plate-info`, or `plate-NNN` files exist
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit any fixes**
|
||||||
|
|
||||||
|
If any issues were found and fixed, commit them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -u
|
||||||
|
git commit -m "fix: address issues found during nest format v2 smoke test"
|
||||||
|
```
|
||||||
134
docs/superpowers/specs/2026-03-12-contour-reindexing-design.md
Normal file
134
docs/superpowers/specs/2026-03-12-contour-reindexing-design.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Contour Re-Indexing Design
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Add entity-splitting primitives and a `Shape.ReindexAt` method so that a closed contour can be reordered to start (and end) at an arbitrary point. Then wire this into `ContourCuttingStrategy.Apply()` to replace the `NotImplementedException` stubs.
|
||||||
|
|
||||||
|
All geometry additions live on existing classes in `OpenNest.Geometry`. The strategy wiring is a change to the existing `ContourCuttingStrategy` in `OpenNest.CNC.CuttingStrategy`.
|
||||||
|
|
||||||
|
## Entity Splitting Primitives
|
||||||
|
|
||||||
|
### Line.SplitAt(Vector point)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public (Line first, Line second) SplitAt(Vector point)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Returns two lines: `StartPoint → point` and `point → EndPoint`.
|
||||||
|
- If the point is at `StartPoint` (within `Tolerance.Epsilon` distance), `first` is null.
|
||||||
|
- If the point is at `EndPoint` (within `Tolerance.Epsilon` distance), `second` is null.
|
||||||
|
- The point is assumed to lie on the line (caller is responsible — it comes from `ClosestPointTo`).
|
||||||
|
|
||||||
|
### Arc.SplitAt(Vector point)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public (Arc first, Arc second) SplitAt(Vector point)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Computes `splitAngle = Center.AngleTo(point)`, normalized via `Angle.NormalizeRad`.
|
||||||
|
- First arc: same center, radius, direction — `StartAngle → splitAngle`.
|
||||||
|
- Second arc: same center, radius, direction — `splitAngle → EndAngle`.
|
||||||
|
- **Endpoint tolerance**: compare `point.DistanceTo(arc.StartPoint())` and `point.DistanceTo(arc.EndPoint())` rather than comparing angles directly. This avoids wrap-around issues at the 0/2π boundary.
|
||||||
|
- If the point is at `StartPoint()` (within `Tolerance.Epsilon` distance), `first` is null.
|
||||||
|
- If the point is at `EndPoint()` (within `Tolerance.Epsilon` distance), `second` is null.
|
||||||
|
|
||||||
|
### Circle — no conversion needed
|
||||||
|
|
||||||
|
Circles are kept as-is in `ReindexAt`. The `ConvertShapeToMoves` method handles circles directly by emitting an `ArcMove` from the start point back to itself (a full circle), matching the existing `ConvertGeometry.AddCircle` pattern. This avoids the problem of constructing a "full-sweep arc" where `StartAngle == EndAngle` would produce zero sweep.
|
||||||
|
|
||||||
|
## Shape.ReindexAt
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Shape ReindexAt(Vector point, Entity entity)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `point`: the start/end point for the reindexed contour (from `ClosestPointTo`).
|
||||||
|
- `entity`: the entity containing `point` (from `ClosestPointTo`'s `out` parameter).
|
||||||
|
- Returns a **new** Shape (does not modify the original). The new shape shares entity references with the original for unsplit entities — callers must not mutate either.
|
||||||
|
- Throws `ArgumentException` if `entity` is not found in `Entities`.
|
||||||
|
|
||||||
|
### Algorithm
|
||||||
|
|
||||||
|
1. If `entity` is a `Circle`:
|
||||||
|
- Return a new Shape with that single `Circle` entity and `point` stored for `ConvertShapeToMoves` to use as the start point.
|
||||||
|
|
||||||
|
2. Find the index `i` of `entity` in `Entities`. Throw `ArgumentException` if not found.
|
||||||
|
|
||||||
|
3. Split the entity at `point`:
|
||||||
|
- `Line` → `line.SplitAt(point)` → `(firstHalf, secondHalf)`
|
||||||
|
- `Arc` → `arc.SplitAt(point)` → `(firstHalf, secondHalf)`
|
||||||
|
|
||||||
|
4. Build the new entity list (skip null entries):
|
||||||
|
- `secondHalf` (if not null)
|
||||||
|
- `Entities[i+1]`, `Entities[i+2]`, ..., `Entities[count-1]` (after the split)
|
||||||
|
- `Entities[0]`, `Entities[1]`, ..., `Entities[i-1]` (before the split, wrapping around)
|
||||||
|
- `firstHalf` (if not null)
|
||||||
|
|
||||||
|
5. Return a new Shape with this entity list.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- **Point lands on entity boundary** (start/end of an entity): one half of the split is null. The reordering still works — it just starts from the next full entity.
|
||||||
|
- **Single-entity shape that is an Arc**: split produces two arcs, reorder is just `[secondHalf, firstHalf]`.
|
||||||
|
- **Single-entity Circle**: handled by step 1 — kept as Circle, converted to a full-circle ArcMove in `ConvertShapeToMoves`.
|
||||||
|
|
||||||
|
## Wiring into ContourCuttingStrategy
|
||||||
|
|
||||||
|
### Entity-to-ICode Conversion
|
||||||
|
|
||||||
|
Add a private method to `ContourCuttingStrategy`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `startPoint` parameter is needed for the Circle case (to know where the full-circle ArcMove starts).
|
||||||
|
|
||||||
|
Iterates `shape.Entities` and converts each to cutting moves using **absolute coordinates** (consistent with `ConvertGeometry`):
|
||||||
|
- `Line` → `LinearMove(line.EndPoint)`
|
||||||
|
- `Arc` → `ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW)`
|
||||||
|
- `Circle` → `ArcMove(startPoint, circle.Center, circle.Rotation)` — full circle from start point back to itself, matching `ConvertGeometry.AddCircle`
|
||||||
|
- Any other entity type → throw `InvalidOperationException`
|
||||||
|
|
||||||
|
No `RapidMove` between entities — they are contiguous in a reindexed shape. The lead-in already positions the head at the shape's start point.
|
||||||
|
|
||||||
|
### Replace NotImplementedException
|
||||||
|
|
||||||
|
In `ContourCuttingStrategy.Apply()`, replace the two `throw new NotImplementedException(...)` blocks:
|
||||||
|
|
||||||
|
**Cutout loop** (uses `cutout` shape variable):
|
||||||
|
```csharp
|
||||||
|
var reindexed = cutout.ReindexAt(closestPt, entity);
|
||||||
|
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Perimeter block** (uses `profile.Perimeter`):
|
||||||
|
```csharp
|
||||||
|
var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
|
||||||
|
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
|
||||||
|
```
|
||||||
|
|
||||||
|
The full sequence for each contour becomes:
|
||||||
|
1. Lead-in codes (rapid to pierce point, cutting moves to contour start)
|
||||||
|
2. Contour body (reindexed entity moves from `ConvertShapeToMoves`)
|
||||||
|
3. Lead-out codes (overcut moves away from contour)
|
||||||
|
|
||||||
|
### MicrotabLeadOut Handling
|
||||||
|
|
||||||
|
When the lead-out is `MicrotabLeadOut`, the last cutting move must be trimmed by `GapSize`. This is a separate concern from re-indexing — stub it with a TODO comment for now. The trimming logic will shorten the last `LinearMove` or `ArcMove` in the contour body.
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `OpenNest.Core/Geometry/Line.cs` | Add `SplitAt(Vector)` method |
|
||||||
|
| `OpenNest.Core/Geometry/Arc.cs` | Add `SplitAt(Vector)` method |
|
||||||
|
| `OpenNest.Core/Geometry/Shape.cs` | Add `ReindexAt(Vector, Entity)` method |
|
||||||
|
| `OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs` | Add `ConvertShapeToMoves`, replace `NotImplementedException` blocks |
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- **MicrotabLeadOut trimming** (trim last move by gap size — stubbed with TODO)
|
||||||
|
- **Tab insertion** (inserting tab codes mid-contour — already stubbed)
|
||||||
|
- **Lead-in editor UI** (interactive start point selection — separate feature)
|
||||||
|
- **Contour re-indexing for open shapes** (only closed contours supported)
|
||||||
420
docs/superpowers/specs/2026-03-12-cutting-strategy-design.md
Normal file
420
docs/superpowers/specs/2026-03-12-cutting-strategy-design.md
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
# CNC Cutting Strategy Design
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Add lead-in, lead-out, and tab classes to `OpenNest.Core` that generate `ICode` instructions for CNC cutting approach/exit geometry. The strategy runs at nest-time — `ContourCuttingStrategy.Apply()` produces a new `Program` with lead-ins, lead-outs, start points, and contour ordering baked in. This modified program is what gets saved to the nest file and later fed to the post-processor for machine-specific G-code translation. The original `Drawing.Program` stays untouched; the strategy output lives on the `Part`.
|
||||||
|
|
||||||
|
All new code lives in `OpenNest.Core/CNC/CuttingStrategy/`.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
OpenNest.Core/CNC/CuttingStrategy/
|
||||||
|
├── LeadIns/
|
||||||
|
│ ├── LeadIn.cs
|
||||||
|
│ ├── NoLeadIn.cs
|
||||||
|
│ ├── LineLeadIn.cs
|
||||||
|
│ ├── LineArcLeadIn.cs
|
||||||
|
│ ├── ArcLeadIn.cs
|
||||||
|
│ ├── LineLineLeadIn.cs
|
||||||
|
│ └── CleanHoleLeadIn.cs
|
||||||
|
├── LeadOuts/
|
||||||
|
│ ├── LeadOut.cs
|
||||||
|
│ ├── NoLeadOut.cs
|
||||||
|
│ ├── LineLeadOut.cs
|
||||||
|
│ ├── ArcLeadOut.cs
|
||||||
|
│ └── MicrotabLeadOut.cs
|
||||||
|
├── Tabs/
|
||||||
|
│ ├── Tab.cs
|
||||||
|
│ ├── NormalTab.cs
|
||||||
|
│ ├── BreakerTab.cs
|
||||||
|
│ └── MachineTab.cs
|
||||||
|
├── ContourType.cs
|
||||||
|
├── CuttingParameters.cs
|
||||||
|
├── ContourCuttingStrategy.cs
|
||||||
|
├── SequenceParameters.cs
|
||||||
|
└── AssignmentParameters.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Namespace
|
||||||
|
|
||||||
|
All classes use `namespace OpenNest.CNC.CuttingStrategy`.
|
||||||
|
|
||||||
|
## Type Mappings from Original Spec
|
||||||
|
|
||||||
|
The original spec used placeholder names. These are the correct codebase types:
|
||||||
|
|
||||||
|
| Spec type | Actual type | Notes |
|
||||||
|
|-----------|------------|-------|
|
||||||
|
| `PointD` | `Vector` | `OpenNest.Geometry.Vector` — struct with `X`, `Y` fields |
|
||||||
|
| `CircularMove` | `ArcMove` | Constructor: `ArcMove(Vector endPoint, Vector centerPoint, RotationType rotation)` |
|
||||||
|
| `CircularDirection` | `RotationType` | Enum with `CW`, `CCW` |
|
||||||
|
| `value.ToRadians()` | `Angle.ToRadians(value)` | Static method on `OpenNest.Math.Angle` |
|
||||||
|
| `new Program(codes)` | Build manually | Create `Program()`, add to `.Codes` list |
|
||||||
|
|
||||||
|
## LeadIn Hierarchy
|
||||||
|
|
||||||
|
### Abstract Base: `LeadIn`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public abstract class LeadIn
|
||||||
|
{
|
||||||
|
public abstract List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW);
|
||||||
|
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `contourStartPoint`: where the contour cut begins (first point of the part profile).
|
||||||
|
- `contourNormalAngle`: normal angle (radians) at the contour start point, pointing **away from the part material** (outward from perimeter, into scrap for cutouts).
|
||||||
|
- `winding`: contour winding direction — arc-based lead-ins use this for their `ArcMove` rotation.
|
||||||
|
- `Generate` returns ICode instructions starting with a `RapidMove` to the pierce point, followed by cutting moves to reach the contour start.
|
||||||
|
- `GetPiercePoint` computes where the head rapids to before firing — useful for visualization and collision detection.
|
||||||
|
|
||||||
|
### NoLeadIn (Type 0)
|
||||||
|
|
||||||
|
Pierce directly on the contour start point. Returns a single `RapidMove(contourStartPoint)`.
|
||||||
|
|
||||||
|
### LineLeadIn (Type 1)
|
||||||
|
|
||||||
|
Straight line approach.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `Length` (double): distance from pierce point to contour start (inches)
|
||||||
|
- `ApproachAngle` (double): approach angle in degrees relative to contour tangent. 90 = perpendicular, 135 = acute angle (common for plasma). Default: 90.
|
||||||
|
|
||||||
|
Pierce point offset: `contourStartPoint + Length` along `contourNormalAngle + Angle.ToRadians(ApproachAngle)`.
|
||||||
|
|
||||||
|
Generates: `RapidMove(piercePoint)` → `LinearMove(contourStartPoint)`.
|
||||||
|
|
||||||
|
> **Note:** Properties are named `ApproachAngle` (not `Angle`) to avoid shadowing the `OpenNest.Math.Angle` static class. This applies to all lead-in/lead-out/tab classes.
|
||||||
|
|
||||||
|
### LineArcLeadIn (Type 2)
|
||||||
|
|
||||||
|
Line followed by tangential arc meeting the contour. Most common for plasma.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `LineLength` (double): straight approach segment length
|
||||||
|
- `ApproachAngle` (double): line angle relative to contour. Default: 135.
|
||||||
|
- `ArcRadius` (double): radius of tangential arc
|
||||||
|
|
||||||
|
Geometry: Pierce → [Line] → Arc start → [Arc] → Contour start. Arc center is at `contourStartPoint + ArcRadius` along normal. Arc rotation direction matches contour winding (CW for CW contours, CCW for CCW).
|
||||||
|
|
||||||
|
Generates: `RapidMove(piercePoint)` → `LinearMove(arcStart)` → `ArcMove(contourStartPoint, arcCenter, rotation)`.
|
||||||
|
|
||||||
|
### ArcLeadIn (Type 3)
|
||||||
|
|
||||||
|
Pure arc approach, no straight line segment.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `Radius` (double): arc radius
|
||||||
|
|
||||||
|
Pierce point is diametrically opposite the contour start on the arc circle. Arc center at `contourStartPoint + Radius` along normal.
|
||||||
|
|
||||||
|
Arc rotation direction matches contour winding.
|
||||||
|
|
||||||
|
Generates: `RapidMove(piercePoint)` → `ArcMove(contourStartPoint, arcCenter, rotation)`.
|
||||||
|
|
||||||
|
### LineLineLeadIn (Type 5)
|
||||||
|
|
||||||
|
Two-segment straight line approach.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `Length1` (double): first segment length
|
||||||
|
- `ApproachAngle1` (double): first segment angle. Default: 90.
|
||||||
|
- `Length2` (double): second segment length
|
||||||
|
- `ApproachAngle2` (double): direction change. Default: 90.
|
||||||
|
|
||||||
|
Generates: `RapidMove(piercePoint)` → `LinearMove(midPoint)` → `LinearMove(contourStartPoint)`.
|
||||||
|
|
||||||
|
### CleanHoleLeadIn
|
||||||
|
|
||||||
|
Specialized for precision circular holes. Same geometry as `LineArcLeadIn` but with hard-coded 135° angle and a `Kerf` property. The overcut (cutting past start to close the hole) is handled at the lead-out, not here.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `LineLength` (double)
|
||||||
|
- `ArcRadius` (double)
|
||||||
|
- `Kerf` (double)
|
||||||
|
|
||||||
|
## LeadOut Hierarchy
|
||||||
|
|
||||||
|
### Abstract Base: `LeadOut`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public abstract class LeadOut
|
||||||
|
{
|
||||||
|
public abstract List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `contourEndPoint`: where the contour cut ends. For closed contours, same as start.
|
||||||
|
- Returns ICode instructions appended after the contour's last cut point.
|
||||||
|
|
||||||
|
### NoLeadOut (Type 0)
|
||||||
|
|
||||||
|
Returns empty list. Cut ends exactly at contour end.
|
||||||
|
|
||||||
|
### LineLeadOut (Type 1)
|
||||||
|
|
||||||
|
Straight line overcut past contour end.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `Length` (double): overcut distance
|
||||||
|
- `ApproachAngle` (double): direction relative to contour tangent. Default: 90.
|
||||||
|
|
||||||
|
Generates: `LinearMove(endPoint)` where endPoint is offset from contourEndPoint.
|
||||||
|
|
||||||
|
### ArcLeadOut (Type 3)
|
||||||
|
|
||||||
|
Arc overcut curving away from the part.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `Radius` (double)
|
||||||
|
|
||||||
|
Arc center at `contourEndPoint + Radius` along normal. End point is a quarter turn away. Arc rotation direction matches contour winding.
|
||||||
|
|
||||||
|
Generates: `ArcMove(endPoint, arcCenter, rotation)`.
|
||||||
|
|
||||||
|
### MicrotabLeadOut (Type 4)
|
||||||
|
|
||||||
|
Stops short of contour end, leaving an uncut bridge. Laser only.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `GapSize` (double): uncut material length. Default: 0.03".
|
||||||
|
|
||||||
|
Does NOT add instructions — returns empty list. The `ContourCuttingStrategy` detects this type and trims the last cutting move by `GapSize` instead.
|
||||||
|
|
||||||
|
## Tab Hierarchy
|
||||||
|
|
||||||
|
Tabs are mid-contour features that temporarily lift the beam to leave bridges holding the part in place.
|
||||||
|
|
||||||
|
### Abstract Base: `Tab`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public abstract class Tab
|
||||||
|
{
|
||||||
|
public double Size { get; set; } = 0.03;
|
||||||
|
public LeadIn TabLeadIn { get; set; }
|
||||||
|
public LeadOut TabLeadOut { get; set; }
|
||||||
|
|
||||||
|
public abstract List<ICode> Generate(
|
||||||
|
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### NormalTab
|
||||||
|
|
||||||
|
Standard tab: cut up to tab start, lift/rapid over gap, resume cutting.
|
||||||
|
|
||||||
|
Additional properties:
|
||||||
|
- `CutoutMinWidth`, `CutoutMinHeight` (double): minimum cutout size to receive this tab
|
||||||
|
- `CutoutMaxWidth`, `CutoutMaxHeight` (double): maximum cutout size to receive this tab
|
||||||
|
- `AppliesToCutout(double width, double height)` method for size filtering
|
||||||
|
|
||||||
|
Generates: TabLeadOut codes → `RapidMove(tabEndPoint)` → TabLeadIn codes.
|
||||||
|
|
||||||
|
### BreakerTab
|
||||||
|
|
||||||
|
Like NormalTab but adds a scoring cut into the part at the tab location to make snapping easier.
|
||||||
|
|
||||||
|
Additional properties:
|
||||||
|
- `BreakerDepth` (double): how far the score cuts into the part
|
||||||
|
- `BreakerLeadInLength` (double)
|
||||||
|
- `BreakerAngle` (double)
|
||||||
|
|
||||||
|
Generates: TabLeadOut codes → `LinearMove(scoreEnd)` → `RapidMove(tabEndPoint)` → TabLeadIn codes.
|
||||||
|
|
||||||
|
### MachineTab
|
||||||
|
|
||||||
|
Tab behavior configured at the CNC controller level. OpenNest just signals the controller.
|
||||||
|
|
||||||
|
Additional properties:
|
||||||
|
- `MachineTabId` (int): passed to post-processor for M-code translation
|
||||||
|
|
||||||
|
Returns a placeholder `RapidMove(tabEndPoint)` — the post-processor plugin replaces this with machine-specific commands.
|
||||||
|
|
||||||
|
## CuttingParameters
|
||||||
|
|
||||||
|
One instance per material/machine combination. Ties everything together.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class CuttingParameters
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
// Material/Machine identification
|
||||||
|
public string MachineName { get; set; }
|
||||||
|
public string MaterialName { get; set; }
|
||||||
|
public string Grade { get; set; }
|
||||||
|
public double Thickness { get; set; }
|
||||||
|
|
||||||
|
// Kerf and spacing
|
||||||
|
public double Kerf { get; set; }
|
||||||
|
public double PartSpacing { get; set; }
|
||||||
|
|
||||||
|
// External contour lead-in/out
|
||||||
|
public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn();
|
||||||
|
public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut();
|
||||||
|
|
||||||
|
// Internal contour lead-in/out
|
||||||
|
public LeadIn InternalLeadIn { get; set; } = new LineLeadIn { Length = 0.125, Angle = 90 };
|
||||||
|
public LeadOut InternalLeadOut { get; set; } = new NoLeadOut();
|
||||||
|
|
||||||
|
// Arc/circle specific (overrides internal for circular features)
|
||||||
|
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
|
||||||
|
public LeadOut ArcCircleLeadOut { get; set; } = new NoLeadOut();
|
||||||
|
|
||||||
|
// Tab configuration
|
||||||
|
public Tab TabConfig { get; set; }
|
||||||
|
public bool TabsEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
// Sequencing and assignment
|
||||||
|
public SequenceParameters Sequencing { get; set; } = new SequenceParameters();
|
||||||
|
public AssignmentParameters Assignment { get; set; } = new AssignmentParameters();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SequenceParameters and AssignmentParameters
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Values match PEP Technology's numbering scheme (value 6 intentionally skipped)
|
||||||
|
public enum SequenceMethod
|
||||||
|
{
|
||||||
|
RightSide = 1, LeastCode = 2, Advanced = 3,
|
||||||
|
BottomSide = 4, EdgeStart = 5, LeftSide = 7, RightSideAlt = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SequenceParameters
|
||||||
|
{
|
||||||
|
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
|
||||||
|
public double SmallCutoutWidth { get; set; } = 1.5;
|
||||||
|
public double SmallCutoutHeight { get; set; } = 1.5;
|
||||||
|
public double MediumCutoutWidth { get; set; } = 8.0;
|
||||||
|
public double MediumCutoutHeight { get; set; } = 8.0;
|
||||||
|
public double DistanceMediumSmall { get; set; }
|
||||||
|
public bool AlternateRowsColumns { get; set; } = true;
|
||||||
|
public bool AlternateCutoutsWithinRowColumn { get; set; } = true;
|
||||||
|
public double MinDistanceBetweenRowsColumns { get; set; } = 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AssignmentParameters
|
||||||
|
{
|
||||||
|
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
|
||||||
|
public string Preference { get; set; } = "ILAT";
|
||||||
|
public double MinGeometryLength { get; set; } = 0.01;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ContourCuttingStrategy
|
||||||
|
|
||||||
|
The orchestrator. Uses `ShapeProfile` to decompose a part into perimeter + cutouts, then sequences and applies cutting parameters using nearest-neighbor chaining from an exit point.
|
||||||
|
|
||||||
|
### Exit Point from Plate Quadrant
|
||||||
|
|
||||||
|
The exit point is the **opposite corner** of the plate from the quadrant origin. This is where the head ends up after traversing the plate, and is the starting point for backwards nearest-neighbor sequencing.
|
||||||
|
|
||||||
|
| Quadrant | Origin | Exit Point |
|
||||||
|
|----------|--------|------------|
|
||||||
|
| 1 | TopRight | BottomLeft (0, 0) |
|
||||||
|
| 2 | TopLeft | BottomRight (width, 0) |
|
||||||
|
| 3 | BottomLeft | TopRight (width, length) |
|
||||||
|
| 4 | BottomRight | TopLeft (0, length) |
|
||||||
|
|
||||||
|
The exit point is derived from `Plate.Quadrant` and `Plate.Size` — not passed in manually.
|
||||||
|
|
||||||
|
### Approach
|
||||||
|
|
||||||
|
Instead of requiring `Program.GetStartPoint()` / `GetNormalAtStart()` (which don't exist), the strategy:
|
||||||
|
|
||||||
|
1. Computes the **exit point** from the plate's quadrant and size
|
||||||
|
2. Converts the program to geometry via `Program.ToGeometry()`
|
||||||
|
3. Builds a `ShapeProfile` from the geometry — gives `Perimeter` (Shape) and `Cutouts` (List<Shape>)
|
||||||
|
4. Uses `Shape.ClosestPointTo(point, out Entity entity)` to find lead-in points and the entity for normal computation
|
||||||
|
5. Chains cutouts by nearest-neighbor distance from the perimeter closest point
|
||||||
|
6. Reverses the chain → cut order is cutouts first (nearest-last), perimeter last
|
||||||
|
|
||||||
|
### Contour Re-Indexing
|
||||||
|
|
||||||
|
After `ClosestPointTo` finds the lead-in point on a shape, the shape's entity list must be reordered so that cutting starts at that point. This means:
|
||||||
|
|
||||||
|
1. Find which entity in `Shape.Entities` contains the closest point
|
||||||
|
2. Split that entity at the closest point into two segments
|
||||||
|
3. Reorder: second half of split entity → remaining entities in order → first half of split entity
|
||||||
|
4. The contour now starts and ends at the lead-in point (for closed contours)
|
||||||
|
|
||||||
|
This produces the `List<ICode>` for the contour body that goes between the lead-in and lead-out codes.
|
||||||
|
|
||||||
|
### ContourType Detection
|
||||||
|
|
||||||
|
- `ShapeProfile.Perimeter` → `ContourType.External`
|
||||||
|
- Each cutout in `ShapeProfile.Cutouts`:
|
||||||
|
- If single entity and entity is `Circle` → `ContourType.ArcCircle`
|
||||||
|
- Otherwise → `ContourType.Internal`
|
||||||
|
|
||||||
|
### Normal Angle Computation
|
||||||
|
|
||||||
|
Derived from the `out Entity` returned by `ClosestPointTo`:
|
||||||
|
|
||||||
|
- **Line**: normal is perpendicular to line direction. Use the line's tangent angle, then add π/2 for the normal pointing away from the part interior.
|
||||||
|
- **Arc/Circle**: normal is radial direction from arc center to the closest point: `closestPoint.AngleFrom(arc.Center)`.
|
||||||
|
|
||||||
|
Normal direction convention: always points **away from the part material** (outward from perimeter, inward toward scrap for cutouts). The lead-in approaches from this direction.
|
||||||
|
|
||||||
|
### Arc Rotation Direction
|
||||||
|
|
||||||
|
Lead-in/lead-out arcs must match the **contour winding direction**, not be hardcoded CW. Determine winding from the shape's entity traversal order. Pass the appropriate `RotationType` to `ArcMove`.
|
||||||
|
|
||||||
|
### Method Signature
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ContourCuttingStrategy
|
||||||
|
{
|
||||||
|
public CuttingParameters Parameters { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply cutting strategy to a part's program.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="partProgram">Original part program (unmodified).</param>
|
||||||
|
/// <param name="plate">Plate for quadrant/size to compute exit point.</param>
|
||||||
|
/// <returns>New Program with lead-ins, lead-outs, and tabs applied. Cutouts first, perimeter last.</returns>
|
||||||
|
public Program Apply(Program partProgram, Plate plate)
|
||||||
|
{
|
||||||
|
// 1. Compute exit point from plate quadrant + size
|
||||||
|
// 2. Convert to geometry, build ShapeProfile
|
||||||
|
// 3. Find closest point on perimeter from exitPoint
|
||||||
|
// 4. Chain cutouts by nearest-neighbor from perimeter point
|
||||||
|
// 5. Reverse chain → cut order
|
||||||
|
// 6. For each contour:
|
||||||
|
// a. Re-index shape entities to start at closest point
|
||||||
|
// b. Detect ContourType
|
||||||
|
// c. Compute normal angle from entity
|
||||||
|
// d. Select lead-in/out from CuttingParameters by ContourType
|
||||||
|
// e. Generate lead-in codes + contour body + lead-out codes
|
||||||
|
// 7. Handle MicrotabLeadOut by trimming last segment
|
||||||
|
// 8. Assemble and return new Program
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ContourType Enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum ContourType
|
||||||
|
{
|
||||||
|
External,
|
||||||
|
Internal,
|
||||||
|
ArcCircle
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Point
|
||||||
|
|
||||||
|
`ContourCuttingStrategy.Apply()` runs at nest-time (when parts are placed or cutting parameters are assigned), not at post-processing time. The output `Program` — with lead-ins, lead-outs, start points, and contour ordering — is stored on the `Part` and saved through the normal `NestWriter` path. The post-processor receives this already-complete program and only translates it to machine-specific G-code.
|
||||||
|
|
||||||
|
## Out of Scope (Deferred)
|
||||||
|
|
||||||
|
- **Serialization** of CuttingParameters (JSON/XML discriminators)
|
||||||
|
- **UI integration** (parameter editor forms in WinForms app)
|
||||||
|
- **Part.CutProgram property** (storing the strategy-applied program on `Part`, separate from `Drawing.Program`)
|
||||||
|
- **Tab insertion logic** (`InsertTabs` / `TrimLastSegment` — stubbed with `NotImplementedException`)
|
||||||
134
docs/superpowers/specs/2026-03-12-nest-file-format-v2-design.md
Normal file
134
docs/superpowers/specs/2026-03-12-nest-file-format-v2-design.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Nest File Format v2 Design
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The current nest file format stores metadata across three separate XML files (`info`, `drawing-info`, `plate-info`) plus per-plate G-code files for part placements inside a ZIP archive. This results in ~400 lines of hand-written XML read/write code, fragile dictionary-linking to reconnect drawings/plates by ID after parsing, and the overhead of running the full G-code parser just to extract part positions.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
The nest file remains a ZIP archive. Contents:
|
||||||
|
|
||||||
|
```
|
||||||
|
nest.json
|
||||||
|
programs/
|
||||||
|
program-1
|
||||||
|
program-2
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`nest.json`** — single JSON file containing all metadata and part placements.
|
||||||
|
- **`programs/program-N`** — G-code text for each drawing's CNC program (1-indexed, no zero-padding). Previously stored at the archive root as `program-NNN` (zero-padded). Parsed by `ProgramReader`, written by existing G-code serialization logic. Format unchanged.
|
||||||
|
|
||||||
|
Plate G-code files (`plate-NNN`) are removed. Part placements are stored inline in `nest.json`.
|
||||||
|
|
||||||
|
### JSON Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"name": "string",
|
||||||
|
"units": "Inches | Millimeters",
|
||||||
|
"customer": "string",
|
||||||
|
"dateCreated": "2026-03-12T10:30:00",
|
||||||
|
"dateLastModified": "2026-03-12T14:00:00",
|
||||||
|
"notes": "string (plain JSON, no URI-escaping)",
|
||||||
|
"plateDefaults": {
|
||||||
|
"size": { "width": 0.0, "height": 0.0 },
|
||||||
|
"thickness": 0.0,
|
||||||
|
"quadrant": 1,
|
||||||
|
"partSpacing": 0.0,
|
||||||
|
"material": { "name": "string", "grade": "string", "density": 0.0 },
|
||||||
|
"edgeSpacing": { "left": 0.0, "top": 0.0, "right": 0.0, "bottom": 0.0 }
|
||||||
|
},
|
||||||
|
"drawings": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "string",
|
||||||
|
"customer": "string",
|
||||||
|
"color": { "a": 255, "r": 0, "g": 0, "b": 0 },
|
||||||
|
"quantity": { "required": 0 },
|
||||||
|
"priority": 0,
|
||||||
|
"constraints": {
|
||||||
|
"stepAngle": 0.0,
|
||||||
|
"startAngle": 0.0,
|
||||||
|
"endAngle": 0.0,
|
||||||
|
"allow180Equivalent": false
|
||||||
|
},
|
||||||
|
"material": { "name": "string", "grade": "string", "density": 0.0 },
|
||||||
|
"source": {
|
||||||
|
"path": "string",
|
||||||
|
"offset": { "x": 0.0, "y": 0.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plates": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"size": { "width": 0.0, "height": 0.0 },
|
||||||
|
"thickness": 0.0,
|
||||||
|
"quadrant": 1,
|
||||||
|
"quantity": 1,
|
||||||
|
"partSpacing": 0.0,
|
||||||
|
"material": { "name": "string", "grade": "string", "density": 0.0 },
|
||||||
|
"edgeSpacing": { "left": 0.0, "top": 0.0, "right": 0.0, "bottom": 0.0 },
|
||||||
|
"parts": [
|
||||||
|
{ "drawingId": 1, "x": 0.0, "y": 0.0, "rotation": 0.0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key details:
|
||||||
|
- **Version**: `"version": 2` at the top level for future format migration.
|
||||||
|
- Drawing `id` values are 1-indexed, matching `programs/program-N` filenames.
|
||||||
|
- Part `rotation` is stored in **radians** (matches internal domain model, no conversion needed).
|
||||||
|
- Part `drawingId` references the drawing's `id` in the `drawings` array.
|
||||||
|
- **Dates**: local time, serialized via `DateTime.ToString("o")` (ISO 8601 round-trip format with timezone offset).
|
||||||
|
- **Notes**: stored as plain JSON strings. The v1 URI-escaping (`Uri.EscapeDataString`) is not needed since JSON handles special characters natively.
|
||||||
|
- `quantity.required` is the only quantity persisted; `nested` is computed at load time from part placements.
|
||||||
|
- **Units**: enum values match the domain model: `Inches` or `Millimeters`.
|
||||||
|
- **Size**: uses `width`/`height` matching the `OpenNest.Geometry.Size` struct.
|
||||||
|
- **Drawing.Priority** and **Drawing.Constraints** (stepAngle, startAngle, endAngle, allow180Equivalent) are now persisted (v1 omitted these).
|
||||||
|
- **Empty collections**: `drawings` and `plates` arrays are always present (may be empty `[]`). The `programs/` folder is empty when there are no drawings.
|
||||||
|
|
||||||
|
### Serialization Approach
|
||||||
|
|
||||||
|
Use `System.Text.Json` with small DTO (Data Transfer Object) classes for serialization. The DTOs map between the domain model and the JSON structure, keeping serialization concerns out of the domain classes.
|
||||||
|
|
||||||
|
### What Changes
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `NestWriter.cs` | Replace all XML writing and plate G-code writing with JSON serialization. Programs written to `programs/` folder. |
|
||||||
|
| `NestReader.cs` | Replace all XML parsing, plate G-code parsing, and dictionary-linking with JSON deserialization. Programs read from `programs/` folder. |
|
||||||
|
|
||||||
|
### What Stays the Same
|
||||||
|
|
||||||
|
| File | Reason |
|
||||||
|
|------|--------|
|
||||||
|
| `ProgramReader.cs` | G-code parsing for CNC programs is unchanged. |
|
||||||
|
| `NestWriter` G-code writing (`WriteDrawing`, `GetCodeString`) | G-code serialization for programs is unchanged. |
|
||||||
|
| `DxfImporter.cs`, `DxfExporter.cs`, `Extensions.cs` | Unrelated to nest file format. |
|
||||||
|
| Domain model classes | No changes needed. |
|
||||||
|
|
||||||
|
### Public API
|
||||||
|
|
||||||
|
The public API is unchanged:
|
||||||
|
- `NestReader(string file)` and `NestReader(Stream stream)` constructors preserved.
|
||||||
|
- `NestReader.Read()` returns `Nest`.
|
||||||
|
- `NestWriter(Nest nest)` constructor preserved.
|
||||||
|
- `NestWriter.Write(string file)` returns `bool`.
|
||||||
|
|
||||||
|
### Callers (no changes needed)
|
||||||
|
|
||||||
|
- `MainForm.cs:329` — `new NestReader(path)`
|
||||||
|
- `MainForm.cs:363` — `new NestReader(dlg.FileName)`
|
||||||
|
- `EditNestForm.cs:212` — `new NestWriter(Nest)`
|
||||||
|
- `EditNestForm.cs:223` — `new NestWriter(nst)`
|
||||||
|
- `Document.cs:27` — `new NestWriter(Nest)`
|
||||||
|
- `OpenNest.Console/Program.cs:94` — `new NestReader(nestFile)`
|
||||||
|
- `OpenNest.Console/Program.cs:190` — `new NestWriter(nest)`
|
||||||
|
- `OpenNest.Mcp/InputTools.cs:30` — `new NestReader(path)`
|
||||||
Reference in New Issue
Block a user