Compare commits
40 Commits
a548d5329a
...
0246073b31
| Author | SHA1 | Date | |
|---|---|---|---|
| 0246073b31 | |||
| 4801895321 | |||
| 833abfe72e | |||
| 379000bbd8 | |||
| 5936272ce4 | |||
| da8e7e6fd3 | |||
| 53d24ddaf1 | |||
| 8efdc8720c | |||
| ca8a0942ab | |||
| 8c3659a439 | |||
| 95a0815484 | |||
| e9caa9b8eb | |||
| 95a0db1983 | |||
| a323dcc230 | |||
| 24cd18da88 | |||
| 5d26efb552 | |||
| 60c4545a17 | |||
| 4db51b8cdf | |||
| 1c561d880e | |||
| 17fc9c6cab | |||
| 4287c5fa46 | |||
| a735884ee9 | |||
| 22554b0fa3 | |||
| 48b4849a88 | |||
| f79df4d426 | |||
| ebb18d9b49 | |||
| 31a9e6dbad | |||
| a576f9fafa | |||
| 9453bb51ce | |||
| ad58332a5d | |||
| d4f60d5e8e | |||
| 3ea05257eb | |||
| 7e49ed620b | |||
| 57bd0447e9 | |||
| 07d6f08e8b | |||
| 2f19f47a85 | |||
| d58a446eac | |||
| 5fc7d1989a | |||
| 3f6bc2b2a1 | |||
| 7681a1bad0 |
@@ -30,10 +30,11 @@ Domain model, geometry, and CNC primitives organized into namespaces:
|
||||
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
|
||||
- **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration.
|
||||
- **Collections** (`Collections/`, `namespace OpenNest.Collections`): `ObservableList<T>`, `DrawingCollection`.
|
||||
- **CutOffs** (`namespace OpenNest`): `CutOff` (axis-aligned cut line with position, axis, optional start/end limits), `CutOffAxis` enum (`Horizontal`, `Vertical`), `CutOffSettings` (clearance, overtravel, min segment length, direction), `CutDirection` enum (`TowardOrigin`, `AwayFromOrigin`). Cut-offs generate CNC `Program` objects with trimmed line segments that avoid parts.
|
||||
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
|
||||
|
||||
### OpenNest.Engine (class library, depends on Core)
|
||||
Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the abstract base class; `DefaultNestEngine` (formerly `NestEngine`) provides the multi-phase fill strategy. `NestEngineRegistry` manages available engines (built-in + plugins from `Engines/` directory) and the globally active engine. `AutoNester` handles mixed-part NFP-based nesting with simulated annealing (not yet integrated into the registry).
|
||||
Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the abstract base class; `DefaultNestEngine` (formerly `NestEngine`) provides the multi-phase fill strategy. `NestEngineRegistry` manages available engines (built-in + plugins from `Engines/` directory) and the globally active engine.
|
||||
|
||||
- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`.
|
||||
- **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`.
|
||||
@@ -43,7 +44,7 @@ Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the
|
||||
- **BestFit/** (`namespace OpenNest.Engine.BestFit`): NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups.
|
||||
- **RectanglePacking/** (`namespace OpenNest.RectanglePacking`): `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions.
|
||||
- **CirclePacking/** (`namespace OpenNest.CirclePacking`): Alternative packing for circular parts.
|
||||
- **Nfp/** (`namespace OpenNest.Engine.Nfp`): NFP-based nesting (not yet integrated) — `AutoNester` (mixed-part nesting with simulated annealing), `BottomLeftFill` (BLF placement), `NfpCache` (computed NFP caching), `SimulatedAnnealing` (optimizer), `INestOptimizer`/`NestResult`.
|
||||
- **Nfp/** (`namespace OpenNest.Engine.Nfp`): Internal NFP-based single-part placement utilities — `AutoNester` (NFP placement with simulated annealing), `BottomLeftFill` (BLF placement), `NfpCache` (computed NFP caching), `SimulatedAnnealing` (optimizer), `INestOptimizer`/`OptimizationResult`. Not exposed as a nest engine; used internally for individual part placement.
|
||||
- **ML/** (`namespace OpenNest.Engine.ML`): `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data).
|
||||
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
|
||||
- `NestProgress`: Progress reporting model with `NestPhase` enum for UI feedback.
|
||||
@@ -79,13 +80,13 @@ The UI application with MDI interface.
|
||||
|
||||
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
||||
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan, supports temporary preview parts), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
|
||||
- **Actions/**: User interaction modes — `ActionSelect`, `ActionClone`, `ActionFillArea`, `ActionSelectArea`, `ActionZoomWindow`, `ActionSetSequence`.
|
||||
- **Actions/**: User interaction modes — `ActionSelect`, `ActionClone`, `ActionFillArea`, `ActionSelectArea`, `ActionZoomWindow`, `ActionSetSequence`, `ActionCutOff`.
|
||||
- **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
|
||||
|
||||
## File Format
|
||||
|
||||
Nest files (`.nest`, ZIP-based) use v2 JSON format:
|
||||
- `nest.json` — single JSON file containing all nest metadata: nest info (name, units, customer, dates, notes), plate defaults (size, thickness, quadrant, spacing, material, edge spacing), drawings array (id, name, color, quantity, priority, rotation constraints, material, source), and plates array (id, size, material, edge spacing, parts with drawingId/x/y/rotation)
|
||||
- `nest.json` — single JSON file containing all nest metadata: nest info (name, units, customer, dates, notes), plate defaults (size, thickness, quadrant, spacing, material, edge spacing), drawings array (id, name, color, quantity, priority, rotation constraints, material, source), and plates array (id, size, material, edge spacing, parts with drawingId/x/y/rotation, cutoffs with x/y/axis/startLimit/endLimit)
|
||||
- `programs/program-N` — G-code text for each drawing's cut program (N = drawing id)
|
||||
- `bestfits/bestfit-N` — JSON array of best-fit pair evaluation results per drawing, keyed by plate size/spacing (optional, only present if best-fit data was computed)
|
||||
|
||||
@@ -113,3 +114,4 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe
|
||||
- Nesting uses async progress/cancellation: `IProgress<NestProgress>` and `CancellationToken` flow through the engine to the UI's `NestProgressForm`.
|
||||
- `Compactor` performs post-fill gravity compaction — after filling, parts are pushed toward a plate edge using directional distance calculations to close gaps between irregular shapes.
|
||||
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
|
||||
- **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load).
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
public sealed class ProgramVariable
|
||||
{
|
||||
public int Number { get; }
|
||||
public string Name { get; }
|
||||
public string Expression { get; set; }
|
||||
|
||||
public ProgramVariable(int number, string name, string expression = null)
|
||||
{
|
||||
Number = number;
|
||||
Name = name;
|
||||
Expression = expression;
|
||||
}
|
||||
|
||||
public string Reference => $"#{Number}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
public sealed class ProgramVariableManager
|
||||
{
|
||||
private readonly Dictionary<int, ProgramVariable> _variables = new();
|
||||
|
||||
public ProgramVariable GetOrCreate(string name, int number, string expression = null)
|
||||
{
|
||||
if (_variables.TryGetValue(number, out var existing))
|
||||
return existing;
|
||||
|
||||
var variable = new ProgramVariable(number, name, expression);
|
||||
_variables[number] = variable;
|
||||
return variable;
|
||||
}
|
||||
|
||||
public List<string> EmitDeclarations()
|
||||
{
|
||||
return _variables.Values
|
||||
.Where(v => v.Expression != null)
|
||||
.OrderBy(v => v.Number)
|
||||
.Select(v => $"{v.Reference}={v.Expression} ({FormatComment(v.Name)})")
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string FormatComment(string name)
|
||||
{
|
||||
// "LeadInFeedrate" -> "LEAD IN FEEDRATE"
|
||||
var sb = new StringBuilder();
|
||||
foreach (var c in name)
|
||||
{
|
||||
if (char.IsUpper(c) && sb.Length > 0)
|
||||
sb.Append(' ');
|
||||
sb.Append(char.ToUpper(c));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
public enum CutOffAxis
|
||||
{
|
||||
Horizontal,
|
||||
Vertical
|
||||
}
|
||||
|
||||
public class CutOff
|
||||
{
|
||||
public Vector Position { get; set; }
|
||||
public CutOffAxis Axis { get; set; }
|
||||
public double? StartLimit { get; set; }
|
||||
public double? EndLimit { get; set; }
|
||||
public Drawing Drawing { get; private set; }
|
||||
|
||||
public CutOff(Vector position, CutOffAxis axis)
|
||||
{
|
||||
Position = position;
|
||||
Axis = axis;
|
||||
Drawing = new Drawing(GetName()) { IsCutOff = true };
|
||||
}
|
||||
|
||||
public void Regenerate(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache = null)
|
||||
{
|
||||
var segments = ComputeSegments(plate, settings, cache);
|
||||
var program = BuildProgram(segments, settings);
|
||||
Drawing.Program = program;
|
||||
}
|
||||
|
||||
private string GetName()
|
||||
{
|
||||
var axisChar = Axis == CutOffAxis.Vertical ? "V" : "H";
|
||||
var coord = Axis == CutOffAxis.Vertical ? Position.X : Position.Y;
|
||||
return $"CutOff-{axisChar}-{coord:F2}";
|
||||
}
|
||||
|
||||
private List<(double Start, double End)> ComputeSegments(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache)
|
||||
{
|
||||
var bounds = plate.BoundingBox(includeParts: false);
|
||||
|
||||
double lineStart, lineEnd, cutPosition;
|
||||
|
||||
if (Axis == CutOffAxis.Vertical)
|
||||
{
|
||||
cutPosition = Position.X;
|
||||
lineStart = StartLimit ?? bounds.Y;
|
||||
lineEnd = EndLimit ?? (bounds.Y + bounds.Length + settings.Overtravel);
|
||||
}
|
||||
else
|
||||
{
|
||||
cutPosition = Position.Y;
|
||||
lineStart = StartLimit ?? bounds.X;
|
||||
lineEnd = EndLimit ?? (bounds.X + bounds.Width + settings.Overtravel);
|
||||
}
|
||||
|
||||
var exclusions = new List<(double Start, double End)>();
|
||||
|
||||
foreach (var part in plate.Parts)
|
||||
{
|
||||
if (part.BaseDrawing.IsCutOff)
|
||||
continue;
|
||||
|
||||
Entity perimeter = null;
|
||||
cache?.TryGetValue(part, out perimeter);
|
||||
var partExclusions = GetPartExclusions(part, perimeter, cutPosition, lineStart, lineEnd, settings.PartClearance);
|
||||
exclusions.AddRange(partExclusions);
|
||||
}
|
||||
|
||||
exclusions.Sort((a, b) => a.Start.CompareTo(b.Start));
|
||||
var merged = new List<(double Start, double End)>();
|
||||
foreach (var ex in exclusions)
|
||||
{
|
||||
if (merged.Count > 0 && ex.Start <= merged[^1].End)
|
||||
merged[^1] = (merged[^1].Start, System.Math.Max(merged[^1].End, ex.End));
|
||||
else
|
||||
merged.Add(ex);
|
||||
}
|
||||
|
||||
var segments = new List<(double Start, double End)>();
|
||||
var current = lineStart;
|
||||
|
||||
foreach (var ex in merged)
|
||||
{
|
||||
var clampedStart = System.Math.Max(ex.Start, lineStart);
|
||||
var clampedEnd = System.Math.Min(ex.End, lineEnd);
|
||||
|
||||
if (clampedStart > current)
|
||||
segments.Add((current, clampedStart));
|
||||
|
||||
current = System.Math.Max(current, clampedEnd);
|
||||
}
|
||||
|
||||
if (current < lineEnd)
|
||||
segments.Add((current, lineEnd));
|
||||
|
||||
segments = segments.Where(s => (s.End - s.Start) >= settings.MinSegmentLength).ToList();
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static readonly List<(double Start, double End)> EmptyExclusions = new();
|
||||
|
||||
private List<(double Start, double End)> GetPartExclusions(
|
||||
Part part, Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
||||
{
|
||||
var bb = part.BoundingBox;
|
||||
var (partMin, partMax) = AxisBounds(bb, clearance);
|
||||
var (partStart, partEnd) = CrossAxisBounds(bb, clearance);
|
||||
|
||||
if (cutPosition < partMin || cutPosition > partMax)
|
||||
return EmptyExclusions;
|
||||
|
||||
if (perimeter != null)
|
||||
{
|
||||
var perimeterExclusions = IntersectPerimeter(perimeter, cutPosition, lineStart, lineEnd, clearance);
|
||||
if (perimeterExclusions != null)
|
||||
return perimeterExclusions;
|
||||
}
|
||||
|
||||
return new List<(double Start, double End)> { (partStart, partEnd) };
|
||||
}
|
||||
|
||||
private List<(double Start, double End)> IntersectPerimeter(
|
||||
Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
||||
{
|
||||
var target = OffsetOutward(perimeter, clearance) ?? perimeter;
|
||||
var usedOffset = target != perimeter;
|
||||
var cutLine = new Line(MakePoint(cutPosition, lineStart), MakePoint(cutPosition, lineEnd));
|
||||
|
||||
if (!target.Intersects(cutLine, out var pts) || pts.Count < 2)
|
||||
return null;
|
||||
|
||||
var coords = pts
|
||||
.Select(pt => Axis == CutOffAxis.Vertical ? pt.Y : pt.X)
|
||||
.OrderBy(c => c)
|
||||
.ToList();
|
||||
|
||||
if (coords.Count % 2 != 0)
|
||||
return null;
|
||||
|
||||
var padding = usedOffset ? 0 : clearance;
|
||||
var result = new List<(double Start, double End)>();
|
||||
for (var i = 0; i < coords.Count; i += 2)
|
||||
result.Add((coords[i] - padding, coords[i + 1] + padding));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Entity OffsetOutward(Entity perimeter, double clearance)
|
||||
{
|
||||
if (clearance <= 0)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var offset = perimeter.OffsetEntity(clearance, OffsetSide.Left);
|
||||
offset?.UpdateBounds();
|
||||
return offset;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Vector MakePoint(double cutCoord, double lineCoord) =>
|
||||
Axis == CutOffAxis.Vertical
|
||||
? new Vector(cutCoord, lineCoord)
|
||||
: new Vector(lineCoord, cutCoord);
|
||||
|
||||
private (double Min, double Max) AxisBounds(Box bb, double clearance) =>
|
||||
Axis == CutOffAxis.Vertical
|
||||
? (bb.X - clearance, bb.X + bb.Width + clearance)
|
||||
: (bb.Y - clearance, bb.Y + bb.Length + clearance);
|
||||
|
||||
private (double Start, double End) CrossAxisBounds(Box bb, double clearance) =>
|
||||
Axis == CutOffAxis.Vertical
|
||||
? (bb.Y - clearance, bb.Y + bb.Length + clearance)
|
||||
: (bb.X - clearance, bb.X + bb.Width + clearance);
|
||||
|
||||
private Program BuildProgram(List<(double Start, double End)> segments, CutOffSettings settings)
|
||||
{
|
||||
var program = new Program();
|
||||
|
||||
if (segments.Count == 0)
|
||||
return program;
|
||||
|
||||
var toward = settings.CutDirection == CutDirection.TowardOrigin;
|
||||
segments = toward
|
||||
? segments.OrderByDescending(s => s.Start).ToList()
|
||||
: segments.OrderBy(s => s.Start).ToList();
|
||||
|
||||
var cutPos = Axis == CutOffAxis.Vertical ? Position.X : Position.Y;
|
||||
|
||||
foreach (var seg in segments)
|
||||
{
|
||||
var (from, to) = toward ? (seg.End, seg.Start) : (seg.Start, seg.End);
|
||||
program.Codes.Add(new RapidMove(MakePoint(cutPos, from)));
|
||||
program.Codes.Add(new LinearMove(MakePoint(cutPos, to)));
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace OpenNest
|
||||
{
|
||||
public enum CutDirection
|
||||
{
|
||||
TowardOrigin,
|
||||
AwayFromOrigin
|
||||
}
|
||||
|
||||
public class CutOffSettings
|
||||
{
|
||||
public double PartClearance { get; set; } = 0.02;
|
||||
public double Overtravel { get; set; }
|
||||
public double MinSegmentLength { get; set; } = 0.05;
|
||||
public CutDirection CutDirection { get; set; } = CutDirection.AwayFromOrigin;
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,8 @@ namespace OpenNest
|
||||
|
||||
public Color Color { get; set; }
|
||||
|
||||
public bool IsCutOff { get; set; }
|
||||
|
||||
public NestConstraints Constraints { get; set; }
|
||||
|
||||
public SourceInfo Source { get; set; }
|
||||
|
||||
@@ -247,7 +247,7 @@ namespace OpenNest.Geometry
|
||||
|
||||
public static class EntityExtensions
|
||||
{
|
||||
public static BoundingRectangleResult FindBestRotation(this List<Entity> entities, double startAngle = 0, double endAngle = Angle.TwoPI)
|
||||
public static List<Vector> CollectPoints(this IEnumerable<Entity> entities)
|
||||
{
|
||||
var points = new List<Vector>();
|
||||
|
||||
@@ -286,17 +286,35 @@ namespace OpenNest.Geometry
|
||||
|
||||
case EntityType.Shape:
|
||||
var shape = (Shape)entity;
|
||||
var subResult = shape.Entities.FindBestRotation(startAngle, endAngle);
|
||||
return subResult;
|
||||
points.AddRange(shape.Entities.CollectPoints());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
public static BoundingRectangleResult FindBestRotation(this List<Entity> entities, double startAngle = 0, double endAngle = Angle.TwoPI)
|
||||
{
|
||||
// Check for Shape entity first (recursive case returns early)
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (entity.Type == EntityType.Shape)
|
||||
{
|
||||
var shape = (Shape)entity;
|
||||
var subResult = shape.Entities.FindBestRotation(startAngle, endAngle);
|
||||
return subResult;
|
||||
}
|
||||
}
|
||||
|
||||
var points = entities.CollectPoints();
|
||||
|
||||
if (points.Count == 0)
|
||||
return new BoundingRectangleResult(startAngle, 0, 0);
|
||||
|
||||
var hull = ConvexHull.Compute(points);
|
||||
|
||||
bool constrained = !startAngle.IsEqualTo(0) || !endAngle.IsEqualTo(Angle.TwoPI);
|
||||
var constrained = !startAngle.IsEqualTo(0) || !endAngle.IsEqualTo(Angle.TwoPI);
|
||||
|
||||
return constrained
|
||||
? RotatingCalipers.MinimumBoundingRectangle(hull, startAngle, endAngle)
|
||||
|
||||
@@ -249,9 +249,8 @@ namespace OpenNest.Geometry
|
||||
|
||||
foreach (var geo in shape.Entities)
|
||||
{
|
||||
List<Vector> pts3;
|
||||
geo.Intersects(line, out pts3);
|
||||
pts.AddRange(pts3);
|
||||
if (geo.Intersects(line, out var pts3))
|
||||
pts.AddRange(pts3);
|
||||
}
|
||||
|
||||
return pts.Count > 0;
|
||||
|
||||
@@ -317,12 +317,68 @@ namespace OpenNest.Geometry
|
||||
|
||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
if (Vertices.Count < 3)
|
||||
return null;
|
||||
|
||||
var isClosed = IsClosed();
|
||||
var count = isClosed ? Vertices.Count - 1 : Vertices.Count;
|
||||
if (count < 3)
|
||||
return null;
|
||||
|
||||
var ccw = CalculateArea() > 0;
|
||||
var outward = ccw ? OffsetSide.Left : OffsetSide.Right;
|
||||
var sign = side == outward ? 1.0 : -1.0;
|
||||
var d = distance * sign;
|
||||
|
||||
var normals = new Vector[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var next = (i + 1) % count;
|
||||
var dx = Vertices[next].X - Vertices[i].X;
|
||||
var dy = Vertices[next].Y - Vertices[i].Y;
|
||||
var len = System.Math.Sqrt(dx * dx + dy * dy);
|
||||
if (len < Tolerance.Epsilon)
|
||||
return null;
|
||||
normals[i] = new Vector(-dy / len * d, dx / len * d);
|
||||
}
|
||||
|
||||
var result = new Polygon();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var prev = (i - 1 + count) % count;
|
||||
|
||||
var a1 = new Vector(Vertices[prev].X + normals[prev].X, Vertices[prev].Y + normals[prev].Y);
|
||||
var a2 = new Vector(Vertices[i].X + normals[prev].X, Vertices[i].Y + normals[prev].Y);
|
||||
var b1 = new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y);
|
||||
var b2 = new Vector(Vertices[(i + 1) % count].X + normals[i].X, Vertices[(i + 1) % count].Y + normals[i].Y);
|
||||
|
||||
var edgeA = new Line(a1, a2);
|
||||
var edgeB = new Line(b1, b2);
|
||||
|
||||
if (edgeA.Intersects(edgeB, out var pt) && pt.IsValid())
|
||||
result.Vertices.Add(pt);
|
||||
else
|
||||
result.Vertices.Add(new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y));
|
||||
}
|
||||
|
||||
result.Close();
|
||||
result.RemoveSelfIntersections();
|
||||
result.UpdateBounds();
|
||||
return result;
|
||||
}
|
||||
|
||||
public override Entity OffsetEntity(double distance, Vector pt)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var left = OffsetEntity(distance, OffsetSide.Left);
|
||||
var right = OffsetEntity(distance, OffsetSide.Right);
|
||||
|
||||
if (left == null) return right;
|
||||
if (right == null) return left;
|
||||
|
||||
var distLeft = left.ClosestPointTo(pt).DistanceTo(pt);
|
||||
var distRight = right.ClosestPointTo(pt).DistanceTo(pt);
|
||||
|
||||
return distLeft > distRight ? left : right;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -21,9 +21,12 @@ namespace OpenNest.Geometry
|
||||
Perimeter = shapes[0];
|
||||
Cutouts = new List<Shape>();
|
||||
|
||||
for (int i = 1; i < shapes.Count; i++)
|
||||
for (var i = 1; i < shapes.Count; i++)
|
||||
{
|
||||
if (shapes[i].Left < Perimeter.Left)
|
||||
var bb = shapes[i].BoundingBox;
|
||||
var perimBB = Perimeter.BoundingBox;
|
||||
|
||||
if (bb.Width * bb.Length > perimBB.Width * perimBB.Length)
|
||||
{
|
||||
Cutouts.Add(Perimeter);
|
||||
Perimeter = shapes[i];
|
||||
|
||||
+144
-14
@@ -47,17 +47,20 @@ namespace OpenNest
|
||||
Parts = new ObservableList<Part>();
|
||||
Parts.ItemAdded += Parts_PartAdded;
|
||||
Parts.ItemRemoved += Parts_PartRemoved;
|
||||
CutOffs = new ObservableList<CutOff>();
|
||||
Quadrant = 1;
|
||||
}
|
||||
|
||||
private void Parts_PartAdded(object sender, ItemAddedEventArgs<Part> e)
|
||||
{
|
||||
e.Item.BaseDrawing.Quantity.Nested += Quantity;
|
||||
if (!e.Item.BaseDrawing.IsCutOff)
|
||||
e.Item.BaseDrawing.Quantity.Nested += Quantity;
|
||||
}
|
||||
|
||||
private void Parts_PartRemoved(object sender, ItemRemovedEventArgs<Part> e)
|
||||
{
|
||||
e.Item.BaseDrawing.Quantity.Nested -= Quantity;
|
||||
if (!e.Item.BaseDrawing.IsCutOff)
|
||||
e.Item.BaseDrawing.Quantity.Nested -= Quantity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -90,6 +93,92 @@ namespace OpenNest
|
||||
/// </summary>
|
||||
public ObservableList<Part> Parts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cut-off lines defined on this plate.
|
||||
/// </summary>
|
||||
public ObservableList<CutOff> CutOffs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Regenerates all cut-off drawings and materializes them as parts.
|
||||
/// Existing cut-off parts are removed first, then each cut-off is
|
||||
/// regenerated and added back if it produces any geometry.
|
||||
/// </summary>
|
||||
public void RegenerateCutOffs(CutOffSettings settings)
|
||||
{
|
||||
// Remove existing cut-off parts
|
||||
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (Parts[i].BaseDrawing.IsCutOff)
|
||||
Parts.RemoveAt(i);
|
||||
}
|
||||
|
||||
var cache = BuildPerimeterCache(this);
|
||||
|
||||
// Regenerate and materialize each cut-off
|
||||
foreach (var cutoff in CutOffs)
|
||||
{
|
||||
cutoff.Regenerate(this, settings, cache);
|
||||
|
||||
if (cutoff.Drawing.Program.Codes.Count == 0)
|
||||
continue;
|
||||
|
||||
var part = new Part(cutoff.Drawing);
|
||||
Parts.Add(part);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a dictionary mapping each non-cut-off part to its perimeter entity.
|
||||
/// Closed shapes use ShapeProfile; open contours fall back to ConvexHull.
|
||||
/// </summary>
|
||||
public static Dictionary<Part, Geometry.Entity> BuildPerimeterCache(Plate plate)
|
||||
{
|
||||
var cache = new Dictionary<Part, Geometry.Entity>();
|
||||
|
||||
foreach (var part in plate.Parts)
|
||||
{
|
||||
if (part.BaseDrawing.IsCutOff)
|
||||
continue;
|
||||
|
||||
Geometry.Entity perimeter = null;
|
||||
try
|
||||
{
|
||||
var entities = Converters.ConvertProgram.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
|
||||
if (entities.Count > 0)
|
||||
{
|
||||
var profile = new Geometry.ShapeProfile(entities);
|
||||
|
||||
if (profile.Perimeter.IsClosed())
|
||||
{
|
||||
perimeter = profile.Perimeter;
|
||||
perimeter.Offset(part.Location);
|
||||
}
|
||||
else
|
||||
{
|
||||
var points = entities.CollectPoints();
|
||||
if (points.Count >= 3)
|
||||
{
|
||||
var hull = Geometry.ConvexHull.Compute(points);
|
||||
hull.Offset(part.Location);
|
||||
perimeter = hull;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
perimeter = null;
|
||||
}
|
||||
|
||||
cache[part] = perimeter;
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of times to cut the plate.
|
||||
/// </summary>
|
||||
@@ -240,11 +329,20 @@ namespace OpenNest
|
||||
/// <param name="angle"></param>
|
||||
public void Rotate(double angle)
|
||||
{
|
||||
for (int i = 0; i < Parts.Count; ++i)
|
||||
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (Parts[i].BaseDrawing.IsCutOff)
|
||||
Parts.RemoveAt(i);
|
||||
}
|
||||
|
||||
for (var i = 0; i < Parts.Count; ++i)
|
||||
{
|
||||
var part = Parts[i];
|
||||
part.Rotate(angle);
|
||||
}
|
||||
|
||||
foreach (var cutoff in CutOffs)
|
||||
cutoff.Position = cutoff.Position.Rotate(angle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -254,11 +352,24 @@ namespace OpenNest
|
||||
/// <param name="origin"></param>
|
||||
public void Rotate(double angle, Vector origin)
|
||||
{
|
||||
for (int i = 0; i < Parts.Count; ++i)
|
||||
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (Parts[i].BaseDrawing.IsCutOff)
|
||||
Parts.RemoveAt(i);
|
||||
}
|
||||
|
||||
for (var i = 0; i < Parts.Count; ++i)
|
||||
{
|
||||
var part = Parts[i];
|
||||
part.Rotate(angle, origin);
|
||||
}
|
||||
|
||||
foreach (var cutoff in CutOffs)
|
||||
{
|
||||
var pos = cutoff.Position - origin;
|
||||
pos = pos.Rotate(angle);
|
||||
cutoff.Position = pos + origin;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -268,11 +379,22 @@ namespace OpenNest
|
||||
/// <param name="y"></param>
|
||||
public void Offset(double x, double y)
|
||||
{
|
||||
for (int i = 0; i < Parts.Count; ++i)
|
||||
// Remove cut-off parts before transforming
|
||||
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (Parts[i].BaseDrawing.IsCutOff)
|
||||
Parts.RemoveAt(i);
|
||||
}
|
||||
|
||||
for (var i = 0; i < Parts.Count; ++i)
|
||||
{
|
||||
var part = Parts[i];
|
||||
part.Offset(x, y);
|
||||
}
|
||||
|
||||
// Transform cut-off positions
|
||||
foreach (var cutoff in CutOffs)
|
||||
cutoff.Position = new Vector(cutoff.Position.X + x, cutoff.Position.Y + y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -281,11 +403,20 @@ namespace OpenNest
|
||||
/// <param name="voffset"></param>
|
||||
public void Offset(Vector voffset)
|
||||
{
|
||||
for (int i = 0; i < Parts.Count; ++i)
|
||||
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (Parts[i].BaseDrawing.IsCutOff)
|
||||
Parts.RemoveAt(i);
|
||||
}
|
||||
|
||||
for (var i = 0; i < Parts.Count; ++i)
|
||||
{
|
||||
var part = Parts[i];
|
||||
part.Offset(voffset);
|
||||
}
|
||||
|
||||
foreach (var cutoff in CutOffs)
|
||||
cutoff.Position = new Vector(cutoff.Position.X + voffset.X, cutoff.Position.Y + voffset.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -454,24 +585,23 @@ namespace OpenNest
|
||||
/// <returns>Returns a number between 0.0 and 1.0</returns>
|
||||
public double Utilization()
|
||||
{
|
||||
return Parts.Sum(part => part.BaseDrawing.Area) / Area();
|
||||
return Parts.Where(p => !p.BaseDrawing.IsCutOff).Sum(part => part.BaseDrawing.Area) / Area();
|
||||
}
|
||||
|
||||
public bool HasOverlappingParts(out List<Vector> pts)
|
||||
{
|
||||
pts = new List<Vector>();
|
||||
var realParts = Parts.Where(p => !p.BaseDrawing.IsCutOff).ToList();
|
||||
|
||||
for (int i = 0; i < Parts.Count; i++)
|
||||
for (var i = 0; i < realParts.Count; i++)
|
||||
{
|
||||
var part1 = Parts[i];
|
||||
var part1 = realParts[i];
|
||||
|
||||
for (int j = i + 1; j < Parts.Count; j++)
|
||||
for (var j = i + 1; j < realParts.Count; j++)
|
||||
{
|
||||
var part2 = Parts[j];
|
||||
var part2 = realParts[j];
|
||||
|
||||
List<Vector> pts2;
|
||||
|
||||
if (part1.Intersects(part2, out pts2))
|
||||
if (part1.Intersects(part2, out var pts2))
|
||||
pts.AddRange(pts2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace OpenNest
|
||||
var best = context.CurrentBest ?? new List<Part>();
|
||||
|
||||
if (item.Quantity > 0 && best.Count > item.Quantity)
|
||||
best = ShrinkFiller.TrimToCount(best, item.Quantity, ShrinkAxis.Width);
|
||||
best = ShrinkFiller.TrimToCount(best, item.Quantity, TrimAxis);
|
||||
|
||||
ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace OpenNest.Engine.Fill
|
||||
/// <summary>
|
||||
/// Composes <see cref="RemnantFiller"/> and <see cref="ShrinkFiller"/> with
|
||||
/// dual-direction shrink selection. Wraps the caller's fill function in a
|
||||
/// closure that tries both <see cref="ShrinkAxis.Height"/> and
|
||||
/// closure that tries both <see cref="ShrinkAxis.Length"/> and
|
||||
/// <see cref="ShrinkAxis.Width"/>, picks the better <see cref="FillScore"/>,
|
||||
/// and passes the wrapper to <see cref="RemnantFiller.FillItems"/>.
|
||||
/// </summary>
|
||||
@@ -85,7 +85,7 @@ namespace OpenNest.Engine.Fill
|
||||
ShrinkResult widthResult = null;
|
||||
|
||||
Parallel.Invoke(
|
||||
() => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token,
|
||||
() => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Length, token,
|
||||
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar),
|
||||
() => widthResult = ShrinkFiller.Shrink(wFillFunc, ni, box, spacing, ShrinkAxis.Width, token,
|
||||
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar)
|
||||
|
||||
@@ -102,11 +102,21 @@ namespace OpenNest.Engine.Fill
|
||||
if (placed == null)
|
||||
continue;
|
||||
|
||||
// Remove the topmost bounding box part to create a clean
|
||||
// rectangular obstacle boundary. Without this, gaps between
|
||||
// individual bounding boxes cause the next drawing to fill
|
||||
// into inter-row spaces, producing an interleaved layout.
|
||||
if (placed.Count > 1)
|
||||
RemoveTopmostPart(placed);
|
||||
|
||||
allParts.AddRange(placed);
|
||||
localQty[item.Drawing.Name] = System.Math.Max(0, qty - placed.Count);
|
||||
|
||||
foreach (var p in placed)
|
||||
finder.AddObstacle(p.BoundingBox.Offset(spacing));
|
||||
// Add the envelope of all placed parts as a single obstacle
|
||||
// rather than individual bounding boxes, preventing the
|
||||
// remnant finder from seeing inter-part gaps.
|
||||
var envelope = ComputeEnvelope(placed, spacing);
|
||||
finder.AddObstacle(envelope);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -114,6 +124,39 @@ namespace OpenNest.Engine.Fill
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void RemoveTopmostPart(List<Part> parts)
|
||||
{
|
||||
var topIdx = 0;
|
||||
|
||||
for (var i = 1; i < parts.Count; i++)
|
||||
{
|
||||
if (parts[i].BoundingBox.Top > parts[topIdx].BoundingBox.Top)
|
||||
topIdx = i;
|
||||
}
|
||||
|
||||
parts.RemoveAt(topIdx);
|
||||
}
|
||||
|
||||
private static Box ComputeEnvelope(List<Part> parts, double spacing)
|
||||
{
|
||||
var left = double.MaxValue;
|
||||
var bottom = double.MaxValue;
|
||||
var right = double.MinValue;
|
||||
var top = double.MinValue;
|
||||
|
||||
foreach (var p in parts)
|
||||
{
|
||||
var bb = p.BoundingBox;
|
||||
if (bb.Left < left) left = bb.Left;
|
||||
if (bb.Bottom < bottom) bottom = bb.Bottom;
|
||||
if (bb.Right > right) right = bb.Right;
|
||||
if (bb.Top > top) top = bb.Top;
|
||||
}
|
||||
|
||||
return new Box(left - spacing, bottom - spacing,
|
||||
right - left + spacing * 2, top - bottom + spacing * 2);
|
||||
}
|
||||
|
||||
private static List<Part> TryFillInRemnants(
|
||||
NestItem item,
|
||||
int qty,
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Threading;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
public enum ShrinkAxis { Width, Height }
|
||||
public enum ShrinkAxis { Width, Length }
|
||||
|
||||
public class ShrinkResult
|
||||
{
|
||||
@@ -101,7 +101,7 @@ namespace OpenNest.Engine.Fill
|
||||
if (bbox.Width <= 0 || bbox.Length <= 0)
|
||||
return box;
|
||||
|
||||
var maxDim = axis == ShrinkAxis.Height ? box.Length : box.Width;
|
||||
var maxDim = axis == ShrinkAxis.Length ? box.Length : box.Width;
|
||||
|
||||
// Use FillBestFit for a fast, accurate rectangle count on the full box.
|
||||
var bin = new Bin { Size = new Size(box.Width, box.Length) };
|
||||
@@ -121,7 +121,7 @@ namespace OpenNest.Engine.Fill
|
||||
if (estimate <= 0 || estimate >= maxDim)
|
||||
return box;
|
||||
|
||||
return axis == ShrinkAxis.Height
|
||||
return axis == ShrinkAxis.Length
|
||||
? new Box(box.X, box.Y, box.Width, estimate)
|
||||
: new Box(box.X, box.Y, estimate, box.Length);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace OpenNest
|
||||
|
||||
public override NestDirection? PreferredDirection => NestDirection.Vertical;
|
||||
|
||||
public override ShrinkAxis TrimAxis => ShrinkAxis.Length;
|
||||
|
||||
public override List<double> BuildAngles(NestItem item, double bestRotation, Box workArea)
|
||||
{
|
||||
var baseAngles = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
@@ -43,6 +43,8 @@ namespace OpenNest
|
||||
|
||||
public virtual NestDirection? PreferredDirection => null;
|
||||
|
||||
public virtual ShrinkAxis TrimAxis => ShrinkAxis.Width;
|
||||
|
||||
public virtual List<double> BuildAngles(NestItem item, double bestRotation, Box workArea)
|
||||
{
|
||||
return new List<double> { bestRotation, bestRotation + OpenNest.Math.Angle.HalfPI };
|
||||
|
||||
@@ -21,10 +21,6 @@ namespace OpenNest
|
||||
"Strip-based nesting for mixed-drawing layouts",
|
||||
plate => new StripNestEngine(plate));
|
||||
|
||||
Register("NFP",
|
||||
"NFP-based mixed-part nesting with simulated annealing",
|
||||
plate => new NfpNestEngine(plate));
|
||||
|
||||
Register("Vertical Remnant",
|
||||
"Optimizes for largest right-side vertical drop",
|
||||
plate => new VerticalRemnantEngine(plate));
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Nfp;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
public class NfpNestEngine : NestEngineBase
|
||||
{
|
||||
public NfpNestEngine(Plate plate) : base(plate)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Name => "NFP";
|
||||
|
||||
public override string Description => "NFP-based mixed-part nesting with simulated annealing";
|
||||
|
||||
public override List<Part> Fill(NestItem item, Box workArea,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
{
|
||||
var inner = new DefaultNestEngine(Plate);
|
||||
return inner.Fill(item, workArea, progress, token);
|
||||
}
|
||||
|
||||
public override List<Part> Fill(List<Part> groupParts, Box workArea,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
{
|
||||
var inner = new DefaultNestEngine(Plate);
|
||||
return inner.Fill(groupParts, workArea, progress, token);
|
||||
}
|
||||
|
||||
public override List<Part> PackArea(Box box, List<NestItem> items,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
{
|
||||
var inner = new DefaultNestEngine(Plate);
|
||||
return inner.PackArea(box, items, progress, token);
|
||||
}
|
||||
|
||||
public override List<Part> Nest(List<NestItem> items,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return new List<Part>();
|
||||
|
||||
var parts = AutoNester.Nest(items, Plate, progress, token);
|
||||
|
||||
// Compact placed parts toward the origin to close gaps.
|
||||
Compactor.Settle(parts, Plate.WorkArea(), Plate.PartSpacing);
|
||||
|
||||
// Deduct placed quantities from original items.
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Quantity <= 0)
|
||||
continue;
|
||||
|
||||
var placed = parts.FindAll(p => p.BaseDrawing.Name == item.Drawing.Name).Count;
|
||||
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,42 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
public class PairsFillStrategy : IFillStrategy
|
||||
{
|
||||
private static readonly AsyncLocal<bool> active = new();
|
||||
|
||||
public string Name => "Pairs";
|
||||
public NestPhase Phase => NestPhase.Pairs;
|
||||
public int Order => 100;
|
||||
|
||||
public List<Part> Fill(FillContext context)
|
||||
{
|
||||
var comparer = context.Policy?.Comparer;
|
||||
var dedup = GridDedup.GetOrCreate(context.SharedState);
|
||||
var filler = new PairFiller(context.Plate, comparer, dedup);
|
||||
var result = filler.Fill(context.Item, context.WorkArea,
|
||||
context.PlateNumber, context.Token, context.Progress);
|
||||
// Prevent recursive PairFiller — remnant fills within PairFiller
|
||||
// create a new engine that runs the full pipeline, which would
|
||||
// invoke PairsFillStrategy again, causing deep recursion.
|
||||
if (active.Value)
|
||||
return null;
|
||||
|
||||
context.SharedState["BestFits"] = result.BestFits;
|
||||
active.Value = true;
|
||||
try
|
||||
{
|
||||
var comparer = context.Policy?.Comparer;
|
||||
var dedup = GridDedup.GetOrCreate(context.SharedState);
|
||||
var filler = new PairFiller(context.Plate, comparer, dedup);
|
||||
var result = filler.Fill(context.Item, context.WorkArea,
|
||||
context.PlateNumber, context.Token, context.Progress);
|
||||
|
||||
return result.Parts;
|
||||
context.SharedState["BestFits"] = result.BestFits;
|
||||
|
||||
return result.Parts;
|
||||
}
|
||||
finally
|
||||
{
|
||||
active.Value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace OpenNest.IO
|
||||
public MaterialDto Material { get; init; } = new();
|
||||
public SpacingDto EdgeSpacing { get; init; } = new();
|
||||
public List<PartDto> Parts { get; init; } = new();
|
||||
public List<CutOffDto> CutOffs { get; init; } = new();
|
||||
}
|
||||
|
||||
public record PartDto
|
||||
@@ -72,6 +73,15 @@ namespace OpenNest.IO
|
||||
public double Rotation { get; init; }
|
||||
}
|
||||
|
||||
public record CutOffDto
|
||||
{
|
||||
public double X { get; init; }
|
||||
public double Y { get; init; }
|
||||
public string Axis { get; init; } = "vertical";
|
||||
public double? StartLimit { get; init; }
|
||||
public double? EndLimit { get; init; }
|
||||
}
|
||||
|
||||
public record SizeDto
|
||||
{
|
||||
public double Width { get; init; }
|
||||
|
||||
@@ -197,6 +197,25 @@ namespace OpenNest.IO
|
||||
plate.Parts.Add(part);
|
||||
}
|
||||
|
||||
// Cut-offs
|
||||
if (p.CutOffs != null)
|
||||
{
|
||||
foreach (var cutoffDto in p.CutOffs)
|
||||
{
|
||||
var axis = cutoffDto.Axis?.ToLowerInvariant() == "horizontal"
|
||||
? CutOffAxis.Horizontal
|
||||
: CutOffAxis.Vertical;
|
||||
var cutoff = new CutOff(new Vector(cutoffDto.X, cutoffDto.Y), axis)
|
||||
{
|
||||
StartLimit = cutoffDto.StartLimit,
|
||||
EndLimit = cutoffDto.EndLimit
|
||||
};
|
||||
plate.CutOffs.Add(cutoff);
|
||||
}
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
}
|
||||
|
||||
nest.Plates.Add(plate);
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ namespace OpenNest.IO
|
||||
{
|
||||
var plate = nest.Plates[i];
|
||||
var parts = new List<PartDto>();
|
||||
foreach (var part in plate.Parts)
|
||||
foreach (var part in plate.Parts.Where(p => !p.BaseDrawing.IsCutOff))
|
||||
{
|
||||
var match = drawingDict.Where(dwg => dwg.Value == part.BaseDrawing).FirstOrDefault();
|
||||
parts.Add(new PartDto
|
||||
@@ -164,6 +164,19 @@ namespace OpenNest.IO
|
||||
});
|
||||
}
|
||||
|
||||
var cutoffs = new List<CutOffDto>();
|
||||
foreach (var cutoff in plate.CutOffs)
|
||||
{
|
||||
cutoffs.Add(new CutOffDto
|
||||
{
|
||||
X = cutoff.Position.X,
|
||||
Y = cutoff.Position.Y,
|
||||
Axis = cutoff.Axis == CutOffAxis.Vertical ? "vertical" : "horizontal",
|
||||
StartLimit = cutoff.StartLimit,
|
||||
EndLimit = cutoff.EndLimit
|
||||
});
|
||||
}
|
||||
|
||||
list.Add(new PlateDto
|
||||
{
|
||||
Id = i + 1,
|
||||
@@ -185,7 +198,8 @@ namespace OpenNest.IO
|
||||
Right = plate.EdgeSpacing.Right,
|
||||
Bottom = plate.EdgeSpacing.Bottom
|
||||
},
|
||||
Parts = parts
|
||||
Parts = parts,
|
||||
CutOffs = cutoffs
|
||||
});
|
||||
}
|
||||
return list;
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati;
|
||||
|
||||
/// <summary>
|
||||
/// Data class carrying all context needed to emit one Cincinnati-format G-code feature block.
|
||||
/// </summary>
|
||||
public sealed class FeatureContext
|
||||
{
|
||||
public List<ICode> Codes { get; set; } = new();
|
||||
public int FeatureNumber { get; set; }
|
||||
public string PartName { get; set; } = "";
|
||||
public bool IsFirstFeatureOfPart { get; set; }
|
||||
public bool IsLastFeatureOnSheet { get; set; }
|
||||
public bool IsSafetyHeadraise { get; set; }
|
||||
public bool IsExteriorFeature { get; set; }
|
||||
public string LibraryFile { get; set; } = "";
|
||||
public double CutDistance { get; set; }
|
||||
public double SheetDiagonal { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits one Cincinnati-format G-code feature block (one contour) to a TextWriter.
|
||||
/// Handles rapid positioning, pierce, kerf compensation, anti-dive, feedrate modal
|
||||
/// suppression, arc I/J conversion (absolute to incremental), and M47 head raise.
|
||||
/// </summary>
|
||||
public sealed class CincinnatiFeatureWriter
|
||||
{
|
||||
private readonly CincinnatiPostConfig _config;
|
||||
private readonly CoordinateFormatter _fmt;
|
||||
private readonly SpeedClassifier _speedClassifier;
|
||||
|
||||
public CincinnatiFeatureWriter(CincinnatiPostConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_fmt = new CoordinateFormatter(config.PostedAccuracy);
|
||||
_speedClassifier = new SpeedClassifier();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a complete feature block for the given context.
|
||||
/// </summary>
|
||||
public void Write(TextWriter writer, FeatureContext ctx)
|
||||
{
|
||||
var currentPos = Vector.Zero;
|
||||
var lastFeedVar = "";
|
||||
var kerfEmitted = false;
|
||||
|
||||
// Find the pierce point from the first rapid move
|
||||
var piercePoint = FindPiercePoint(ctx.Codes);
|
||||
|
||||
// 1. Rapid to pierce point (with line number if configured)
|
||||
WriteRapidToPierce(writer, ctx.FeatureNumber, piercePoint);
|
||||
|
||||
// 2. Part name comment on first feature of each part
|
||||
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
|
||||
writer.WriteLine(CoordinateFormatter.Comment($"PART: {ctx.PartName}"));
|
||||
|
||||
// 3. G89 process params (if RepeatG89BeforeEachFeature)
|
||||
if (_config.RepeatG89BeforeEachFeature && _config.ProcessParameterMode == G89Mode.LibraryFile)
|
||||
{
|
||||
var lib = !string.IsNullOrEmpty(ctx.LibraryFile) ? ctx.LibraryFile : _config.DefaultLibraryFile;
|
||||
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
|
||||
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
|
||||
writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
|
||||
}
|
||||
|
||||
// 4. Pierce and start cut
|
||||
writer.WriteLine("G84");
|
||||
|
||||
// 5. Anti-dive off
|
||||
if (_config.UseAntiDive)
|
||||
writer.WriteLine("M130 (ANTI DIVE OFF)");
|
||||
|
||||
// Update current position to pierce point
|
||||
currentPos = piercePoint;
|
||||
|
||||
// 6. Lead-in + contour moves with kerf comp and feedrate variables
|
||||
foreach (var code in ctx.Codes)
|
||||
{
|
||||
if (code is RapidMove)
|
||||
continue; // skip rapids in contour (already handled above)
|
||||
|
||||
if (code is LinearMove linear)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Kerf compensation on first cutting move
|
||||
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
|
||||
{
|
||||
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
|
||||
kerfEmitted = true;
|
||||
}
|
||||
|
||||
sb.Append($"G1X{_fmt.FormatCoord(linear.EndPoint.X)}Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
|
||||
|
||||
// Feedrate
|
||||
var feedVar = GetFeedVariable(linear.Layer);
|
||||
if (feedVar != lastFeedVar)
|
||||
{
|
||||
sb.Append($"F{feedVar}");
|
||||
lastFeedVar = feedVar;
|
||||
}
|
||||
|
||||
writer.WriteLine(sb.ToString());
|
||||
currentPos = linear.EndPoint;
|
||||
}
|
||||
else if (code is ArcMove arc)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Kerf compensation on first cutting move
|
||||
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
|
||||
{
|
||||
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
|
||||
kerfEmitted = true;
|
||||
}
|
||||
|
||||
// G2 = CW, G3 = CCW
|
||||
var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3";
|
||||
sb.Append($"{gCode}X{_fmt.FormatCoord(arc.EndPoint.X)}Y{_fmt.FormatCoord(arc.EndPoint.Y)}");
|
||||
|
||||
// Convert absolute center to incremental I/J
|
||||
var i = arc.CenterPoint.X - currentPos.X;
|
||||
var j = arc.CenterPoint.Y - currentPos.Y;
|
||||
sb.Append($"I{_fmt.FormatCoord(i)}J{_fmt.FormatCoord(j)}");
|
||||
|
||||
// Feedrate — full circles use multiplied feedrate
|
||||
var isFullCircle = IsFullCircle(currentPos, arc.EndPoint);
|
||||
var feedVar = isFullCircle ? "[#148*#128]" : GetFeedVariable(arc.Layer);
|
||||
if (feedVar != lastFeedVar)
|
||||
{
|
||||
sb.Append($"F{feedVar}");
|
||||
lastFeedVar = feedVar;
|
||||
}
|
||||
|
||||
writer.WriteLine(sb.ToString());
|
||||
currentPos = arc.EndPoint;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Cancel kerf compensation
|
||||
if (kerfEmitted)
|
||||
writer.WriteLine("G40");
|
||||
|
||||
// 8. Beam off
|
||||
writer.WriteLine(_config.UseSpeedGas ? "M135" : "M35");
|
||||
|
||||
// 9. Anti-dive on
|
||||
if (_config.UseAntiDive)
|
||||
writer.WriteLine("M131 (ANTI DIVE ON)");
|
||||
|
||||
// 10. Head raise (unless last feature on sheet)
|
||||
if (!ctx.IsLastFeatureOnSheet)
|
||||
WriteM47(writer, ctx);
|
||||
}
|
||||
|
||||
private Vector FindPiercePoint(List<ICode> codes)
|
||||
{
|
||||
foreach (var code in codes)
|
||||
{
|
||||
if (code is RapidMove rapid)
|
||||
return rapid.EndPoint;
|
||||
}
|
||||
|
||||
// If no rapid move, use the endpoint of the first motion
|
||||
foreach (var code in codes)
|
||||
{
|
||||
if (code is Motion motion)
|
||||
return motion.EndPoint;
|
||||
}
|
||||
|
||||
return Vector.Zero;
|
||||
}
|
||||
|
||||
private void WriteRapidToPierce(TextWriter writer, int featureNumber, Vector piercePoint)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (_config.UseLineNumbers)
|
||||
sb.Append($"N{featureNumber}");
|
||||
|
||||
sb.Append($"G0X{_fmt.FormatCoord(piercePoint.X)}Y{_fmt.FormatCoord(piercePoint.Y)}");
|
||||
|
||||
writer.WriteLine(sb.ToString());
|
||||
}
|
||||
|
||||
private void WriteM47(TextWriter writer, FeatureContext ctx)
|
||||
{
|
||||
if (ctx.IsSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
|
||||
{
|
||||
writer.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
|
||||
return;
|
||||
}
|
||||
|
||||
var mode = ctx.IsExteriorFeature ? _config.ExteriorM47 : _config.InteriorM47;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case M47Mode.Always:
|
||||
writer.WriteLine("M47");
|
||||
break;
|
||||
case M47Mode.BlockDelete:
|
||||
writer.WriteLine("/M47");
|
||||
break;
|
||||
case M47Mode.None:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFeedVariable(LayerType layer)
|
||||
{
|
||||
return layer switch
|
||||
{
|
||||
LayerType.Leadin => "#126",
|
||||
LayerType.Cut => "#148",
|
||||
_ => "#148"
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsFullCircle(Vector start, Vector end)
|
||||
{
|
||||
return Tolerance.IsEqualTo(start.X, end.X) && Tolerance.IsEqualTo(start.Y, end.Y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati;
|
||||
|
||||
/// <summary>
|
||||
/// Writes a Cincinnati-format part sub-program definition.
|
||||
/// Each sub-program contains the complete cutting sequence for one unique part geometry
|
||||
/// (drawing + rotation), with coordinates normalized to origin (0,0).
|
||||
/// Called via M98 from sheet sub-programs.
|
||||
/// </summary>
|
||||
public sealed class CincinnatiPartSubprogramWriter
|
||||
{
|
||||
private readonly CincinnatiPostConfig _config;
|
||||
private readonly CincinnatiFeatureWriter _featureWriter;
|
||||
|
||||
public CincinnatiPartSubprogramWriter(CincinnatiPostConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_featureWriter = new CincinnatiFeatureWriter(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a complete part sub-program for the given normalized program.
|
||||
/// The program coordinates must already be normalized to origin (0,0).
|
||||
/// </summary>
|
||||
public void Write(TextWriter w, Program normalizedProgram, string drawingName,
|
||||
int subNumber, string libraryFile, double sheetDiagonal)
|
||||
{
|
||||
var features = SplitFeatures(normalizedProgram.Codes);
|
||||
if (features.Count == 0)
|
||||
return;
|
||||
|
||||
w.WriteLine("(*****************************************************)");
|
||||
w.WriteLine($":{subNumber}");
|
||||
w.WriteLine(CoordinateFormatter.Comment($"PART: {drawingName}"));
|
||||
|
||||
for (var i = 0; i < features.Count; i++)
|
||||
{
|
||||
var codes = features[i];
|
||||
var featureNumber = i == 0
|
||||
? _config.FeatureLineNumberStart
|
||||
: 1000 + i + 1;
|
||||
var cutDistance = ComputeCutDistance(codes);
|
||||
|
||||
var ctx = new FeatureContext
|
||||
{
|
||||
Codes = codes,
|
||||
FeatureNumber = featureNumber,
|
||||
PartName = drawingName,
|
||||
IsFirstFeatureOfPart = false,
|
||||
IsLastFeatureOnSheet = i == features.Count - 1,
|
||||
IsSafetyHeadraise = false,
|
||||
IsExteriorFeature = false,
|
||||
LibraryFile = libraryFile,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
}
|
||||
|
||||
w.WriteLine("G0X0Y0");
|
||||
w.WriteLine($"M99(END OF {drawingName})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a sub-program key for matching parts to their sub-programs.
|
||||
/// </summary>
|
||||
internal static (int drawingId, long rotationKey) SubprogramKey(Part part) =>
|
||||
(part.BaseDrawing.Id, (long)System.Math.Round(part.Rotation * 1e6));
|
||||
|
||||
internal static List<List<ICode>> SplitFeatures(List<ICode> codes)
|
||||
{
|
||||
var features = new List<List<ICode>>();
|
||||
List<ICode> current = null;
|
||||
|
||||
foreach (var code in codes)
|
||||
{
|
||||
if (code is RapidMove)
|
||||
{
|
||||
if (current != null)
|
||||
features.Add(current);
|
||||
current = new List<ICode> { code };
|
||||
}
|
||||
else
|
||||
{
|
||||
current ??= new List<ICode>();
|
||||
current.Add(code);
|
||||
}
|
||||
}
|
||||
|
||||
if (current != null && current.Count > 0)
|
||||
features.Add(current);
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
internal static double ComputeCutDistance(List<ICode> codes)
|
||||
{
|
||||
var distance = 0.0;
|
||||
var currentPos = Vector.Zero;
|
||||
|
||||
foreach (var code in codes)
|
||||
{
|
||||
if (code is RapidMove rapid)
|
||||
currentPos = rapid.EndPoint;
|
||||
else if (code is LinearMove linear)
|
||||
{
|
||||
distance += currentPos.DistanceTo(linear.EndPoint);
|
||||
currentPos = linear.EndPoint;
|
||||
}
|
||||
else if (code is ArcMove arc)
|
||||
{
|
||||
distance += currentPos.DistanceTo(arc.EndPoint);
|
||||
currentPos = arc.EndPoint;
|
||||
}
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
namespace OpenNest.Posts.Cincinnati
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies how coordinate positioning is handled between parts.
|
||||
/// </summary>
|
||||
public enum CoordinateMode
|
||||
{
|
||||
/// <summary>Set absolute position.</summary>
|
||||
G92,
|
||||
|
||||
/// <summary>Use relative/incremental positioning.</summary>
|
||||
G91,
|
||||
|
||||
/// <summary>Use machine coordinate system.</summary>
|
||||
G53
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how G89 (hole drilling/tapping parameters) are provided.
|
||||
/// </summary>
|
||||
public enum G89Mode
|
||||
{
|
||||
/// <summary>Use external library file for G89 parameters.</summary>
|
||||
LibraryFile,
|
||||
|
||||
/// <summary>Explicitly define G89 parameters in the program.</summary>
|
||||
Explicit
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies where kerf compensation is applied.
|
||||
/// </summary>
|
||||
public enum KerfMode
|
||||
{
|
||||
/// <summary>Controller side (using cutter compensation codes).</summary>
|
||||
ControllerSide,
|
||||
|
||||
/// <summary>Pre-applied to part geometry during post-processing.</summary>
|
||||
PreApplied
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies which side of the cut line kerf compensation is applied to.
|
||||
/// </summary>
|
||||
public enum KerfSide
|
||||
{
|
||||
/// <summary>Kerf applied to the left side of the cut.</summary>
|
||||
Left,
|
||||
|
||||
/// <summary>Kerf applied to the right side of the cut.</summary>
|
||||
Right
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how M47 (optional stop) commands are used.
|
||||
/// </summary>
|
||||
public enum M47Mode
|
||||
{
|
||||
/// <summary>Always include M47.</summary>
|
||||
Always,
|
||||
|
||||
/// <summary>Include M47 with block delete functionality.</summary>
|
||||
BlockDelete,
|
||||
|
||||
/// <summary>Automatically determine M47 placement.</summary>
|
||||
Auto,
|
||||
|
||||
/// <summary>Do not use M47.</summary>
|
||||
None
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies when pallet exchange occurs.
|
||||
/// </summary>
|
||||
public enum PalletMode
|
||||
{
|
||||
/// <summary>No pallet exchange.</summary>
|
||||
None,
|
||||
|
||||
/// <summary>Pallet exchange at end of sheet.</summary>
|
||||
EndOfSheet,
|
||||
|
||||
/// <summary>Pallet exchange at start and end of sheet.</summary>
|
||||
StartAndEnd
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for Cincinnati post processor.
|
||||
/// Defines machine-specific parameters, output format, and cutting strategies.
|
||||
/// </summary>
|
||||
public sealed class CincinnatiPostConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration name/identifier.
|
||||
/// Default: "CL940"
|
||||
/// </summary>
|
||||
public string ConfigurationName { get; set; } = "CL940";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the units for posted output.
|
||||
/// Default: Units.Inches
|
||||
/// </summary>
|
||||
public Units PostedUnits { get; set; } = Units.Inches;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the decimal accuracy for numeric output.
|
||||
/// Default: 4
|
||||
/// </summary>
|
||||
public int PostedAccuracy { get; set; } = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how coordinate positioning is handled between parts.
|
||||
/// Default: CoordinateMode.G92
|
||||
/// </summary>
|
||||
public CoordinateMode CoordModeBetweenParts { get; set; } = CoordinateMode.G92;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use subprograms for sheet operations.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseSheetSubprograms { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting subprogram number for sheet operations.
|
||||
/// Default: 101
|
||||
/// </summary>
|
||||
public int SheetSubprogramStart { get; set; } = 101;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use M98 sub-programs for part geometry.
|
||||
/// When enabled, each unique part geometry is written as a reusable sub-program
|
||||
/// called via M98, reducing output size for nests with repeated parts.
|
||||
/// Default: false
|
||||
/// </summary>
|
||||
public bool UsePartSubprograms { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting sub-program number for part geometry sub-programs.
|
||||
/// Default: 200
|
||||
/// </summary>
|
||||
public int PartSubprogramStart { get; set; } = 200;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the subprogram number for variable declarations.
|
||||
/// Default: 100
|
||||
/// </summary>
|
||||
public int VariableDeclarationSubprogram { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how G89 parameters are provided.
|
||||
/// Default: G89Mode.LibraryFile
|
||||
/// </summary>
|
||||
public G89Mode ProcessParameterMode { get; set; } = G89Mode.LibraryFile;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default G89 library file path.
|
||||
/// Default: empty string
|
||||
/// </summary>
|
||||
public string DefaultLibraryFile { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to repeat G89 before each feature.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool RepeatG89BeforeEachFeature { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use exact stop mode (G61).
|
||||
/// Default: false
|
||||
/// </summary>
|
||||
public bool UseExactStopMode { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets where kerf compensation is applied.
|
||||
/// Default: KerfMode.ControllerSide
|
||||
/// </summary>
|
||||
public KerfMode KerfCompensation { get; set; } = KerfMode.ControllerSide;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default side for kerf compensation.
|
||||
/// Default: KerfSide.Left
|
||||
/// </summary>
|
||||
public KerfSide DefaultKerfSide { get; set; } = KerfSide.Left;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how M47 is used in interior cuts.
|
||||
/// Default: M47Mode.Always
|
||||
/// </summary>
|
||||
public M47Mode InteriorM47 { get; set; } = M47Mode.Always;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how M47 is used in exterior cuts.
|
||||
/// Default: M47Mode.Always
|
||||
/// </summary>
|
||||
public M47Mode ExteriorM47 { get; set; } = M47Mode.Always;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the safety head raise distance (in machine units).
|
||||
/// Default: 2000
|
||||
/// </summary>
|
||||
public int? SafetyHeadraiseDistance { get; set; } = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the distance threshold for M47 override.
|
||||
/// Default: null
|
||||
/// </summary>
|
||||
public double? M47OverrideDistanceThreshold { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use anti-dive functionality.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseAntiDive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use smart rapids optimization.
|
||||
/// Default: false
|
||||
/// </summary>
|
||||
public bool UseSmartRapids { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets when pallet exchange occurs.
|
||||
/// Default: PalletMode.EndOfSheet
|
||||
/// </summary>
|
||||
public PalletMode PalletExchange { get; set; } = PalletMode.EndOfSheet;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use line numbers in output.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseLineNumbers { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting line number for features.
|
||||
/// Default: 1
|
||||
/// </summary>
|
||||
public int FeatureLineNumberStart { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use speed/gas commands.
|
||||
/// Default: false
|
||||
/// </summary>
|
||||
public bool UseSpeedGas { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the feedrate percentage for lead-in moves.
|
||||
/// Default: 0.5 (50%)
|
||||
/// </summary>
|
||||
public double LeadInFeedratePercent { get; set; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the feedrate percentage for lead-in arc-to-line moves.
|
||||
/// Default: 0.5 (50%)
|
||||
/// </summary>
|
||||
public double LeadInArcLine2FeedratePercent { get; set; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the feedrate multiplier for circular cuts.
|
||||
/// Default: 0.8 (80%)
|
||||
/// </summary>
|
||||
public double CircleFeedrateMultiplier { get; set; } = 0.8;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the variable number for sheet width.
|
||||
/// Default: 110
|
||||
/// </summary>
|
||||
public int SheetWidthVariable { get; set; } = 110;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the variable number for sheet length.
|
||||
/// Default: 111
|
||||
/// </summary>
|
||||
public int SheetLengthVariable { get; set; } = 111;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using OpenNest.CNC;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati
|
||||
{
|
||||
public sealed class CincinnatiPostProcessor : IPostProcessor
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public string Name => "Cincinnati CL-707";
|
||||
public string Author => "OpenNest";
|
||||
public string Description => "Cincinnati CL-707/CL-800/CL-900/CL-940/CLX family";
|
||||
|
||||
public CincinnatiPostConfig Config { get; }
|
||||
|
||||
public CincinnatiPostProcessor()
|
||||
{
|
||||
var configPath = GetConfigPath();
|
||||
if (File.Exists(configPath))
|
||||
{
|
||||
var json = File.ReadAllText(configPath);
|
||||
Config = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, JsonOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
Config = new CincinnatiPostConfig();
|
||||
SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
public CincinnatiPostProcessor(CincinnatiPostConfig config)
|
||||
{
|
||||
Config = config;
|
||||
}
|
||||
|
||||
public void SaveConfig()
|
||||
{
|
||||
var configPath = GetConfigPath();
|
||||
var json = JsonSerializer.Serialize(Config, JsonOptions);
|
||||
File.WriteAllText(configPath, json);
|
||||
}
|
||||
|
||||
private static string GetConfigPath()
|
||||
{
|
||||
var assemblyPath = typeof(CincinnatiPostProcessor).Assembly.Location;
|
||||
var dir = Path.GetDirectoryName(assemblyPath);
|
||||
var name = Path.GetFileNameWithoutExtension(assemblyPath);
|
||||
return Path.Combine(dir, name + ".json");
|
||||
}
|
||||
|
||||
public void Post(Nest nest, Stream outputStream)
|
||||
{
|
||||
// 1. Create variable manager and register standard variables
|
||||
var vars = CreateVariableManager();
|
||||
|
||||
// 2. Filter to non-empty plates
|
||||
var plates = nest.Plates
|
||||
.Where(p => p.Parts.Count > 0)
|
||||
.ToList();
|
||||
|
||||
// 3. Build part sub-program registry (if enabled)
|
||||
Dictionary<(int, long), int> partSubprograms = null;
|
||||
List<(int subNum, string name, Program program)> subprogramEntries = null;
|
||||
|
||||
if (Config.UsePartSubprograms)
|
||||
{
|
||||
partSubprograms = new Dictionary<(int, long), int>();
|
||||
subprogramEntries = new List<(int, string, Program)>();
|
||||
var nextSubNum = Config.PartSubprogramStart;
|
||||
|
||||
foreach (var plate in plates)
|
||||
{
|
||||
foreach (var part in plate.Parts)
|
||||
{
|
||||
if (part.BaseDrawing.IsCutOff) continue;
|
||||
var key = CincinnatiPartSubprogramWriter.SubprogramKey(part);
|
||||
if (!partSubprograms.ContainsKey(key))
|
||||
{
|
||||
var subNum = nextSubNum++;
|
||||
partSubprograms[key] = subNum;
|
||||
|
||||
// Create normalized program at origin
|
||||
var pgm = part.Program.Clone() as Program;
|
||||
var bbox = pgm.BoundingBox();
|
||||
pgm.Offset(-bbox.Location.X, -bbox.Location.Y);
|
||||
|
||||
subprogramEntries.Add((subNum, part.BaseDrawing.Name, pgm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Create writers
|
||||
var preamble = new CincinnatiPreambleWriter(Config);
|
||||
var sheetWriter = new CincinnatiSheetWriter(Config, vars);
|
||||
|
||||
// 5. Build material description from first plate
|
||||
var material = plates.FirstOrDefault()?.Material;
|
||||
var materialDesc = material != null
|
||||
? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}"
|
||||
: "";
|
||||
|
||||
// 6. Write to stream
|
||||
using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true);
|
||||
|
||||
// Main program
|
||||
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates.Count);
|
||||
|
||||
// Variable declaration subprogram
|
||||
preamble.WriteVariableDeclaration(writer, vars);
|
||||
|
||||
// Sheet subprograms
|
||||
for (var i = 0; i < plates.Count; i++)
|
||||
{
|
||||
var sheetIndex = i + 1;
|
||||
var subNumber = Config.SheetSubprogramStart + i;
|
||||
sheetWriter.Write(writer, plates[i], nest.Name ?? "NEST", sheetIndex, subNumber,
|
||||
partSubprograms);
|
||||
}
|
||||
|
||||
// Part sub-programs (if enabled)
|
||||
if (subprogramEntries != null)
|
||||
{
|
||||
var partSubWriter = new CincinnatiPartSubprogramWriter(Config);
|
||||
var firstPlate = plates.FirstOrDefault();
|
||||
var sheetDiagonal = firstPlate != null
|
||||
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
|
||||
+ firstPlate.Size.Length * firstPlate.Size.Length)
|
||||
: 100.0;
|
||||
|
||||
foreach (var (subNum, name, pgm) in subprogramEntries)
|
||||
{
|
||||
partSubWriter.Write(writer, pgm, name, subNum,
|
||||
Config.DefaultLibraryFile ?? "", sheetDiagonal);
|
||||
}
|
||||
}
|
||||
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
public void Post(Nest nest, string outputFile)
|
||||
{
|
||||
using var fs = new FileStream(outputFile, FileMode.Create, FileAccess.Write);
|
||||
Post(nest, fs);
|
||||
}
|
||||
|
||||
private ProgramVariableManager CreateVariableManager()
|
||||
{
|
||||
var vars = new ProgramVariableManager();
|
||||
vars.GetOrCreate("ProcessFeedrate", 148); // Set by G89, no expression
|
||||
vars.GetOrCreate("LeadInFeedrate", 126, $"[#148*{Config.LeadInFeedratePercent}]");
|
||||
vars.GetOrCreate("LeadInArcLine2Feedrate", 127, $"[#148*{Config.LeadInArcLine2FeedratePercent}]");
|
||||
vars.GetOrCreate("CircleFeedrate", 128, Config.CircleFeedrateMultiplier.ToString("0.#"));
|
||||
return vars;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using OpenNest;
|
||||
using OpenNest.CNC;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati;
|
||||
|
||||
/// <summary>
|
||||
/// Emits the main program header and variable declaration subprogram
|
||||
/// for a Cincinnati laser post-processor output file.
|
||||
/// </summary>
|
||||
public sealed class CincinnatiPreambleWriter
|
||||
{
|
||||
private readonly CincinnatiPostConfig _config;
|
||||
|
||||
public CincinnatiPreambleWriter(CincinnatiPostConfig config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the main program header block.
|
||||
/// </summary>
|
||||
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription, int sheetCount)
|
||||
{
|
||||
w.WriteLine(CoordinateFormatter.Comment($"NEST {nestName}"));
|
||||
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
|
||||
w.WriteLine(CoordinateFormatter.Comment(DateTime.Now.ToString("MM-dd-yyyy hh:mm:ss tt", System.Globalization.CultureInfo.InvariantCulture)));
|
||||
|
||||
if (!string.IsNullOrEmpty(materialDescription))
|
||||
w.WriteLine(CoordinateFormatter.Comment($"Material = {materialDescription}"));
|
||||
|
||||
if (_config.UseExactStopMode)
|
||||
w.WriteLine("G61");
|
||||
|
||||
w.WriteLine(CoordinateFormatter.Comment("MAIN PROGRAM"));
|
||||
|
||||
w.WriteLine(_config.PostedUnits == Units.Millimeters ? "G21" : "G20");
|
||||
|
||||
w.WriteLine("M42");
|
||||
|
||||
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(_config.DefaultLibraryFile))
|
||||
w.WriteLine($"G89 P {_config.DefaultLibraryFile}");
|
||||
|
||||
w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)");
|
||||
|
||||
w.WriteLine("GOTO1 (GOTO SHEET NUMBER)");
|
||||
|
||||
for (var i = 1; i <= sheetCount; i++)
|
||||
{
|
||||
var subNum = _config.SheetSubprogramStart + (i - 1);
|
||||
w.WriteLine($"N{i}M98 P{subNum} (SHEET {i})");
|
||||
}
|
||||
|
||||
w.WriteLine("M42");
|
||||
w.WriteLine("M30 (END OF MAIN)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the variable declaration subprogram block.
|
||||
/// </summary>
|
||||
public void WriteVariableDeclaration(TextWriter w, ProgramVariableManager vars)
|
||||
{
|
||||
w.WriteLine("(*****************************************************)");
|
||||
w.WriteLine($":{_config.VariableDeclarationSubprogram}");
|
||||
w.WriteLine("(Variable Declaration Start)");
|
||||
|
||||
foreach (var line in vars.EmitDeclarations())
|
||||
w.WriteLine(line);
|
||||
|
||||
w.WriteLine("M99 (Variable Declaration End)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati;
|
||||
|
||||
/// <summary>
|
||||
/// Emits one Cincinnati-format sheet subprogram per plate.
|
||||
/// Supports two modes: inline features (default) or M98 sub-program calls per part.
|
||||
/// </summary>
|
||||
public sealed class CincinnatiSheetWriter
|
||||
{
|
||||
private readonly CincinnatiPostConfig _config;
|
||||
private readonly ProgramVariableManager _vars;
|
||||
private readonly CoordinateFormatter _fmt;
|
||||
private readonly CincinnatiFeatureWriter _featureWriter;
|
||||
|
||||
public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars)
|
||||
{
|
||||
_config = config;
|
||||
_vars = vars;
|
||||
_fmt = new CoordinateFormatter(config.PostedAccuracy);
|
||||
_featureWriter = new CincinnatiFeatureWriter(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a complete sheet subprogram for the given plate.
|
||||
/// </summary>
|
||||
/// <param name="partSubprograms">
|
||||
/// Optional mapping of (drawingId, rotationKey) to sub-program number.
|
||||
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
|
||||
/// </param>
|
||||
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
|
||||
Dictionary<(int, long), int> partSubprograms = null)
|
||||
{
|
||||
if (plate.Parts.Count == 0)
|
||||
return;
|
||||
|
||||
var width = plate.Size.Width;
|
||||
var length = plate.Size.Length;
|
||||
var sheetDiagonal = System.Math.Sqrt(width * width + length * length);
|
||||
var libraryFile = _config.DefaultLibraryFile ?? "";
|
||||
var varDeclSub = _config.VariableDeclarationSubprogram;
|
||||
var partCount = plate.Parts.Count(p => !p.BaseDrawing.IsCutOff);
|
||||
|
||||
// 1. Sheet header
|
||||
w.WriteLine("(*****************************************************)");
|
||||
w.WriteLine($"( START OF {nestName}.{sheetIndex:D3} )");
|
||||
w.WriteLine($":{subNumber}");
|
||||
w.WriteLine($"( Sheet {sheetIndex} )");
|
||||
w.WriteLine($"( Layout {sheetIndex} )");
|
||||
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(length)} X {_fmt.FormatCoord(width)} )");
|
||||
w.WriteLine($"( Total parts on sheet = {partCount} )");
|
||||
w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)}(SHEET WIDTH FOR CUTOFFS)");
|
||||
w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)}(SHEET LENGTH FOR CUTOFFS)");
|
||||
|
||||
// 2. Coordinate setup
|
||||
w.WriteLine("M42");
|
||||
w.WriteLine("N10000");
|
||||
w.WriteLine("G92X#5021Y#5022");
|
||||
if (!string.IsNullOrEmpty(libraryFile))
|
||||
w.WriteLine($"G89 P {libraryFile}");
|
||||
w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)");
|
||||
w.WriteLine("G90");
|
||||
w.WriteLine("M47(CPT)");
|
||||
if (!string.IsNullOrEmpty(libraryFile))
|
||||
w.WriteLine($"G89 P {libraryFile}");
|
||||
w.WriteLine("GOTO1( Goto Feature )");
|
||||
|
||||
// 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last
|
||||
var nonCutoffParts = plate.Parts
|
||||
.Where(p => !p.BaseDrawing.IsCutOff)
|
||||
.OrderBy(p => p.Bottom)
|
||||
.ThenBy(p => p.Left)
|
||||
.ToList();
|
||||
|
||||
var cutoffParts = plate.Parts
|
||||
.Where(p => p.BaseDrawing.IsCutOff)
|
||||
.ToList();
|
||||
|
||||
var allParts = nonCutoffParts.Concat(cutoffParts).ToList();
|
||||
|
||||
// 4. Emit parts
|
||||
if (partSubprograms != null)
|
||||
WritePartsWithSubprograms(w, allParts, libraryFile, sheetDiagonal, partSubprograms);
|
||||
else
|
||||
WritePartsInline(w, allParts, libraryFile, sheetDiagonal);
|
||||
|
||||
// 5. Footer
|
||||
w.WriteLine("M42");
|
||||
w.WriteLine("G0X0Y0");
|
||||
if (_config.PalletExchange != PalletMode.None)
|
||||
w.WriteLine($"N{sheetIndex + 1}M50");
|
||||
w.WriteLine($"M99(END OF {nestName}.{sheetIndex:D3})");
|
||||
}
|
||||
|
||||
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
|
||||
string libraryFile, double sheetDiagonal,
|
||||
Dictionary<(int, long), int> partSubprograms)
|
||||
{
|
||||
var lastPartName = "";
|
||||
var featureIndex = 0;
|
||||
|
||||
for (var p = 0; p < allParts.Count; p++)
|
||||
{
|
||||
var part = allParts[p];
|
||||
var partName = part.BaseDrawing.Name;
|
||||
var isNewPart = partName != lastPartName;
|
||||
var isSafetyHeadraise = isNewPart && lastPartName != "";
|
||||
var isLastPart = p == allParts.Count - 1;
|
||||
|
||||
var key = CincinnatiPartSubprogramWriter.SubprogramKey(part);
|
||||
partSubprograms.TryGetValue(key, out var subNum);
|
||||
var hasSubprogram = !part.BaseDrawing.IsCutOff && subNum != 0;
|
||||
|
||||
if (hasSubprogram)
|
||||
{
|
||||
WriteSubprogramCall(w, part, subNum, featureIndex, partName,
|
||||
isSafetyHeadraise, isLastPart);
|
||||
featureIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Inline features for cutoffs or parts without sub-programs
|
||||
var features = SplitPartFeatures(part);
|
||||
for (var f = 0; f < features.Count; f++)
|
||||
{
|
||||
var featureNumber = featureIndex == 0
|
||||
? _config.FeatureLineNumberStart
|
||||
: 1000 + featureIndex + 1;
|
||||
|
||||
var isLastFeature = isLastPart && f == features.Count - 1;
|
||||
var cutDistance = ComputeCutDistance(features[f]);
|
||||
|
||||
var ctx = new FeatureContext
|
||||
{
|
||||
Codes = features[f],
|
||||
FeatureNumber = featureNumber,
|
||||
PartName = partName,
|
||||
IsFirstFeatureOfPart = isNewPart && f == 0,
|
||||
IsLastFeatureOnSheet = isLastFeature,
|
||||
IsSafetyHeadraise = isSafetyHeadraise && f == 0,
|
||||
IsExteriorFeature = false,
|
||||
LibraryFile = libraryFile,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
featureIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
lastPartName = partName;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteSubprogramCall(TextWriter w, Part part, int subNum,
|
||||
int featureIndex, string partName, bool isSafetyHeadraise, bool isLastPart)
|
||||
{
|
||||
// Safety headraise before rapid to new part
|
||||
if (isSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
|
||||
w.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
|
||||
|
||||
// Rapid to part position (bounding box lower-left)
|
||||
var featureNumber = featureIndex == 0
|
||||
? _config.FeatureLineNumberStart
|
||||
: 1000 + featureIndex + 1;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
if (_config.UseLineNumbers)
|
||||
sb.Append($"N{featureNumber}");
|
||||
sb.Append($"G0X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}");
|
||||
w.WriteLine(sb.ToString());
|
||||
|
||||
// Part name comment
|
||||
w.WriteLine(CoordinateFormatter.Comment($"PART: {partName}"));
|
||||
|
||||
// Set local coordinate system at part position
|
||||
w.WriteLine("G92X0Y0");
|
||||
|
||||
// Call part sub-program
|
||||
w.WriteLine($"M98P{subNum}({partName})");
|
||||
|
||||
// Restore sheet coordinate system
|
||||
w.WriteLine($"G92X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}");
|
||||
|
||||
// Head raise (unless last part on sheet)
|
||||
if (!isLastPart)
|
||||
w.WriteLine("M47");
|
||||
}
|
||||
|
||||
private void WritePartsInline(TextWriter w, List<Part> allParts,
|
||||
string libraryFile, double sheetDiagonal)
|
||||
{
|
||||
// Multi-contour splitting
|
||||
var features = new List<(Part part, List<ICode> codes)>();
|
||||
foreach (var part in allParts)
|
||||
{
|
||||
List<ICode> current = null;
|
||||
foreach (var code in part.Program.Codes)
|
||||
{
|
||||
if (code is RapidMove)
|
||||
{
|
||||
if (current != null)
|
||||
features.Add((part, current));
|
||||
current = new List<ICode> { code };
|
||||
}
|
||||
else
|
||||
{
|
||||
current ??= new List<ICode>();
|
||||
current.Add(code);
|
||||
}
|
||||
}
|
||||
if (current != null && current.Count > 0)
|
||||
features.Add((part, current));
|
||||
}
|
||||
|
||||
// Emit features
|
||||
var lastPartName = "";
|
||||
for (var i = 0; i < features.Count; i++)
|
||||
{
|
||||
var (part, codes) = features[i];
|
||||
var partName = part.BaseDrawing.Name;
|
||||
var isFirstFeatureOfPart = partName != lastPartName;
|
||||
var isSafetyHeadraise = partName != lastPartName && lastPartName != "";
|
||||
var isLastFeature = i == features.Count - 1;
|
||||
|
||||
var featureNumber = i == 0
|
||||
? _config.FeatureLineNumberStart
|
||||
: 1000 + i + 1;
|
||||
|
||||
var cutDistance = ComputeCutDistance(codes);
|
||||
|
||||
var ctx = new FeatureContext
|
||||
{
|
||||
Codes = codes,
|
||||
FeatureNumber = featureNumber,
|
||||
PartName = partName,
|
||||
IsFirstFeatureOfPart = isFirstFeatureOfPart,
|
||||
IsLastFeatureOnSheet = isLastFeature,
|
||||
IsSafetyHeadraise = isSafetyHeadraise,
|
||||
IsExteriorFeature = false,
|
||||
LibraryFile = libraryFile,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
lastPartName = partName;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<List<ICode>> SplitPartFeatures(Part part)
|
||||
{
|
||||
var features = new List<List<ICode>>();
|
||||
List<ICode> current = null;
|
||||
|
||||
foreach (var code in part.Program.Codes)
|
||||
{
|
||||
if (code is RapidMove)
|
||||
{
|
||||
if (current != null)
|
||||
features.Add(current);
|
||||
current = new List<ICode> { code };
|
||||
}
|
||||
else
|
||||
{
|
||||
current ??= new List<ICode>();
|
||||
current.Add(code);
|
||||
}
|
||||
}
|
||||
|
||||
if (current != null && current.Count > 0)
|
||||
features.Add(current);
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
private static double ComputeCutDistance(List<ICode> codes)
|
||||
{
|
||||
var distance = 0.0;
|
||||
var currentPos = Vector.Zero;
|
||||
|
||||
foreach (var code in codes)
|
||||
{
|
||||
if (code is RapidMove rapid)
|
||||
{
|
||||
currentPos = rapid.EndPoint;
|
||||
}
|
||||
else if (code is LinearMove linear)
|
||||
{
|
||||
distance += currentPos.DistanceTo(linear.EndPoint);
|
||||
currentPos = linear.EndPoint;
|
||||
}
|
||||
else if (code is ArcMove arc)
|
||||
{
|
||||
distance += currentPos.DistanceTo(arc.EndPoint);
|
||||
currentPos = arc.EndPoint;
|
||||
}
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace OpenNest.Posts.Cincinnati
|
||||
{
|
||||
public sealed class CoordinateFormatter
|
||||
{
|
||||
private readonly int _accuracy;
|
||||
private readonly string _format;
|
||||
|
||||
public CoordinateFormatter(int accuracy)
|
||||
{
|
||||
_accuracy = accuracy;
|
||||
_format = "0." + new string('#', accuracy);
|
||||
}
|
||||
|
||||
public string FormatCoord(double value)
|
||||
{
|
||||
return System.Math.Round(value, _accuracy)
|
||||
.ToString(_format, System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string Comment(string text) => $"( {text} )";
|
||||
|
||||
public static string InlineComment(string text) => $"({text})";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RootNamespace>OpenNest.Posts.Cincinnati</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<Target Name="CopyToPostsDir" AfterTargets="Build">
|
||||
<PropertyGroup>
|
||||
<PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir>
|
||||
</PropertyGroup>
|
||||
<MakeDir Directories="$(PostsDir)" />
|
||||
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace OpenNest.Posts.Cincinnati
|
||||
{
|
||||
public sealed class SpeedClassifier
|
||||
{
|
||||
public double FastThreshold { get; set; } = 0.5;
|
||||
public double SlowThreshold { get; set; } = 0.1;
|
||||
|
||||
public string Classify(double contourLength, double sheetDiagonal)
|
||||
{
|
||||
var ratio = contourLength / sheetDiagonal;
|
||||
if (ratio >= FastThreshold) return "FAST";
|
||||
if (ratio <= SlowThreshold) return "SLOW";
|
||||
return "MEDIUM";
|
||||
}
|
||||
|
||||
public string FormatCutDist(double contourLength, double sheetDiagonal)
|
||||
{
|
||||
return $"CutDist={FormatValue(contourLength)}/{FormatValue(sheetDiagonal)}";
|
||||
}
|
||||
|
||||
private static string FormatValue(double value)
|
||||
{
|
||||
// Cincinnati convention: no leading zero for values < 1 (e.g., ".8702" not "0.8702")
|
||||
var rounded = System.Math.Round(value, 4);
|
||||
var str = rounded.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
|
||||
if (rounded > 0 && rounded < 1 && str.StartsWith("0."))
|
||||
return str.Substring(1);
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
public class CincinnatiFeatureWriterTests
|
||||
{
|
||||
private static CincinnatiPostConfig DefaultConfig() => new()
|
||||
{
|
||||
UseLineNumbers = true,
|
||||
FeatureLineNumberStart = 1,
|
||||
UseAntiDive = true,
|
||||
KerfCompensation = KerfMode.ControllerSide,
|
||||
DefaultKerfSide = KerfSide.Left,
|
||||
RepeatG89BeforeEachFeature = true,
|
||||
ProcessParameterMode = G89Mode.LibraryFile,
|
||||
DefaultLibraryFile = "MILD10",
|
||||
InteriorM47 = M47Mode.Always,
|
||||
ExteriorM47 = M47Mode.Always,
|
||||
UseSpeedGas = false,
|
||||
PostedAccuracy = 4,
|
||||
SafetyHeadraiseDistance = 2000
|
||||
};
|
||||
|
||||
private static FeatureContext SimpleContext(List<ICode>? codes = null) => new()
|
||||
{
|
||||
Codes = codes ?? new List<ICode>
|
||||
{
|
||||
new RapidMove(13.401, 57.4895),
|
||||
new LinearMove(14.0, 57.5) { Layer = LayerType.Leadin },
|
||||
new LinearMove(20.0, 57.5) { Layer = LayerType.Cut }
|
||||
},
|
||||
FeatureNumber = 1,
|
||||
PartName = "BRACKET",
|
||||
IsFirstFeatureOfPart = true,
|
||||
IsLastFeatureOnSheet = false,
|
||||
IsSafetyHeadraise = false,
|
||||
IsExteriorFeature = false,
|
||||
LibraryFile = "MILD10",
|
||||
CutDistance = 18.0,
|
||||
SheetDiagonal = 30.0
|
||||
};
|
||||
|
||||
private static string WriteFeature(CincinnatiPostConfig config, FeatureContext ctx)
|
||||
{
|
||||
var writer = new CincinnatiFeatureWriter(config);
|
||||
using var sw = new StringWriter();
|
||||
writer.Write(sw, ctx);
|
||||
return sw.ToString();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RapidToPiercePoint_WithLineNumber()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
Assert.StartsWith("N1G0X13.401Y57.4895", lines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RapidToPiercePoint_WithoutLineNumber()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.UseLineNumbers = false;
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
Assert.StartsWith("G0X13.401Y57.4895", lines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void G84_PierceEmitted()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("G84", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AntiDive_M130M131_EmittedWhenEnabled()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.UseAntiDive = true;
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("M130 (ANTI DIVE OFF)", output);
|
||||
Assert.Contains("M131 (ANTI DIVE ON)", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AntiDive_NotEmittedWhenDisabled()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.UseAntiDive = false;
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.DoesNotContain("M130", output);
|
||||
Assert.DoesNotContain("M131", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KerfCompensation_G41G40_EmittedWhenControllerSide()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.ControllerSide;
|
||||
config.DefaultKerfSide = KerfSide.Left;
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("G41", output);
|
||||
Assert.Contains("G40", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KerfCompensation_G42_EmittedForRightSide()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.ControllerSide;
|
||||
config.DefaultKerfSide = KerfSide.Right;
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("G42", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KerfCompensation_NotEmittedWhenPreApplied()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.DoesNotContain("G41", output);
|
||||
Assert.DoesNotContain("G42", output);
|
||||
Assert.DoesNotContain("G40", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void M35_BeamOffEmitted()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.UseSpeedGas = false;
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("M35", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void M135_BeamOffEmittedWhenSpeedGas()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.UseSpeedGas = true;
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("M135", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void M47_EmittedWhenNotLastFeature()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
var ctx = SimpleContext();
|
||||
ctx.IsLastFeatureOnSheet = false;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("M47", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void M47_OmittedWhenLastFeatureOnSheet()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
var ctx = SimpleContext();
|
||||
ctx.IsLastFeatureOnSheet = true;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
// M47 should not appear, but M35 should still be there
|
||||
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
||||
Assert.DoesNotContain(lines, l => l.Trim() == "M47" || l.Trim() == "/M47");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void M47_BlockDeleteMode_EmitsSlashM47()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.InteriorM47 = M47Mode.BlockDelete;
|
||||
var ctx = SimpleContext();
|
||||
ctx.IsExteriorFeature = false;
|
||||
ctx.IsLastFeatureOnSheet = false;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("/M47", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void M47_NoneMode_NoM47Emitted()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.InteriorM47 = M47Mode.None;
|
||||
var ctx = SimpleContext();
|
||||
ctx.IsExteriorFeature = false;
|
||||
ctx.IsLastFeatureOnSheet = false;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
||||
Assert.DoesNotContain(lines, l => l.Trim() == "M47" || l.Trim() == "/M47");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcIJ_ConvertedFromAbsoluteToIncremental()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
// Arc starts at rapid endpoint (10, 20), center at (15, 20) absolute
|
||||
// So incremental I = 15 - 10 = 5, J = 20 - 20 = 0
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(
|
||||
endPoint: new Vector(10.0, 20.0),
|
||||
centerPoint: new Vector(15.0, 20.0),
|
||||
rotation: RotationType.CW
|
||||
) { Layer = LayerType.Cut }
|
||||
};
|
||||
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
// Should contain incremental I=5, J=0
|
||||
Assert.Contains("I5", output);
|
||||
Assert.Contains("J0", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcMove_G2ForCW_G3ForCCW()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
var cwCodes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ccwCodes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CCW) { Layer = LayerType.Cut }
|
||||
};
|
||||
|
||||
var cwOutput = WriteFeature(config, SimpleContext(cwCodes));
|
||||
var ccwOutput = WriteFeature(config, SimpleContext(ccwCodes));
|
||||
|
||||
Assert.Contains("G2X", cwOutput);
|
||||
Assert.Contains("G3X", ccwOutput);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartNameComment_EmittedOnFirstFeature()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
var ctx = SimpleContext();
|
||||
ctx.IsFirstFeatureOfPart = true;
|
||||
ctx.PartName = "FLANGE";
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("( PART: FLANGE )", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartNameComment_NotEmittedOnSubsequentFeatures()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
var ctx = SimpleContext();
|
||||
ctx.IsFirstFeatureOfPart = false;
|
||||
ctx.PartName = "FLANGE";
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.DoesNotContain("PART:", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void G89_EmittedWhenRepeatEnabled()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.RepeatG89BeforeEachFeature = true;
|
||||
config.ProcessParameterMode = G89Mode.LibraryFile;
|
||||
config.DefaultLibraryFile = "MILD10";
|
||||
var ctx = SimpleContext();
|
||||
ctx.LibraryFile = "MILD10";
|
||||
ctx.CutDistance = 18.0;
|
||||
ctx.SheetDiagonal = 30.0;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("G89 P MILD10", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void G89_NotEmittedWhenRepeatDisabled()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.RepeatG89BeforeEachFeature = false;
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.DoesNotContain("G89", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FeedrateModalSuppression_OnlyEmitsOnChange()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied; // simplify output
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(1.0, 1.0),
|
||||
new LinearMove(2.0, 1.0) { Layer = LayerType.Cut },
|
||||
new LinearMove(3.0, 1.0) { Layer = LayerType.Cut },
|
||||
new LinearMove(4.0, 1.0) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
// F#148 should appear only once (on the first cut move)
|
||||
var count = CountOccurrences(output, "F#148");
|
||||
Assert.Equal(1, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeadinFeedrate_UsesVariable126()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied; // simplify output
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(1.0, 1.0),
|
||||
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#126", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullCircleArc_UsesMultipliedFeedrate()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
// Full circle: start == end
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F[#148*#128]", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SafetyHeadraise_EmitsM47WithDistance()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.SafetyHeadraiseDistance = 2000;
|
||||
var ctx = SimpleContext();
|
||||
ctx.IsSafetyHeadraise = true;
|
||||
ctx.IsLastFeatureOnSheet = false;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("M47 P2000(Safety Headraise)", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExteriorM47Mode_UsesExteriorConfig()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.ExteriorM47 = M47Mode.BlockDelete;
|
||||
config.InteriorM47 = M47Mode.Always;
|
||||
var ctx = SimpleContext();
|
||||
ctx.IsExteriorFeature = true;
|
||||
ctx.IsLastFeatureOnSheet = false;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("/M47", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputSequence_CorrectOrder()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
var ctx = SimpleContext();
|
||||
var output = WriteFeature(config, ctx);
|
||||
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Find indices of key lines
|
||||
var rapidIdx = Array.FindIndex(lines, l => l.Contains("G0X"));
|
||||
var partIdx = Array.FindIndex(lines, l => l.Contains("PART:"));
|
||||
var g89Idx = Array.FindIndex(lines, l => l.Contains("G89"));
|
||||
var g84Idx = Array.FindIndex(lines, l => l.Contains("G84"));
|
||||
var m130Idx = Array.FindIndex(lines, l => l.Contains("M130"));
|
||||
var g40Idx = Array.FindIndex(lines, l => l.Contains("G40"));
|
||||
var m35Idx = Array.FindIndex(lines, l => l.Contains("M35"));
|
||||
var m131Idx = Array.FindIndex(lines, l => l.Contains("M131"));
|
||||
var m47Idx = Array.FindIndex(lines, l => l.Trim() == "M47");
|
||||
|
||||
Assert.True(rapidIdx < partIdx, "Rapid should come before part comment");
|
||||
Assert.True(partIdx < g89Idx, "Part comment should come before G89");
|
||||
Assert.True(g89Idx < g84Idx, "G89 should come before G84");
|
||||
Assert.True(g84Idx < m130Idx, "G84 should come before M130");
|
||||
Assert.True(g40Idx < m35Idx, "G40 should come before M35");
|
||||
Assert.True(m35Idx < m131Idx, "M35 should come before M131");
|
||||
Assert.True(m131Idx < m47Idx, "M131 should come before M47");
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string text, string pattern)
|
||||
{
|
||||
var count = 0;
|
||||
var idx = 0;
|
||||
while ((idx = text.IndexOf(pattern, idx, StringComparison.Ordinal)) != -1)
|
||||
{
|
||||
count++;
|
||||
idx += pattern.Length;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
public class CincinnatiPostProcessorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Post_ProducesOutput_ForSinglePlateNest()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
ConfigurationName = "CL940",
|
||||
DefaultLibraryFile = "MS135N2PANEL.lib",
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
|
||||
// Main program elements
|
||||
Assert.Contains("( NEST TestNest )", output);
|
||||
Assert.Contains("( CONFIGURATION - CL940 )", output);
|
||||
Assert.Contains("G20", output);
|
||||
Assert.Contains("M30 (END OF MAIN)", output);
|
||||
|
||||
// Variable declaration
|
||||
Assert.Contains(":100", output);
|
||||
Assert.Contains("#126=", output);
|
||||
|
||||
// Sheet subprogram
|
||||
Assert.Contains(":101", output);
|
||||
Assert.Contains("( Sheet 1 )", output);
|
||||
Assert.Contains("G84", output);
|
||||
Assert.Contains("M99", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_ImplementsIPostProcessor()
|
||||
{
|
||||
var post = new CincinnatiPostProcessor(new CincinnatiPostConfig());
|
||||
IPostProcessor pp = post;
|
||||
|
||||
Assert.Equal("Cincinnati CL-707", pp.Name);
|
||||
Assert.Equal("OpenNest", pp.Author);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_SkipsEmptyPlates()
|
||||
{
|
||||
var nest = new Nest("TestNest");
|
||||
nest.Plates.Add(new Plate(48, 96)); // empty plate
|
||||
var plate2 = new Plate(48, 96);
|
||||
plate2.Parts.Add(new Part(new Drawing("Part1", CreateSquareProgram())));
|
||||
nest.Plates.Add(plate2);
|
||||
|
||||
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
|
||||
// Should only have one sheet subprogram call in main
|
||||
Assert.Contains("N1M98 P101 (SHEET 1)", output);
|
||||
Assert.DoesNotContain("SHEET 2", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_ToFile_CreatesFile()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
var tempFile = Path.GetTempFileName() + ".CNC";
|
||||
|
||||
try
|
||||
{
|
||||
post.Post(nest, tempFile);
|
||||
Assert.True(File.Exists(tempFile));
|
||||
var content = File.ReadAllText(tempFile);
|
||||
Assert.Contains("M30", content);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Config_RoundTripsAsJson()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
ConfigurationName = "CL940_CORONA",
|
||||
DefaultLibraryFile = "MS135N2PANEL.lib",
|
||||
PostedUnits = Units.Inches,
|
||||
KerfCompensation = KerfMode.ControllerSide,
|
||||
UseAntiDive = true
|
||||
};
|
||||
|
||||
var opts = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
var json = JsonSerializer.Serialize(config, opts);
|
||||
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
|
||||
|
||||
Assert.Equal("CL940_CORONA", deserialized.ConfigurationName);
|
||||
Assert.Equal("MS135N2PANEL.lib", deserialized.DefaultLibraryFile);
|
||||
Assert.Equal(Units.Inches, deserialized.PostedUnits);
|
||||
Assert.Equal(KerfMode.ControllerSide, deserialized.KerfCompensation);
|
||||
Assert.True(deserialized.UseAntiDive);
|
||||
|
||||
// Enums serialize as strings
|
||||
Assert.Contains("\"Inches\"", json);
|
||||
Assert.Contains("\"ControllerSide\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParameterlessConstructor_LoadsOrCreatesConfig()
|
||||
{
|
||||
// The parameterless constructor reads from a .json file next to the assembly,
|
||||
// or creates defaults if none exists. Either way, Config should be non-null.
|
||||
var post = new CincinnatiPostProcessor();
|
||||
Assert.NotNull(post.Config);
|
||||
Assert.Equal("CL940", post.Config.ConfigurationName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_WithPartSubprograms_WritesM98Calls()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PostedAccuracy = 4,
|
||||
UsePartSubprograms = true,
|
||||
PartSubprogramStart = 200
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
|
||||
// Sheet should contain M98 call to part sub-program
|
||||
Assert.Contains("M98P200", output);
|
||||
|
||||
// Should have G92 for local coordinate positioning
|
||||
Assert.Contains("G92X0Y0", output);
|
||||
|
||||
// Part sub-program definition
|
||||
Assert.Contains(":200", output);
|
||||
Assert.Contains("G84", output);
|
||||
|
||||
// Sub-program ends with G0X0Y0 and M99
|
||||
Assert.Contains("G0X0Y0", output);
|
||||
Assert.Contains("M99(END OF Square)", output);
|
||||
|
||||
// G92 restore after M98 call
|
||||
Assert.Contains("G92X", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_WithPartSubprograms_ReusesSameSubprogram()
|
||||
{
|
||||
var nest = new Nest("TestNest");
|
||||
var drawing = new Drawing("Square", CreateSquareProgram());
|
||||
var plate = new Plate(48, 96);
|
||||
plate.Parts.Add(new Part(drawing, new Vector(5, 5)));
|
||||
plate.Parts.Add(new Part(drawing, new Vector(20, 5)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PostedAccuracy = 4,
|
||||
UsePartSubprograms = true,
|
||||
PartSubprogramStart = 200
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
|
||||
// Both parts should call the same sub-program
|
||||
var m98Count = System.Text.RegularExpressions.Regex.Matches(output, "M98P200").Count;
|
||||
Assert.Equal(2, m98Count);
|
||||
|
||||
// Only one sub-program definition
|
||||
var subDefCount = System.Text.RegularExpressions.Regex.Matches(output, ":200").Count;
|
||||
Assert.Equal(1, subDefCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_WithPartSubprograms_DifferentRotationsGetSeparateSubprograms()
|
||||
{
|
||||
var nest = new Nest("TestNest");
|
||||
var drawing = new Drawing("Square", CreateSquareProgram());
|
||||
var plate = new Plate(48, 96);
|
||||
|
||||
var part1 = new Part(drawing, new Vector(5, 5));
|
||||
plate.Parts.Add(part1);
|
||||
|
||||
var part2 = new Part(drawing, new Vector(20, 5));
|
||||
part2.Rotate(System.Math.PI / 2); // 90 degrees
|
||||
plate.Parts.Add(part2);
|
||||
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PostedAccuracy = 4,
|
||||
UsePartSubprograms = true,
|
||||
PartSubprogramStart = 200
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
|
||||
// Should have two different sub-programs
|
||||
Assert.Contains(":200", output);
|
||||
Assert.Contains(":201", output);
|
||||
Assert.Contains("M98P200", output);
|
||||
Assert.Contains("M98P201", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_WithPartSubprograms_CutoffsAreInline()
|
||||
{
|
||||
var nest = new Nest("TestNest");
|
||||
var drawing = new Drawing("Square", CreateSquareProgram());
|
||||
var cutoffDrawing = new Drawing("CutOff", CreateSquareProgram()) { IsCutOff = true };
|
||||
|
||||
var plate = new Plate(48, 96);
|
||||
plate.Parts.Add(new Part(drawing, new Vector(5, 5)));
|
||||
plate.Parts.Add(new Part(cutoffDrawing, new Vector(0, 30)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PostedAccuracy = 4,
|
||||
UsePartSubprograms = true,
|
||||
PartSubprogramStart = 200
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
|
||||
// Regular part uses sub-program
|
||||
Assert.Contains("M98P200", output);
|
||||
Assert.Contains(":200", output);
|
||||
|
||||
// Cutoff should NOT have its own sub-program
|
||||
Assert.DoesNotContain(":201", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_WithPartSubprograms_ConfigRoundTrips()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
UsePartSubprograms = true,
|
||||
PartSubprogramStart = 300
|
||||
};
|
||||
|
||||
var opts = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
var json = JsonSerializer.Serialize(config, opts);
|
||||
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
|
||||
|
||||
Assert.True(deserialized.UsePartSubprograms);
|
||||
Assert.Equal(300, deserialized.PartSubprogramStart);
|
||||
}
|
||||
|
||||
private static Nest CreateTestNest()
|
||||
{
|
||||
var nest = new Nest("TestNest");
|
||||
var drawing = new Drawing("Square", CreateSquareProgram());
|
||||
nest.Drawings.Add(drawing);
|
||||
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(drawing, new Vector(10, 10)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
return nest;
|
||||
}
|
||||
|
||||
private static Program CreateSquareProgram()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(0, 0));
|
||||
pgm.Codes.Add(new LinearMove(2, 0));
|
||||
pgm.Codes.Add(new LinearMove(2, 2));
|
||||
pgm.Codes.Add(new LinearMove(0, 2));
|
||||
pgm.Codes.Add(new LinearMove(0, 0));
|
||||
return pgm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
public class CincinnatiPreambleWriterTests
|
||||
{
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsHeader()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
ConfigurationName = "CL940",
|
||||
PostedUnits = Units.Inches,
|
||||
DefaultLibraryFile = "MS135N2PANEL.lib"
|
||||
};
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("( NEST TestNest )", output);
|
||||
Assert.Contains("( CONFIGURATION - CL940 )", output);
|
||||
Assert.Contains("G20", output);
|
||||
Assert.Contains("M42", output);
|
||||
Assert.Contains("G89 P MS135N2PANEL.lib", output);
|
||||
Assert.Contains("M98 P100 (Variable Declaration)", output);
|
||||
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
|
||||
Assert.Contains("N1M98 P101 (SHEET 1)", output);
|
||||
Assert.Contains("N2M98 P102 (SHEET 2)", output);
|
||||
Assert.Contains("M30 (END OF MAIN)", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsG21ForMetric()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PostedUnits = Units.Millimeters };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1);
|
||||
|
||||
Assert.Contains("G21", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsG61_WhenExactStop()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { UseExactStopMode = true };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1);
|
||||
|
||||
Assert.Contains("G61", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_OmitsG61_WhenNotExactStop()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { UseExactStopMode = false };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1);
|
||||
|
||||
Assert.DoesNotContain("G61", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteVariableDeclaration_EmitsSubprogram()
|
||||
{
|
||||
var config = new CincinnatiPostConfig();
|
||||
var vars = new ProgramVariableManager();
|
||||
vars.GetOrCreate("LeadInFeedrate", 126, "[#148*0.5]");
|
||||
vars.GetOrCreate("CircleFeedrate", 128, ".8");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteVariableDeclaration(sw, vars);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains(":100", output);
|
||||
Assert.Contains("(Variable Declaration Start)", output);
|
||||
Assert.Contains("#126=", output);
|
||||
Assert.Contains("#128=", output);
|
||||
Assert.Contains("M99 (Variable Declaration End)", output);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
public class CincinnatiSheetWriterTests
|
||||
{
|
||||
[Fact]
|
||||
public void WriteSheet_EmitsSheetHeader()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
DefaultLibraryFile = "MS135N2PANEL.lib",
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains(":101", output);
|
||||
Assert.Contains("( Sheet 1 )", output);
|
||||
Assert.Contains("#110=", output);
|
||||
Assert.Contains("#111=", output);
|
||||
Assert.Contains("G92X#5021Y#5022", output);
|
||||
Assert.Contains("M99", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_EmitsReturnToOriginAndPalletExchange()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PalletExchange = PalletMode.EndOfSheet,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M42", output);
|
||||
Assert.Contains("G0X0Y0", output);
|
||||
Assert.Contains("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_SkipsEmptyPlate()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
|
||||
|
||||
Assert.Equal("", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_SplitsMultiContourParts()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
|
||||
var pgm = new Program();
|
||||
// First contour (hole)
|
||||
pgm.Codes.Add(new RapidMove(1, 1));
|
||||
pgm.Codes.Add(new LinearMove(2, 1));
|
||||
pgm.Codes.Add(new LinearMove(2, 2));
|
||||
pgm.Codes.Add(new LinearMove(1, 1));
|
||||
// Second contour (exterior)
|
||||
pgm.Codes.Add(new RapidMove(0, 0));
|
||||
pgm.Codes.Add(new LinearMove(5, 0));
|
||||
pgm.Codes.Add(new LinearMove(5, 5));
|
||||
pgm.Codes.Add(new LinearMove(0, 0));
|
||||
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("MultiContour", pgm)));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
|
||||
|
||||
var output = sb.ToString();
|
||||
// Should have two G84 pierce commands (one per contour)
|
||||
var g84Count = output.Split('\n').Count(l => l.Trim() == "G84");
|
||||
Assert.Equal(2, g84Count);
|
||||
}
|
||||
|
||||
private static Program CreateSimpleProgram()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(0, 0));
|
||||
pgm.Codes.Add(new LinearMove(1, 0));
|
||||
pgm.Codes.Add(new LinearMove(1, 1));
|
||||
pgm.Codes.Add(new LinearMove(0, 1));
|
||||
pgm.Codes.Add(new LinearMove(0, 0));
|
||||
return pgm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
public class CoordinateFormatterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(13.401, 4, "13.401")]
|
||||
[InlineData(13.0, 4, "13")]
|
||||
[InlineData(0.0, 4, "0")]
|
||||
[InlineData(57.4895, 4, "57.4895")]
|
||||
[InlineData(13.401, 3, "13.401")]
|
||||
[InlineData(13.4016, 3, "13.402")]
|
||||
public void FormatCoord_FormatsCorrectly(double value, int accuracy, string expected)
|
||||
{
|
||||
var formatter = new CoordinateFormatter(accuracy);
|
||||
Assert.Equal(expected, formatter.FormatCoord(value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-5.25, 4, "-5.25")]
|
||||
[InlineData(-0.001, 4, "-0.001")]
|
||||
public void FormatCoord_HandlesNegatives(double value, int accuracy, string expected)
|
||||
{
|
||||
var formatter = new CoordinateFormatter(accuracy);
|
||||
Assert.Equal(expected, formatter.FormatCoord(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Comment_FormatsWithSpaces()
|
||||
{
|
||||
Assert.Equal("( hello )", CoordinateFormatter.Comment("hello"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InlineComment_FormatsWithoutSpaces()
|
||||
{
|
||||
Assert.Equal("(hello)", CoordinateFormatter.InlineComment("hello"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using OpenNest.CNC;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
public class ProgramVariableManagerTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetOrCreate_ReturnsNewVariable()
|
||||
{
|
||||
var mgr = new ProgramVariableManager();
|
||||
var v = mgr.GetOrCreate("LeadInFeedrate", 126);
|
||||
Assert.Equal(126, v.Number);
|
||||
Assert.Equal("LeadInFeedrate", v.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrCreate_ReturnsSameVariable_WhenCalledTwice()
|
||||
{
|
||||
var mgr = new ProgramVariableManager();
|
||||
var v1 = mgr.GetOrCreate("LeadInFeedrate", 126);
|
||||
var v2 = mgr.GetOrCreate("LeadInFeedrate", 126);
|
||||
Assert.Same(v1, v2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrCreate_WithExpression_SetsExpression()
|
||||
{
|
||||
var mgr = new ProgramVariableManager();
|
||||
var v = mgr.GetOrCreate("LeadInFeedrate", 126, "[#148*0.5]");
|
||||
Assert.Equal("[#148*0.5]", v.Expression);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrCreate_WithLiteral_SetsExpression()
|
||||
{
|
||||
var mgr = new ProgramVariableManager();
|
||||
var v = mgr.GetOrCreate("CircleFeedrate", 128, ".8");
|
||||
Assert.Equal(".8", v.Expression);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reference_ReturnsHashNumber()
|
||||
{
|
||||
var v = new ProgramVariable(126, "LeadInFeedrate");
|
||||
Assert.Equal("#126", v.Reference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitDeclarations_ProducesCorrectLines()
|
||||
{
|
||||
var mgr = new ProgramVariableManager();
|
||||
mgr.GetOrCreate("LeadInFeedrate", 126, "[#148*0.5]");
|
||||
mgr.GetOrCreate("CircleFeedrate", 128, ".8");
|
||||
|
||||
var lines = mgr.EmitDeclarations();
|
||||
|
||||
Assert.Contains("#126=[#148*0.5] (LEAD IN FEEDRATE)", lines);
|
||||
Assert.Contains("#128=.8 (CIRCLE FEEDRATE)", lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitDeclarations_SkipsVariablesWithNoExpression()
|
||||
{
|
||||
var mgr = new ProgramVariableManager();
|
||||
mgr.GetOrCreate("ProcessFeedrate", 148);
|
||||
|
||||
var lines = mgr.EmitDeclarations();
|
||||
|
||||
Assert.Empty(lines);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
public class SpeedClassifierTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(20.0, 10.0, "FAST")]
|
||||
[InlineData(5.0, 10.0, "FAST")]
|
||||
[InlineData(4.9, 10.0, "MEDIUM")]
|
||||
[InlineData(0.5, 10.0, "SLOW")]
|
||||
public void Classify_ReturnsExpectedClass(double contourLength, double sheetDiagonal, string expected)
|
||||
{
|
||||
var classifier = new SpeedClassifier();
|
||||
Assert.Equal(expected, classifier.Classify(contourLength, sheetDiagonal));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.8702, 3.927, "CutDist=.8702/3.927")]
|
||||
[InlineData(18.9722, 3.927, "CutDist=18.9722/3.927")]
|
||||
[InlineData(0.0, 10.0, "CutDist=0/10")]
|
||||
public void FormatCutDist_IncludesLengthAndDiagonal(double contour, double diag, string expected)
|
||||
{
|
||||
var classifier = new SpeedClassifier();
|
||||
Assert.Equal(expected, classifier.FormatCutDist(contour, diag));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class CutOffGeometryTests
|
||||
{
|
||||
private static readonly CutOffSettings ZeroClearance = new() { PartClearance = 0.0 };
|
||||
|
||||
private static double TotalCutLength(Program program, CutOffAxis axis = CutOffAxis.Vertical)
|
||||
{
|
||||
var total = 0.0;
|
||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||
{
|
||||
if (program.Codes[i] is RapidMove rapid &&
|
||||
program.Codes[i + 1] is LinearMove linear)
|
||||
{
|
||||
total += axis == CutOffAxis.Vertical
|
||||
? System.Math.Abs(rapid.EndPoint.Y - linear.EndPoint.Y)
|
||||
: System.Math.Abs(rapid.EndPoint.X - linear.EndPoint.X);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static Program MakeSquare(double size)
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, size)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return pgm;
|
||||
}
|
||||
|
||||
private static Program MakeCircle(double radius)
|
||||
{
|
||||
// Rapid to (radius, 0) relative to center at (0, 0),
|
||||
// then full-circle arc back to same point.
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(radius, 0)));
|
||||
pgm.Codes.Add(new ArcMove(new Vector(radius, 0), new Vector(0, 0)));
|
||||
return pgm;
|
||||
}
|
||||
|
||||
private static Program MakeDiamond(double halfSize)
|
||||
{
|
||||
// Diamond: points at (half,0), (2*half,half), (half,2*half), (0,half)
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(halfSize, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(halfSize * 2, halfSize)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(halfSize, halfSize * 2)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, halfSize)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(halfSize, 0)));
|
||||
return pgm;
|
||||
}
|
||||
|
||||
private static Program MakeTriangle(double width, double height)
|
||||
{
|
||||
// Right triangle: (0,0) -> (width,0) -> (0,height) -> (0,0)
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(width, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, height)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return pgm;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Square_GeometryExclusionMatchesBoundingBox()
|
||||
{
|
||||
// For a square, geometry and BB should produce the same exclusion.
|
||||
var drawing = new Drawing("sq", MakeSquare(20));
|
||||
var plate = new Plate(100, 100);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
part.Location = new Vector(10, 10);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
// Vertical cut at X=20 (through the middle of the square).
|
||||
// BB exclusion Y = [10, 30]. Geometry should give the same.
|
||||
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, ZeroClearance);
|
||||
|
||||
var codes = cutoff.Drawing.Program.Codes;
|
||||
// Two segments: before and after the square → 4 codes
|
||||
Assert.Equal(4, codes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Circle_GeometryExclusionNarrowerThanBoundingBox()
|
||||
{
|
||||
// Circle radius=10, center at (10,10) after placement.
|
||||
// BB = (0,0,20,20). Vertical cut at X=2 clips the circle edge.
|
||||
// BB would exclude full Y=[0,20].
|
||||
// Geometry: at X=2, the chord is much narrower.
|
||||
var drawing = new Drawing("circ", MakeCircle(10));
|
||||
var plate = new Plate(100, 100);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
part.Location = new Vector(0, 0);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
|
||||
// Cut at X=2: inside the BB but near the edge of the circle.
|
||||
var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, ZeroClearance, cache);
|
||||
|
||||
// The circle chord at X=2 from center (10,0) is much shorter than 20.
|
||||
// With geometry, we get a tighter exclusion, so the segments should
|
||||
// cover more of the plate than with BB.
|
||||
// Total cut length should be greater than 80 (BB would give 100-20=80)
|
||||
var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
|
||||
Assert.True(totalCutLength > 80, $"Geometry should give more cut length than BB. Got {totalCutLength:F2}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diamond_GeometryExclusionNarrowerThanBoundingBox()
|
||||
{
|
||||
// Diamond half=10 → points at (10,0), (20,10), (10,20), (0,10).
|
||||
// BB = (0,0,20,20).
|
||||
// Vertical cut at X=5: BB excludes Y=[0,20].
|
||||
// Diamond edge at X=5: intersects at Y=5 and Y=15 → exclusion [5,15].
|
||||
var drawing = new Drawing("dia", MakeDiamond(10));
|
||||
var plate = new Plate(100, 100);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
part.Location = new Vector(0, 0);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, ZeroClearance, cache);
|
||||
|
||||
// BB would exclude full 20 → cut length = 80.
|
||||
// Geometry excludes only 10 → cut length = 90.
|
||||
var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
|
||||
Assert.True(totalCutLength > 85, $"Diamond geometry should give more cut than BB. Got {totalCutLength:F2}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Triangle_AsymmetricExclusion()
|
||||
{
|
||||
// Right triangle: (0,0)→(30,0)→(0,30)→(0,0) placed at (10,10).
|
||||
// Vertical cut at X=20 (10 into the triangle from left).
|
||||
// The hypotenuse from (40,10) to (10,40): at X=20, Y = 30.
|
||||
// So geometry exclusion should be roughly [10, 30], not [10, 40] like BB.
|
||||
var drawing = new Drawing("tri", MakeTriangle(30, 30));
|
||||
var plate = new Plate(100, 100);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
part.Location = new Vector(10, 10);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, ZeroClearance, cache);
|
||||
|
||||
// BB would exclude [10,40] = 30 → cut = 70.
|
||||
// Geometry excludes [10,30] = 20 → cut = 80.
|
||||
var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
|
||||
Assert.True(totalCutLength > 75, $"Triangle geometry should give more cut than BB. Got {totalCutLength:F2}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutLineMissesPart_NoExclusion()
|
||||
{
|
||||
var drawing = new Drawing("sq", MakeSquare(10));
|
||||
var plate = new Plate(100, 100);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
part.Location = new Vector(50, 50);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
// Vertical cut at X=5: well outside the part at X=[50,60].
|
||||
var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, ZeroClearance);
|
||||
|
||||
// Single full-length segment → 2 codes
|
||||
Assert.Equal(2, cutoff.Drawing.Program.Codes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorizontalCut_Circle_UsesGeometry()
|
||||
{
|
||||
var drawing = new Drawing("circ", MakeCircle(10));
|
||||
var plate = new Plate(100, 100);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
part.Location = new Vector(0, 0);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
|
||||
// Horizontal cut at Y=2: near the edge of the circle.
|
||||
var cutoff = new CutOff(new Vector(0, 2), CutOffAxis.Horizontal);
|
||||
cutoff.Regenerate(plate, ZeroClearance, cache);
|
||||
|
||||
// BB would exclude X=[0,20] → cut = 80.
|
||||
// Circle chord at Y=2 is much shorter → cut > 80.
|
||||
var totalCutLength = TotalCutLength(cutoff.Drawing.Program, CutOffAxis.Horizontal);
|
||||
Assert.True(totalCutLength > 80, $"Circle horizontal cut should use geometry. Got {totalCutLength:F2}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clearance_ExpandsGeometryExclusion()
|
||||
{
|
||||
var drawing = new Drawing("sq", MakeSquare(20));
|
||||
var plate = new Plate(100, 100);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
part.Location = new Vector(10, 10);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var settings = new CutOffSettings { PartClearance = 5.0 };
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, settings, cache);
|
||||
|
||||
// Square at Y=[10,30]. With 5 clearance → exclusion [5,35].
|
||||
// Segments: [0,5] and [35,100] → 4 codes.
|
||||
Assert.Equal(4, cutoff.Drawing.Program.Codes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPerimeterCache_OpenContourGetsConvexHull()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(new Part(new Drawing("open", pgm)));
|
||||
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
Assert.Single(cache);
|
||||
|
||||
var perimeter = cache[plate.Parts[0]];
|
||||
Assert.NotNull(perimeter);
|
||||
Assert.IsType<Polygon>(perimeter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullCache_FallsBackToBoundingBox()
|
||||
{
|
||||
// Without a cache, should still work (using BB fallback).
|
||||
var drawing = new Drawing("sq", MakeSquare(20));
|
||||
var plate = new Plate(100, 100);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
part.Location = new Vector(10, 10);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, ZeroClearance, null);
|
||||
|
||||
Assert.True(cutoff.Drawing.Program.Codes.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleParts_IndependentExclusions()
|
||||
{
|
||||
var plate = new Plate(100, 100);
|
||||
|
||||
var sq1 = new Drawing("sq1", MakeSquare(10));
|
||||
var p1 = Part.CreateAtOrigin(sq1);
|
||||
p1.Location = new Vector(10, 10);
|
||||
plate.Parts.Add(p1);
|
||||
|
||||
var sq2 = new Drawing("sq2", MakeSquare(10));
|
||||
var p2 = Part.CreateAtOrigin(sq2);
|
||||
p2.Location = new Vector(10, 50);
|
||||
plate.Parts.Add(p2);
|
||||
|
||||
// Vertical cut at X=15 crosses both parts.
|
||||
var cutoff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, ZeroClearance);
|
||||
|
||||
// 3 segments: before p1, between p1 and p2, after p2 → 6 codes
|
||||
Assert.Equal(6, cutoff.Drawing.Program.Codes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollectPoints_LinesAndArcs_ReturnsAllPoints()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(new Vector(0, 0), new Vector(10, 0)),
|
||||
new Arc(new Vector(5, 5), 5, 0, System.Math.PI)
|
||||
};
|
||||
|
||||
var points = entities.CollectPoints();
|
||||
|
||||
// Line: 2 points. Arc: 2 endpoints + 4 cardinals = 6. Total = 8.
|
||||
Assert.Equal(8, points.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlatePerimeterCache_ReturnsOneEntryPerPart()
|
||||
{
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(new Part(new Drawing("a", MakeSquare(10))));
|
||||
plate.Parts.Add(new Part(new Drawing("b", MakeCircle(5))));
|
||||
plate.Parts.Add(new Part(new Drawing("c", MakeDiamond(8))));
|
||||
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
Assert.Equal(3, cache.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlatePerimeterCache_SkipsCutOffParts()
|
||||
{
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(new Part(new Drawing("real", MakeSquare(10))));
|
||||
plate.Parts.Add(new Part(new Drawing("cutoff", new Program()) { IsCutOff = true }));
|
||||
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
Assert.Single(cache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegenerateCutOffs_UsesGeometryExclusions()
|
||||
{
|
||||
// Circle radius=10 at origin. Vertical cut at X=2.
|
||||
// With geometry: tighter exclusion than BB.
|
||||
var drawing = new Drawing("circ", MakeCircle(10));
|
||||
var plate = new Plate(100, 100);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutoff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings { PartClearance = 0 });
|
||||
|
||||
// Find the materialized cut-off part
|
||||
var cutPart = plate.Parts.First(p => p.BaseDrawing.IsCutOff);
|
||||
// BB would give 80 (100 - 20). Geometry should give more.
|
||||
var totalCutLength = TotalCutLength(cutPart.BaseDrawing.Program);
|
||||
Assert.True(totalCutLength > 80, $"RegenerateCutOffs should use geometry. Got {totalCutLength:F2}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShapeProfile_SelectsLargestShapeAsPerimeter()
|
||||
{
|
||||
// Outer square: (5,0)→(25,0)→(25,20)→(5,20)→(5,0)
|
||||
// Inner cutout: (0,5)→(10,5)→(10,15)→(0,15)→(0,5)
|
||||
// The cutout has Left=0, perimeter has Left=5.
|
||||
// Old heuristic would pick the cutout as perimeter.
|
||||
var outer = new Shape();
|
||||
outer.Entities.Add(new Line(new Vector(5, 0), new Vector(25, 0)));
|
||||
outer.Entities.Add(new Line(new Vector(25, 0), new Vector(25, 20)));
|
||||
outer.Entities.Add(new Line(new Vector(25, 20), new Vector(5, 20)));
|
||||
outer.Entities.Add(new Line(new Vector(5, 20), new Vector(5, 0)));
|
||||
|
||||
var inner = new Shape();
|
||||
inner.Entities.Add(new Line(new Vector(0, 5), new Vector(10, 5)));
|
||||
inner.Entities.Add(new Line(new Vector(10, 5), new Vector(10, 15)));
|
||||
inner.Entities.Add(new Line(new Vector(10, 15), new Vector(0, 15)));
|
||||
inner.Entities.Add(new Line(new Vector(0, 15), new Vector(0, 5)));
|
||||
|
||||
// Combine all entities (simulating what ShapeBuilder.GetShapes would produce)
|
||||
var entities = new List<Entity>();
|
||||
entities.AddRange(inner.Entities); // inner first — worst case for old heuristic
|
||||
entities.AddRange(outer.Entities);
|
||||
|
||||
var profile = new ShapeProfile(entities);
|
||||
|
||||
// Perimeter should be the outer (larger) shape
|
||||
var bb = profile.Perimeter.BoundingBox;
|
||||
Assert.Equal(20.0, bb.Width, 1);
|
||||
Assert.Equal(20.0, bb.Length, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class CutOffSerializationTests
|
||||
{
|
||||
[Fact]
|
||||
public void RoundTrip_CutOffsPreserved()
|
||||
{
|
||||
var nest = new Nest();
|
||||
nest.Name = "test";
|
||||
nest.DateCreated = DateTime.Now;
|
||||
nest.DateLastModified = DateTime.Now;
|
||||
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
var drawing = new Drawing("part1", pgm);
|
||||
nest.Drawings.Add(drawing);
|
||||
|
||||
var plate = new Plate(100, 50);
|
||||
plate.Parts.Add(new Part(drawing));
|
||||
plate.CutOffs.Add(new CutOff(new Vector(62.0, 24.0), CutOffAxis.Vertical));
|
||||
plate.CutOffs.Add(new CutOff(new Vector(48.0, 30.0), CutOffAxis.Horizontal));
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
var writer = new NestWriter(nest);
|
||||
writer.Write(stream);
|
||||
|
||||
stream.Position = 0;
|
||||
var reader = new NestReader(stream);
|
||||
var loaded = reader.Read();
|
||||
|
||||
Assert.Single(loaded.Plates);
|
||||
var loadedPlate = loaded.Plates[0];
|
||||
|
||||
Assert.Equal(2, loadedPlate.CutOffs.Count);
|
||||
Assert.Equal(CutOffAxis.Vertical, loadedPlate.CutOffs[0].Axis);
|
||||
Assert.Equal(62.0, loadedPlate.CutOffs[0].Position.X, 5);
|
||||
Assert.Equal(24.0, loadedPlate.CutOffs[0].Position.Y, 5);
|
||||
Assert.Equal(CutOffAxis.Horizontal, loadedPlate.CutOffs[1].Axis);
|
||||
|
||||
Assert.Single(loadedPlate.Parts.Where(p => !p.BaseDrawing.IsCutOff));
|
||||
Assert.Single(loaded.Drawings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestWriter_SkipsCutOffPartsInPartsList()
|
||||
{
|
||||
var nest = new Nest();
|
||||
nest.Name = "test";
|
||||
nest.DateCreated = DateTime.Now;
|
||||
nest.DateLastModified = DateTime.Now;
|
||||
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
var drawing = new Drawing("part1", pgm);
|
||||
nest.Drawings.Add(drawing);
|
||||
|
||||
var plate = new Plate(100, 50);
|
||||
plate.Parts.Add(new Part(drawing));
|
||||
plate.CutOffs.Add(new CutOff(new Vector(50, 25), CutOffAxis.Vertical));
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
Assert.Equal(2, plate.Parts.Count);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
var writer = new NestWriter(nest);
|
||||
writer.Write(stream);
|
||||
|
||||
stream.Position = 0;
|
||||
var reader = new NestReader(stream);
|
||||
var loaded = reader.Read();
|
||||
|
||||
Assert.Single(loaded.Plates[0].Parts.Where(p => !p.BaseDrawing.IsCutOff));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_LimitsPreserved()
|
||||
{
|
||||
var nest = new Nest();
|
||||
nest.Name = "test";
|
||||
nest.DateCreated = DateTime.Now;
|
||||
nest.DateLastModified = DateTime.Now;
|
||||
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
var drawing = new Drawing("part1", pgm);
|
||||
nest.Drawings.Add(drawing);
|
||||
|
||||
var plate = new Plate(100, 50);
|
||||
plate.Parts.Add(new Part(drawing));
|
||||
plate.CutOffs.Add(new CutOff(new Vector(85, 30), CutOffAxis.Horizontal) { EndLimit = 85.0 });
|
||||
plate.CutOffs.Add(new CutOff(new Vector(85, 30), CutOffAxis.Vertical) { StartLimit = 30.0 });
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
var writer = new NestWriter(nest);
|
||||
writer.Write(stream);
|
||||
|
||||
stream.Position = 0;
|
||||
var reader = new NestReader(stream);
|
||||
var loaded = reader.Read();
|
||||
|
||||
var loadedPlate = loaded.Plates[0];
|
||||
Assert.Equal(85.0, loadedPlate.CutOffs[0].EndLimit);
|
||||
Assert.Null(loadedPlate.CutOffs[0].StartLimit);
|
||||
Assert.Equal(30.0, loadedPlate.CutOffs[1].StartLimit);
|
||||
Assert.Null(loadedPlate.CutOffs[1].EndLimit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class CutOffTests
|
||||
{
|
||||
[Fact]
|
||||
public void Drawing_IsCutOff_DefaultsFalse()
|
||||
{
|
||||
var drawing = new Drawing("test", new Program());
|
||||
Assert.False(drawing.IsCutOff);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plate_CutOffPart_DoesNotIncrementQuantity()
|
||||
{
|
||||
var drawing = new Drawing("cutoff", new Program()) { IsCutOff = true };
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(new Part(drawing));
|
||||
Assert.Equal(0, drawing.Quantity.Nested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plate_Utilization_ExcludesCutOffParts()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Geometry.Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Geometry.Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Geometry.Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Geometry.Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Geometry.Vector(0, 0)));
|
||||
var realDrawing = new Drawing("real", pgm);
|
||||
var cutoffDrawing = new Drawing("cutoff", new Program()) { IsCutOff = true };
|
||||
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(new Part(realDrawing));
|
||||
plate.Parts.Add(new Part(cutoffDrawing));
|
||||
|
||||
var utilization = plate.Utilization();
|
||||
var expected = realDrawing.Area / plate.Area();
|
||||
Assert.Equal(expected, utilization, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plate_HasOverlappingParts_SkipsCutOffParts()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Geometry.Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Geometry.Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Geometry.Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Geometry.Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Geometry.Vector(0, 0)));
|
||||
|
||||
var realDrawing = new Drawing("real", pgm);
|
||||
var cutoffDrawing = new Drawing("cutoff", pgm) { IsCutOff = true };
|
||||
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(new Part(realDrawing));
|
||||
plate.Parts.Add(new Part(cutoffDrawing));
|
||||
|
||||
var hasOverlap = plate.HasOverlappingParts(out var pts);
|
||||
Assert.False(hasOverlap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutOff_VerticalCut_GeneratesFullLineOnEmptyPlate()
|
||||
{
|
||||
var plate = new Plate(100, 50);
|
||||
var settings = new CutOffSettings();
|
||||
var cutoff = new CutOff(new Vector(25, 20), CutOffAxis.Vertical);
|
||||
|
||||
cutoff.Regenerate(plate, settings);
|
||||
|
||||
Assert.NotNull(cutoff.Drawing);
|
||||
Assert.True(cutoff.Drawing.IsCutOff);
|
||||
Assert.True(cutoff.Drawing.Program.Codes.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutOff_HorizontalCut_GeneratesFullLineOnEmptyPlate()
|
||||
{
|
||||
var plate = new Plate(100, 50);
|
||||
var settings = new CutOffSettings();
|
||||
var cutoff = new CutOff(new Vector(25, 20), CutOffAxis.Horizontal);
|
||||
|
||||
cutoff.Regenerate(plate, settings);
|
||||
|
||||
var codes = cutoff.Drawing.Program.Codes;
|
||||
Assert.Equal(2, codes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutOff_VerticalCut_TrimsAroundPart()
|
||||
{
|
||||
// Create a 10x10 part at the origin, then move it to (20,20)
|
||||
// so the bounding box is Box(20,20,10,10) and doesn't span the origin.
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
var drawing = new Drawing("sq", pgm);
|
||||
|
||||
var plate = new Plate(50, 50);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
part.Location = new Vector(20, 20);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
// Vertical cut at X=25 runs along Y from 0 to 50.
|
||||
// Part BB at (20,20,10,10) with clearance 1 → exclusion X=[19,31], Y=[19,31].
|
||||
// X=25 is within [19,31] so exclusion applies: skip Y=[19,31].
|
||||
// Segments: (0, 19) and (31, 50) → 2 segments → 4 codes.
|
||||
var settings = new CutOffSettings { PartClearance = 1.0 };
|
||||
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, settings);
|
||||
|
||||
var codes = cutoff.Drawing.Program.Codes;
|
||||
Assert.Equal(4, codes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutOff_ShortSegment_FilteredByMinLength()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(20, 0.02)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(30, 0.02)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(30, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(20, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(20, 0.02)));
|
||||
var drawing = new Drawing("sq", pgm);
|
||||
|
||||
var plate = new Plate(50, 50);
|
||||
plate.Parts.Add(new Part(drawing));
|
||||
|
||||
var settings = new CutOffSettings { PartClearance = 0.0, MinSegmentLength = 0.05 };
|
||||
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, settings);
|
||||
|
||||
var rapidCount = cutoff.Drawing.Program.Codes.Count(c => c is RapidMove);
|
||||
var lineCount = cutoff.Drawing.Program.Codes.Count(c => c is LinearMove);
|
||||
Assert.Equal(rapidCount, lineCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutOff_Overtravel_ExtendsFarEnd()
|
||||
{
|
||||
var plate = new Plate(100, 50);
|
||||
var settings = new CutOffSettings { Overtravel = 2.0 };
|
||||
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, settings);
|
||||
|
||||
// Plate(100, 50) = Width=100, Length=50. Vertical cut runs along Y (Width axis).
|
||||
// BoundingBox Y extent = Size.Width = 100. With 2" overtravel = 102.
|
||||
// Default TowardOrigin: RapidMove to far end (102), LinearMove to near end (0).
|
||||
var rapidMoves = cutoff.Drawing.Program.Codes.OfType<RapidMove>().ToList();
|
||||
Assert.Single(rapidMoves);
|
||||
Assert.Equal(102.0, rapidMoves[0].EndPoint.Y, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutOff_StartLimit_TruncatesNearEnd()
|
||||
{
|
||||
var plate = new Plate(100, 50);
|
||||
var settings = new CutOffSettings();
|
||||
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical)
|
||||
{
|
||||
StartLimit = 20.0
|
||||
};
|
||||
cutoff.Regenerate(plate, settings);
|
||||
|
||||
var rapidMoves = cutoff.Drawing.Program.Codes.OfType<RapidMove>().ToList();
|
||||
Assert.Single(rapidMoves);
|
||||
var linearMoves = cutoff.Drawing.Program.Codes.OfType<LinearMove>().ToList();
|
||||
Assert.Single(linearMoves);
|
||||
Assert.Equal(20.0, linearMoves[0].EndPoint.Y, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutOff_EndLimit_TruncatesFarEnd()
|
||||
{
|
||||
var plate = new Plate(100, 50);
|
||||
var settings = new CutOffSettings();
|
||||
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical)
|
||||
{
|
||||
EndLimit = 80.0
|
||||
};
|
||||
cutoff.Regenerate(plate, settings);
|
||||
|
||||
var rapidMoves = cutoff.Drawing.Program.Codes.OfType<RapidMove>().ToList();
|
||||
Assert.Single(rapidMoves);
|
||||
Assert.Equal(80.0, rapidMoves[0].EndPoint.Y, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutOff_BothLimits_LShapedCornerCut()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var settings = new CutOffSettings { PartClearance = 0 };
|
||||
|
||||
var hCut = new CutOff(new Vector(85, 30), CutOffAxis.Horizontal)
|
||||
{
|
||||
EndLimit = 85.0
|
||||
};
|
||||
hCut.Regenerate(plate, settings);
|
||||
|
||||
var vCut = new CutOff(new Vector(85, 30), CutOffAxis.Vertical)
|
||||
{
|
||||
StartLimit = 30.0
|
||||
};
|
||||
vCut.Regenerate(plate, settings);
|
||||
|
||||
Assert.True(hCut.Drawing.Program.Codes.Count > 0);
|
||||
Assert.True(vCut.Drawing.Program.Codes.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plate_RegenerateCutOffs_MaterializesParts()
|
||||
{
|
||||
var plate = new Plate(100, 50);
|
||||
var cutoff = new CutOff(new Geometry.Vector(25, 10), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutoff);
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
|
||||
Assert.Single(plate.Parts);
|
||||
Assert.True(plate.Parts[0].BaseDrawing.IsCutOff);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plate_RegenerateCutOffs_ReplacesOldParts()
|
||||
{
|
||||
var plate = new Plate(100, 50);
|
||||
var cutoff = new CutOff(new Geometry.Vector(25, 10), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutoff);
|
||||
|
||||
var settings = new CutOffSettings();
|
||||
plate.RegenerateCutOffs(settings);
|
||||
plate.RegenerateCutOffs(settings);
|
||||
|
||||
Assert.Single(plate.Parts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plate_RegenerateCutOffs_DoesNotAffectRegularParts()
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Geometry.Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Geometry.Vector(5, 5)));
|
||||
var drawing = new Drawing("real", pgm);
|
||||
|
||||
var plate = new Plate(100, 50);
|
||||
plate.Parts.Add(new Part(drawing));
|
||||
|
||||
var cutoff = new CutOff(new Geometry.Vector(25, 10), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutoff);
|
||||
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
|
||||
Assert.Equal(2, plate.Parts.Count);
|
||||
Assert.False(plate.Parts[0].BaseDrawing.IsCutOff);
|
||||
Assert.True(plate.Parts[1].BaseDrawing.IsCutOff);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -11,7 +11,7 @@ public class EdgeStartSequencerTests
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var edgePart = MakePartAt(1, 1);
|
||||
var centerPart = MakePartAt(25, 55);
|
||||
var centerPart = MakePartAt(25, 25);
|
||||
var midPart = MakePartAt(10, 10);
|
||||
plate.Parts.Add(edgePart);
|
||||
plate.Parts.Add(centerPart);
|
||||
|
||||
@@ -30,7 +30,7 @@ public class ShrinkFillerTests
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Height);
|
||||
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Length);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Parts.Count > 0);
|
||||
@@ -73,7 +73,7 @@ public class ShrinkFillerTests
|
||||
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
|
||||
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0,
|
||||
ShrinkAxis.Height, token: cts.Token);
|
||||
ShrinkAxis.Length, token: cts.Token);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Parts.Count > 0);
|
||||
@@ -97,7 +97,7 @@ public class ShrinkFillerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimToCount_Height_KeepsPartsNearestToOrigin()
|
||||
public void TrimToCount_Length_KeepsPartsNearestToOrigin()
|
||||
{
|
||||
var parts = new List<Part>
|
||||
{
|
||||
@@ -107,7 +107,7 @@ public class ShrinkFillerTests
|
||||
TestHelpers.MakePartAt(0, 30, 5), // Top = 35
|
||||
};
|
||||
|
||||
var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Height);
|
||||
var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Length);
|
||||
|
||||
Assert.Equal(2, trimmed.Count);
|
||||
Assert.True(trimmed.All(p => p.BoundingBox.Top <= 15));
|
||||
|
||||
@@ -7,11 +7,12 @@ internal static class TestHelpers
|
||||
{
|
||||
public static Part MakePartAt(double x, double y, double size = 1)
|
||||
{
|
||||
// CW winding matches CNC convention (OffsetSide.Left = outward)
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, size)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
var drawing = new Drawing("test", pgm);
|
||||
return new Part(drawing, new Vector(x, y));
|
||||
@@ -27,24 +28,26 @@ internal static class TestHelpers
|
||||
|
||||
public static Drawing MakeSquareDrawing(double size = 10)
|
||||
{
|
||||
// CW winding matches CNC convention (OffsetSide.Left = outward)
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, size)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("square", pgm);
|
||||
}
|
||||
|
||||
public static Drawing MakeLShapeDrawing()
|
||||
{
|
||||
// CW winding matches CNC convention (OffsetSide.Left = outward)
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 5)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(5, 5)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(5, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(5, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(5, 5)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 5)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("lshape", pgm);
|
||||
}
|
||||
|
||||
+27
-2
@@ -1,9 +1,12 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.0.0
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.4.11612.150
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest", "OpenNest\OpenNest.csproj", "{1F1E40E0-5C53-474F-A258-69C9C3FAC15A}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532} = {FB1B2EB2-9D80-4499-BA93-B4E2F295A532}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Core", "OpenNest.Core\OpenNest.Core.csproj", "{5A5FDE8D-F8DB-440E-866C-C4807E1686CF}"
|
||||
EndProject
|
||||
@@ -23,6 +26,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Tests", "OpenNest.
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Api", "OpenNest.Api\OpenNest.Api.csproj", "{44D2810A-16EF-46A4-859C-B897147D8D3C}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PostProcessors", "PostProcessors", "{4052CFAC-1F12-48BE-872D-F503C3B65D7E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Posts.Cincinnati", "OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj", "{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -153,8 +160,26 @@ Global
|
||||
{44D2810A-16EF-46A4-859C-B897147D8D3C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{44D2810A-16EF-46A4-859C-B897147D8D3C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{44D2810A-16EF-46A4-859C-B897147D8D3C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532} = {4052CFAC-1F12-48BE-872D-F503C3B65D7E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {86FE17B3-F764-40AE-BCAA-F26B470CA05C}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -167,6 +167,9 @@ namespace OpenNest.Actions
|
||||
}
|
||||
|
||||
parts.ForEach(p => plateView.Plate.Parts.Add(p.BasePart.Clone() as Part));
|
||||
|
||||
if (plateView.Plate.CutOffs.Count > 0)
|
||||
plateView.Plate.RegenerateCutOffs(plateView.CutOffSettings);
|
||||
}
|
||||
|
||||
private void Fill()
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.Actions
|
||||
{
|
||||
[DisplayName("Sheet Cut-Off")]
|
||||
public class ActionCutOff : Action
|
||||
{
|
||||
private CutOff previewCutOff;
|
||||
private CutOffSettings settings;
|
||||
private CutOffAxis lockedAxis = CutOffAxis.Vertical;
|
||||
private Dictionary<Part, Entity> perimeterCache;
|
||||
private readonly Timer debounceTimer;
|
||||
private bool regeneratePending;
|
||||
|
||||
public ActionCutOff(PlateView plateView)
|
||||
: base(plateView)
|
||||
{
|
||||
settings = plateView.CutOffSettings;
|
||||
debounceTimer = new Timer { Interval = 16 };
|
||||
debounceTimer.Tick += OnDebounce;
|
||||
ConnectEvents();
|
||||
}
|
||||
|
||||
public override void ConnectEvents()
|
||||
{
|
||||
perimeterCache = Plate.BuildPerimeterCache(plateView.Plate);
|
||||
|
||||
plateView.MouseMove += OnMouseMove;
|
||||
plateView.MouseDown += OnMouseDown;
|
||||
plateView.KeyDown += OnKeyDown;
|
||||
plateView.Paint += OnPaint;
|
||||
}
|
||||
|
||||
public override void DisconnectEvents()
|
||||
{
|
||||
debounceTimer.Stop();
|
||||
debounceTimer.Dispose();
|
||||
plateView.MouseMove -= OnMouseMove;
|
||||
plateView.MouseDown -= OnMouseDown;
|
||||
plateView.KeyDown -= OnKeyDown;
|
||||
plateView.Paint -= OnPaint;
|
||||
|
||||
previewCutOff = null;
|
||||
perimeterCache = null;
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
public override void CancelAction() { }
|
||||
|
||||
public override bool IsBusy() => false;
|
||||
|
||||
private void OnMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
regeneratePending = true;
|
||||
debounceTimer.Start();
|
||||
}
|
||||
|
||||
private void OnDebounce(object sender, System.EventArgs e)
|
||||
{
|
||||
debounceTimer.Stop();
|
||||
|
||||
if (!regeneratePending)
|
||||
return;
|
||||
|
||||
regeneratePending = false;
|
||||
var pt = plateView.CurrentPoint;
|
||||
previewCutOff = new CutOff(pt, lockedAxis);
|
||||
previewCutOff.Regenerate(plateView.Plate, settings, perimeterCache);
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
private void OnMouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button != MouseButtons.Left)
|
||||
return;
|
||||
|
||||
var pt = plateView.CurrentPoint;
|
||||
var cutoff = new CutOff(pt, lockedAxis);
|
||||
|
||||
plateView.Plate.CutOffs.Add(cutoff);
|
||||
plateView.Plate.RegenerateCutOffs(settings);
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
private void OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Space)
|
||||
{
|
||||
lockedAxis = lockedAxis == CutOffAxis.Vertical
|
||||
? CutOffAxis.Horizontal
|
||||
: CutOffAxis.Vertical;
|
||||
|
||||
if (previewCutOff != null)
|
||||
{
|
||||
previewCutOff = new CutOff(plateView.CurrentPoint, lockedAxis);
|
||||
previewCutOff.Regenerate(plateView.Plate, settings, perimeterCache);
|
||||
plateView.Invalidate();
|
||||
}
|
||||
}
|
||||
else if (e.KeyCode == Keys.Escape)
|
||||
{
|
||||
plateView.SetAction(typeof(ActionSelect));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPaint(object sender, PaintEventArgs e)
|
||||
{
|
||||
if (previewCutOff?.Drawing?.Program == null)
|
||||
return;
|
||||
|
||||
var program = previewCutOff.Drawing.Program;
|
||||
if (program.Codes.Count == 0)
|
||||
return;
|
||||
|
||||
using var pen = new Pen(Color.FromArgb(128, 64, 64, 64), 1.5f / plateView.ViewScale)
|
||||
{
|
||||
DashStyle = DashStyle.Dash
|
||||
};
|
||||
|
||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||
{
|
||||
if (program.Codes[i] is RapidMove rapid &&
|
||||
program.Codes[i + 1] is LinearMove linear)
|
||||
{
|
||||
var pt1 = plateView.PointWorldToGraph(rapid.EndPoint);
|
||||
var pt2 = plateView.PointWorldToGraph(linear.EndPoint);
|
||||
e.Graphics.DrawLine(pen, pt1, pt2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,6 +133,12 @@ namespace OpenNest.Actions
|
||||
plateView.Invalidate();
|
||||
status = Status.SetFirstPoint;
|
||||
}
|
||||
else if (plateView.SelectedParts.Count > 0)
|
||||
{
|
||||
// Part drag completed — regenerate cut-off programs
|
||||
if (plateView.Plate.CutOffs.Count > 0)
|
||||
plateView.Plate.RegenerateCutOffs(plateView.CutOffSettings);
|
||||
}
|
||||
}
|
||||
|
||||
private void plateView_Paint(object sender, PaintEventArgs e)
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace OpenNest.Controls
|
||||
|
||||
public bool HideDepletedParts { get; set; }
|
||||
|
||||
public bool HideQuantity { get; set; }
|
||||
|
||||
protected override void OnDrawItem(DrawItemEventArgs e)
|
||||
{
|
||||
if (e.Index >= Items.Count || e.Index <= -1)
|
||||
@@ -38,8 +40,15 @@ namespace OpenNest.Controls
|
||||
if (dwg == null)
|
||||
return;
|
||||
|
||||
var isComplete = dwg.Quantity.Nested > 0 && dwg.Quantity.Remaining == 0;
|
||||
var bgBrush = isComplete ? SystemBrushes.Info : Brushes.White;
|
||||
var isSelected = (e.State & DrawItemState.Selected) != 0;
|
||||
Brush bgBrush;
|
||||
|
||||
if (isSelected)
|
||||
bgBrush = SystemBrushes.Highlight;
|
||||
else if (!HideQuantity && dwg.Quantity.Nested > 0 && dwg.Quantity.Remaining == 0)
|
||||
bgBrush = SystemBrushes.Info;
|
||||
else
|
||||
bgBrush = Brushes.White;
|
||||
|
||||
e.Graphics.FillRectangle(bgBrush, e.Bounds);
|
||||
|
||||
@@ -57,19 +66,32 @@ namespace OpenNest.Controls
|
||||
|
||||
pt.X += imageSize.Width + 10;
|
||||
|
||||
e.Graphics.DrawString(dwg.Name, nameFont, Brushes.Black, pt);
|
||||
var textBrush = isSelected ? SystemBrushes.HighlightText : Brushes.Black;
|
||||
var detailBrush = isSelected ? SystemBrushes.HighlightText : Brushes.Gray;
|
||||
|
||||
e.Graphics.DrawString(dwg.Name, nameFont, textBrush, pt);
|
||||
|
||||
var bounds = dwg.Program.BoundingBox();
|
||||
var text1 = string.Format("{0} of {1} nested", dwg.Quantity.Nested, dwg.Quantity.Required);
|
||||
var text2 = bounds.Size.ToString(4);
|
||||
var text3 = string.Format("{0} sq/{1}", System.Math.Round(dwg.Area, 4), UnitsHelper.GetShortString(Units));
|
||||
|
||||
pt.Y += 22;
|
||||
e.Graphics.DrawString(text1, Font, Brushes.Gray, pt);
|
||||
pt.Y += 18;
|
||||
e.Graphics.DrawString(text2, Font, Brushes.Gray, pt);
|
||||
pt.Y += 18;
|
||||
e.Graphics.DrawString(text3, Font, Brushes.Gray, pt);
|
||||
if (HideQuantity)
|
||||
{
|
||||
pt.Y += 22;
|
||||
e.Graphics.DrawString(text2, Font, detailBrush, pt);
|
||||
pt.Y += 18;
|
||||
e.Graphics.DrawString(text3, Font, detailBrush, pt);
|
||||
}
|
||||
else
|
||||
{
|
||||
var text1 = string.Format("{0} of {1} nested", dwg.Quantity.Nested, dwg.Quantity.Required);
|
||||
pt.Y += 22;
|
||||
e.Graphics.DrawString(text1, Font, detailBrush, pt);
|
||||
pt.Y += 18;
|
||||
e.Graphics.DrawString(text2, Font, detailBrush, pt);
|
||||
pt.Y += 18;
|
||||
e.Graphics.DrawString(text3, Font, detailBrush, pt);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseMove(MouseEventArgs e)
|
||||
|
||||
@@ -30,6 +30,10 @@ namespace OpenNest.Controls
|
||||
private Plate plate;
|
||||
private Action currentAction;
|
||||
private Action previousAction;
|
||||
private CutOffSettings cutOffSettings = new CutOffSettings();
|
||||
private CutOff selectedCutOff;
|
||||
private bool draggingCutOff;
|
||||
private Dictionary<Part, Geometry.Entity> dragPerimeterCache;
|
||||
protected List<LayoutPart> parts;
|
||||
private List<LayoutPart> stationaryParts = new List<LayoutPart>();
|
||||
private List<LayoutPart> activeParts = new List<LayoutPart>();
|
||||
@@ -134,6 +138,27 @@ namespace OpenNest.Controls
|
||||
|
||||
public bool FillParts { get; set; }
|
||||
|
||||
public CutOffSettings CutOffSettings
|
||||
{
|
||||
get => cutOffSettings;
|
||||
set
|
||||
{
|
||||
cutOffSettings = value;
|
||||
Plate?.RegenerateCutOffs(value);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public CutOff SelectedCutOff
|
||||
{
|
||||
get => selectedCutOff;
|
||||
set
|
||||
{
|
||||
selectedCutOff = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public double RotateIncrementAngle { get; set; }
|
||||
|
||||
public double OffsetIncrementDistance { get; set; }
|
||||
@@ -211,6 +236,22 @@ namespace OpenNest.Controls
|
||||
if (e.Button == MouseButtons.Middle)
|
||||
middleMouseDownPoint = e.Location;
|
||||
|
||||
if (e.Button == MouseButtons.Left && currentAction is ActionSelect)
|
||||
{
|
||||
var hitCutOff = GetCutOffAtPoint(CurrentPoint, 5.0 / ViewScale);
|
||||
if (hitCutOff != null)
|
||||
{
|
||||
SelectedCutOff = hitCutOff;
|
||||
draggingCutOff = true;
|
||||
dragPerimeterCache = Plate.BuildPerimeterCache(Plate);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedCutOff = null;
|
||||
}
|
||||
}
|
||||
|
||||
base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
@@ -228,6 +269,15 @@ namespace OpenNest.Controls
|
||||
}
|
||||
}
|
||||
|
||||
if (draggingCutOff && selectedCutOff != null)
|
||||
{
|
||||
draggingCutOff = false;
|
||||
dragPerimeterCache = null;
|
||||
Plate.RegenerateCutOffs(cutOffSettings);
|
||||
Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
@@ -284,6 +334,18 @@ namespace OpenNest.Controls
|
||||
|
||||
lastPoint = e.Location;
|
||||
|
||||
if (draggingCutOff && selectedCutOff != null)
|
||||
{
|
||||
if (selectedCutOff.Axis == CutOffAxis.Vertical)
|
||||
selectedCutOff.Position = new Vector(CurrentPoint.X, selectedCutOff.Position.Y);
|
||||
else
|
||||
selectedCutOff.Position = new Vector(selectedCutOff.Position.X, CurrentPoint.Y);
|
||||
|
||||
selectedCutOff.Regenerate(Plate, cutOffSettings, dragPerimeterCache);
|
||||
Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
@@ -300,7 +362,17 @@ namespace OpenNest.Controls
|
||||
switch (e.KeyCode)
|
||||
{
|
||||
case Keys.Delete:
|
||||
RemoveSelectedParts();
|
||||
if (selectedCutOff != null)
|
||||
{
|
||||
Plate.CutOffs.Remove(selectedCutOff);
|
||||
selectedCutOff = null;
|
||||
Plate.RegenerateCutOffs(cutOffSettings);
|
||||
Invalidate();
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveSelectedParts();
|
||||
}
|
||||
break;
|
||||
|
||||
case Keys.F:
|
||||
@@ -390,6 +462,7 @@ namespace OpenNest.Controls
|
||||
|
||||
DrawPlate(e.Graphics);
|
||||
DrawParts(e.Graphics);
|
||||
DrawCutOffs(e.Graphics);
|
||||
DrawActiveWorkArea(e.Graphics);
|
||||
DrawDebugRemnants(e.Graphics);
|
||||
|
||||
@@ -541,6 +614,59 @@ namespace OpenNest.Controls
|
||||
DrawRapids(g);
|
||||
}
|
||||
|
||||
private void DrawCutOffs(Graphics g)
|
||||
{
|
||||
if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0)
|
||||
return;
|
||||
|
||||
using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f);
|
||||
using var selectedPen = new Pen(Color.FromArgb(0, 120, 255), 3.5f);
|
||||
|
||||
foreach (var cutoff in Plate.CutOffs)
|
||||
{
|
||||
var program = cutoff.Drawing?.Program;
|
||||
if (program == null || program.Codes.Count == 0)
|
||||
continue;
|
||||
|
||||
var activePen = cutoff == selectedCutOff ? selectedPen : pen;
|
||||
|
||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||
{
|
||||
if (program.Codes[i] is RapidMove rapid &&
|
||||
program.Codes[i + 1] is LinearMove linear)
|
||||
{
|
||||
DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
|
||||
{
|
||||
if (Plate?.CutOffs == null)
|
||||
return null;
|
||||
|
||||
foreach (var cutoff in Plate.CutOffs)
|
||||
{
|
||||
var program = cutoff.Drawing?.Program;
|
||||
if (program == null)
|
||||
continue;
|
||||
|
||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||
{
|
||||
if (program.Codes[i] is RapidMove rapid &&
|
||||
program.Codes[i + 1] is LinearMove linear)
|
||||
{
|
||||
var line = new Geometry.Line(rapid.EndPoint, linear.EndPoint);
|
||||
if (line.ClosestPointTo(point).DistanceTo(point) <= tolerance)
|
||||
return cutoff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void DrawOffsetGeometry(Graphics g)
|
||||
{
|
||||
using (var offsetPen = new Pen(Color.FromArgb(120, 255, 100, 100)))
|
||||
@@ -966,6 +1092,10 @@ namespace OpenNest.Controls
|
||||
if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted))
|
||||
{
|
||||
AcceptPreviewParts(parts);
|
||||
|
||||
if (Plate.CutOffs.Count > 0)
|
||||
Plate.RegenerateCutOffs(cutOffSettings);
|
||||
|
||||
sw.Stop();
|
||||
Status = $"Fill: {parts.Count} parts in {sw.ElapsedMilliseconds} ms";
|
||||
}
|
||||
@@ -1109,6 +1239,9 @@ namespace OpenNest.Controls
|
||||
|
||||
for (var i = 0; i < SelectedParts.Count; ++i)
|
||||
SelectedParts[i].Offset(diff);
|
||||
|
||||
if (Plate.CutOffs.Count > 0)
|
||||
Plate.RegenerateCutOffs(cutOffSettings);
|
||||
}
|
||||
|
||||
protected override void UpdateMatrix()
|
||||
|
||||
+141
-118
@@ -13,134 +13,157 @@ namespace OpenNest.Forms
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.gridPanel = new System.Windows.Forms.TableLayoutPanel();
|
||||
this.toolbarPanel = new System.Windows.Forms.Panel();
|
||||
this.lblDrawing = new System.Windows.Forms.Label();
|
||||
this.cboDrawing = new System.Windows.Forms.ComboBox();
|
||||
this.navPanel = new System.Windows.Forms.Panel();
|
||||
this.btnPrev = new System.Windows.Forms.Button();
|
||||
this.btnNext = new System.Windows.Forms.Button();
|
||||
this.txtPage = new System.Windows.Forms.TextBox();
|
||||
this.lblPageCount = new System.Windows.Forms.Label();
|
||||
this.toolbarPanel.SuspendLayout();
|
||||
this.navPanel.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
splitContainer = new System.Windows.Forms.SplitContainer();
|
||||
drawingListBox = new OpenNest.Controls.DrawingListBox();
|
||||
gridPanel = new System.Windows.Forms.TableLayoutPanel();
|
||||
navPanel = new System.Windows.Forms.Panel();
|
||||
btnPrev = new System.Windows.Forms.Button();
|
||||
txtPage = new System.Windows.Forms.TextBox();
|
||||
lblPageCount = new System.Windows.Forms.Label();
|
||||
btnNext = new System.Windows.Forms.Button();
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
|
||||
splitContainer.Panel1.SuspendLayout();
|
||||
splitContainer.Panel2.SuspendLayout();
|
||||
splitContainer.SuspendLayout();
|
||||
navPanel.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// splitContainer
|
||||
//
|
||||
splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1;
|
||||
splitContainer.Location = new System.Drawing.Point(0, 0);
|
||||
splitContainer.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
splitContainer.Name = "splitContainer";
|
||||
//
|
||||
// splitContainer.Panel1
|
||||
//
|
||||
splitContainer.Panel1.Controls.Add(drawingListBox);
|
||||
splitContainer.Panel1MinSize = 180;
|
||||
//
|
||||
// splitContainer.Panel2
|
||||
//
|
||||
splitContainer.Panel2.Controls.Add(gridPanel);
|
||||
splitContainer.Panel2.Controls.Add(navPanel);
|
||||
splitContainer.Size = new System.Drawing.Size(792, 486);
|
||||
splitContainer.SplitterDistance = 280;
|
||||
splitContainer.SplitterWidth = 6;
|
||||
splitContainer.TabIndex = 0;
|
||||
//
|
||||
// drawingListBox
|
||||
//
|
||||
drawingListBox.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||
drawingListBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
drawingListBox.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable;
|
||||
drawingListBox.FormattingEnabled = true;
|
||||
drawingListBox.HideDepletedParts = false;
|
||||
drawingListBox.ItemHeight = 85;
|
||||
drawingListBox.Location = new System.Drawing.Point(0, 0);
|
||||
drawingListBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
drawingListBox.Name = "drawingListBox";
|
||||
drawingListBox.Size = new System.Drawing.Size(280, 486);
|
||||
drawingListBox.TabIndex = 0;
|
||||
drawingListBox.Units = Units.Inches;
|
||||
//
|
||||
// gridPanel
|
||||
//
|
||||
this.gridPanel.ColumnCount = 5;
|
||||
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.gridPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.gridPanel.Location = new System.Drawing.Point(0, 32);
|
||||
this.gridPanel.Name = "gridPanel";
|
||||
this.gridPanel.RowCount = 3;
|
||||
this.gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33F));
|
||||
this.gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.34F));
|
||||
this.gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33F));
|
||||
this.gridPanel.Size = new System.Drawing.Size(1200, 732);
|
||||
this.gridPanel.TabIndex = 0;
|
||||
//
|
||||
// toolbarPanel
|
||||
//
|
||||
this.toolbarPanel.Controls.Add(this.lblDrawing);
|
||||
this.toolbarPanel.Controls.Add(this.cboDrawing);
|
||||
this.toolbarPanel.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.toolbarPanel.Location = new System.Drawing.Point(0, 0);
|
||||
this.toolbarPanel.Name = "toolbarPanel";
|
||||
this.toolbarPanel.Size = new System.Drawing.Size(1200, 32);
|
||||
this.toolbarPanel.TabIndex = 2;
|
||||
//
|
||||
// lblDrawing
|
||||
//
|
||||
this.lblDrawing.Location = new System.Drawing.Point(6, 0);
|
||||
this.lblDrawing.Name = "lblDrawing";
|
||||
this.lblDrawing.Size = new System.Drawing.Size(55, 32);
|
||||
this.lblDrawing.TabIndex = 0;
|
||||
this.lblDrawing.Text = "Drawing:";
|
||||
this.lblDrawing.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// cboDrawing
|
||||
//
|
||||
this.cboDrawing.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.cboDrawing.Location = new System.Drawing.Point(64, 5);
|
||||
this.cboDrawing.Name = "cboDrawing";
|
||||
this.cboDrawing.Size = new System.Drawing.Size(250, 21);
|
||||
this.cboDrawing.TabIndex = 1;
|
||||
//
|
||||
//
|
||||
gridPanel.ColumnCount = 5;
|
||||
gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
gridPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
gridPanel.Location = new System.Drawing.Point(0, 0);
|
||||
gridPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
gridPanel.Name = "gridPanel";
|
||||
gridPanel.RowCount = 3;
|
||||
gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33F));
|
||||
gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.34F));
|
||||
gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33F));
|
||||
gridPanel.Size = new System.Drawing.Size(506, 444);
|
||||
gridPanel.TabIndex = 0;
|
||||
//
|
||||
// navPanel
|
||||
//
|
||||
this.navPanel.Controls.Add(this.btnPrev);
|
||||
this.navPanel.Controls.Add(this.txtPage);
|
||||
this.navPanel.Controls.Add(this.lblPageCount);
|
||||
this.navPanel.Controls.Add(this.btnNext);
|
||||
this.navPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
this.navPanel.Location = new System.Drawing.Point(0, 764);
|
||||
this.navPanel.Name = "navPanel";
|
||||
this.navPanel.Size = new System.Drawing.Size(1200, 36);
|
||||
this.navPanel.TabIndex = 1;
|
||||
//
|
||||
//
|
||||
navPanel.Controls.Add(btnPrev);
|
||||
navPanel.Controls.Add(txtPage);
|
||||
navPanel.Controls.Add(lblPageCount);
|
||||
navPanel.Controls.Add(btnNext);
|
||||
navPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
navPanel.Location = new System.Drawing.Point(0, 444);
|
||||
navPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
navPanel.Name = "navPanel";
|
||||
navPanel.Size = new System.Drawing.Size(506, 42);
|
||||
navPanel.TabIndex = 1;
|
||||
//
|
||||
// btnPrev
|
||||
//
|
||||
this.btnPrev.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.btnPrev.Name = "btnPrev";
|
||||
this.btnPrev.Size = new System.Drawing.Size(80, 28);
|
||||
this.btnPrev.TabIndex = 0;
|
||||
this.btnPrev.Text = "< Prev";
|
||||
this.btnPrev.Click += new System.EventHandler(this.btnPrev_Click);
|
||||
//
|
||||
//
|
||||
btnPrev.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
btnPrev.Location = new System.Drawing.Point(0, 0);
|
||||
btnPrev.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
btnPrev.Name = "btnPrev";
|
||||
btnPrev.Size = new System.Drawing.Size(93, 32);
|
||||
btnPrev.TabIndex = 0;
|
||||
btnPrev.Text = "< Prev";
|
||||
btnPrev.Click += btnPrev_Click;
|
||||
//
|
||||
// txtPage
|
||||
//
|
||||
this.txtPage.Name = "txtPage";
|
||||
this.txtPage.Size = new System.Drawing.Size(40, 20);
|
||||
this.txtPage.TabIndex = 1;
|
||||
this.txtPage.Text = "1";
|
||||
this.txtPage.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||
this.txtPage.KeyDown += new System.Windows.Forms.KeyEventHandler(this.txtPage_KeyDown);
|
||||
//
|
||||
//
|
||||
txtPage.Location = new System.Drawing.Point(0, 0);
|
||||
txtPage.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
txtPage.Name = "txtPage";
|
||||
txtPage.Size = new System.Drawing.Size(46, 23);
|
||||
txtPage.TabIndex = 1;
|
||||
txtPage.Text = "1";
|
||||
txtPage.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||
txtPage.KeyDown += txtPage_KeyDown;
|
||||
//
|
||||
// lblPageCount
|
||||
//
|
||||
this.lblPageCount.Name = "lblPageCount";
|
||||
this.lblPageCount.Size = new System.Drawing.Size(50, 28);
|
||||
this.lblPageCount.TabIndex = 2;
|
||||
this.lblPageCount.Text = "/ 1";
|
||||
this.lblPageCount.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
//
|
||||
//
|
||||
lblPageCount.Location = new System.Drawing.Point(0, 0);
|
||||
lblPageCount.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
lblPageCount.Name = "lblPageCount";
|
||||
lblPageCount.Size = new System.Drawing.Size(58, 32);
|
||||
lblPageCount.TabIndex = 2;
|
||||
lblPageCount.Text = "/ 1";
|
||||
lblPageCount.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// btnNext
|
||||
//
|
||||
this.btnNext.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.btnNext.Name = "btnNext";
|
||||
this.btnNext.Size = new System.Drawing.Size(80, 28);
|
||||
this.btnNext.TabIndex = 3;
|
||||
this.btnNext.Text = "Next >";
|
||||
this.btnNext.Click += new System.EventHandler(this.btnNext_Click);
|
||||
//
|
||||
//
|
||||
btnNext.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
btnNext.Location = new System.Drawing.Point(0, 0);
|
||||
btnNext.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
btnNext.Name = "btnNext";
|
||||
btnNext.Size = new System.Drawing.Size(93, 32);
|
||||
btnNext.TabIndex = 3;
|
||||
btnNext.Text = "Next >";
|
||||
btnNext.Click += btnNext_Click;
|
||||
//
|
||||
// BestFitViewerForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(1200, 800);
|
||||
this.Controls.Add(this.gridPanel);
|
||||
this.Controls.Add(this.toolbarPanel);
|
||||
this.Controls.Add(this.navPanel);
|
||||
this.KeyPreview = true;
|
||||
this.Name = "BestFitViewerForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Best-Fit Viewer";
|
||||
this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
|
||||
this.toolbarPanel.ResumeLayout(false);
|
||||
this.navPanel.ResumeLayout(false);
|
||||
this.navPanel.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
ClientSize = new System.Drawing.Size(792, 486);
|
||||
Controls.Add(splitContainer);
|
||||
KeyPreview = true;
|
||||
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
Name = "BestFitViewerForm";
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Best-Fit Viewer";
|
||||
WindowState = System.Windows.Forms.FormWindowState.Maximized;
|
||||
splitContainer.Panel1.ResumeLayout(false);
|
||||
splitContainer.Panel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer).EndInit();
|
||||
splitContainer.ResumeLayout(false);
|
||||
navPanel.ResumeLayout(false);
|
||||
navPanel.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
private System.Windows.Forms.SplitContainer splitContainer;
|
||||
private Controls.DrawingListBox drawingListBox;
|
||||
private System.Windows.Forms.TableLayoutPanel gridPanel;
|
||||
private System.Windows.Forms.Panel toolbarPanel;
|
||||
private System.Windows.Forms.Label lblDrawing;
|
||||
private System.Windows.Forms.ComboBox cboDrawing;
|
||||
private System.Windows.Forms.Panel navPanel;
|
||||
private System.Windows.Forms.Button btnPrev;
|
||||
private System.Windows.Forms.Button btnNext;
|
||||
|
||||
@@ -41,11 +41,12 @@ namespace OpenNest.Forms
|
||||
private int currentPage;
|
||||
private int pageCount;
|
||||
private CancellationTokenSource computeCts;
|
||||
private Label lblLoading;
|
||||
|
||||
public BestFitResult SelectedResult { get; private set; }
|
||||
public Drawing SelectedDrawing => activeDrawing;
|
||||
|
||||
public BestFitViewerForm(DrawingCollection drawings, Plate plate)
|
||||
public BestFitViewerForm(DrawingCollection drawings, Plate plate, Units units = Units.Inches)
|
||||
{
|
||||
this.drawings = drawings.ToList();
|
||||
this.plate = plate;
|
||||
@@ -53,10 +54,12 @@ namespace OpenNest.Forms
|
||||
DoubleBuffered = true;
|
||||
InitializeComponent();
|
||||
|
||||
drawingListBox.Units = units;
|
||||
drawingListBox.HideQuantity = true;
|
||||
foreach (var d in drawings)
|
||||
cboDrawing.Items.Add(d.Name);
|
||||
cboDrawing.SelectedIndex = 0;
|
||||
cboDrawing.SelectedIndexChanged += cboDrawing_SelectedIndexChanged;
|
||||
drawingListBox.Items.Add(d);
|
||||
drawingListBox.SelectedIndex = 0;
|
||||
drawingListBox.SelectedIndexChanged += drawingListBox_SelectedIndexChanged;
|
||||
|
||||
navPanel.SizeChanged += (s, ev) => CenterNavControls();
|
||||
Shown += BestFitViewerForm_Shown;
|
||||
@@ -93,13 +96,13 @@ namespace OpenNest.Forms
|
||||
return base.ProcessCmdKey(ref msg, keyData);
|
||||
}
|
||||
|
||||
private void cboDrawing_SelectedIndexChanged(object sender, EventArgs e)
|
||||
private void drawingListBox_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
var index = cboDrawing.SelectedIndex;
|
||||
if (index < 0 || index >= drawings.Count)
|
||||
var drawing = drawingListBox.SelectedItem as Drawing;
|
||||
if (drawing == null)
|
||||
return;
|
||||
|
||||
activeDrawing = drawings[index];
|
||||
activeDrawing = drawing;
|
||||
LoadResultsAsync();
|
||||
}
|
||||
|
||||
@@ -145,7 +148,6 @@ namespace OpenNest.Forms
|
||||
private void SetLoading(bool loading)
|
||||
{
|
||||
Cursor = loading ? Cursors.WaitCursor : Cursors.Default;
|
||||
cboDrawing.Enabled = !loading;
|
||||
btnPrev.Enabled = !loading;
|
||||
btnNext.Enabled = !loading;
|
||||
txtPage.Enabled = !loading;
|
||||
@@ -155,8 +157,34 @@ namespace OpenNest.Forms
|
||||
Text = "Best-Fit Viewer — Computing...";
|
||||
gridPanel.SuspendLayout();
|
||||
gridPanel.Controls.Clear();
|
||||
lblLoading = null;
|
||||
EnsureLoadingLabel();
|
||||
lblLoading.Text = string.Format("Computing best fits for {0}...", activeDrawing.Name);
|
||||
gridPanel.ResumeLayout(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lblLoading != null)
|
||||
lblLoading.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureLoadingLabel()
|
||||
{
|
||||
if (lblLoading != null)
|
||||
return;
|
||||
|
||||
lblLoading = new Label
|
||||
{
|
||||
AutoSize = false,
|
||||
TextAlign = ContentAlignment.MiddleCenter,
|
||||
ForeColor = Color.Gray,
|
||||
Font = new Font(Font.FontFamily, 14f),
|
||||
Dock = DockStyle.Fill
|
||||
};
|
||||
gridPanel.Controls.Add(lblLoading, 0, 0);
|
||||
gridPanel.SetColumnSpan(lblLoading, Columns);
|
||||
gridPanel.SetRowSpan(lblLoading, Rows);
|
||||
}
|
||||
|
||||
private static ComputeResult ComputeResults(Drawing drawing, double length, double width, double spacing)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
Generated
+11
-2
@@ -117,6 +117,7 @@
|
||||
manualSequenceToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
calculateCutTimeToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
|
||||
centerPartsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuPlateCutOff = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuWindow = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuWindowCascade = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuWindowTileVertical = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -627,7 +628,7 @@
|
||||
//
|
||||
// mnuPlate
|
||||
//
|
||||
mnuPlate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateEdit, mnuPlateSetAsDefault, toolStripMenuItem18, mnuPlateAdd, mnuPlateRemove, toolStripMenuItem16, mnuPlateFill, toolStripMenuItem9, mnuPlateRotate, mnuResizeToFitParts, toolStripMenuItem13, mnuPlateViewInCad, toolStripMenuItem20, mnuSequenceParts, calculateCutTimeToolStripMenuItem1, centerPartsToolStripMenuItem });
|
||||
mnuPlate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateEdit, mnuPlateSetAsDefault, toolStripMenuItem18, mnuPlateAdd, mnuPlateRemove, toolStripMenuItem16, mnuPlateFill, toolStripMenuItem9, mnuPlateCutOff, mnuPlateRotate, mnuResizeToFitParts, toolStripMenuItem13, mnuPlateViewInCad, toolStripMenuItem20, mnuSequenceParts, calculateCutTimeToolStripMenuItem1, centerPartsToolStripMenuItem });
|
||||
mnuPlate.Name = "mnuPlate";
|
||||
mnuPlate.Size = new System.Drawing.Size(45, 20);
|
||||
mnuPlate.Text = "&Plate";
|
||||
@@ -685,7 +686,14 @@
|
||||
toolStripMenuItem9.Size = new System.Drawing.Size(177, 22);
|
||||
toolStripMenuItem9.Text = "Fill Area";
|
||||
toolStripMenuItem9.Click += FillArea_Click;
|
||||
//
|
||||
//
|
||||
// mnuPlateCutOff
|
||||
//
|
||||
mnuPlateCutOff.Name = "mnuPlateCutOff";
|
||||
mnuPlateCutOff.Size = new System.Drawing.Size(177, 22);
|
||||
mnuPlateCutOff.Text = "Sheet Cut-Off";
|
||||
mnuPlateCutOff.Click += CutOff_Click;
|
||||
//
|
||||
// mnuPlateRotate
|
||||
//
|
||||
mnuPlateRotate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateRotateCw, mnuPlateRotateCcw, toolStripSeparator2, mnuPlateRotate180 });
|
||||
@@ -1205,5 +1213,6 @@
|
||||
private System.Windows.Forms.ToolStripComboBox engineComboBox;
|
||||
private System.Windows.Forms.ToolStripButton btnAutoNest;
|
||||
private System.Windows.Forms.ToolStripButton btnShowRemnants;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuPlateCutOff;
|
||||
}
|
||||
}
|
||||
@@ -626,7 +626,7 @@ namespace OpenNest.Forms
|
||||
return;
|
||||
}
|
||||
|
||||
using (var form = new BestFitViewerForm(drawings, plate))
|
||||
using (var form = new BestFitViewerForm(drawings, plate, activeForm.Nest.Units))
|
||||
{
|
||||
if (form.ShowDialog(this) == DialogResult.OK && form.SelectedResult != null)
|
||||
{
|
||||
@@ -1179,6 +1179,14 @@ namespace OpenNest.Forms
|
||||
activeForm.PlateView.SetAction(typeof(ActionSetSequence));
|
||||
}
|
||||
|
||||
private void CutOff_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (activeForm == null)
|
||||
return;
|
||||
|
||||
activeForm.PlateView.SetAction(typeof(ActionCutOff));
|
||||
}
|
||||
|
||||
#endregion Plate Menu Events
|
||||
|
||||
#region Window Menu Events
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" ReferenceOutputAssembly="false" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user