Compare commits
75 Commits
cca70db547
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c53d6fecd | |||
| e239967a7b | |||
| 9d57d3875a | |||
| 0e299d7f6f | |||
| c6f544c5d7 | |||
| 9563094c2b | |||
| 091e750e1b | |||
| 87b965f895 | |||
| 08f60690a7 | |||
| a4609c816c | |||
| 5a4272696e | |||
| 2cf03be360 | |||
| 041e184d93 | |||
| 26df3174ea | |||
| 0f5aace126 | |||
| 399f8dda6e | |||
| d921558b9c | |||
| bf3e3e1f42 | |||
| e120ece014 | |||
| 264e8264be | |||
| 24babe353e | |||
| e63be93051 | |||
| ba3c3cbea3 | |||
| 572fa06a21 | |||
| a6c2235647 | |||
| 5c918a0978 | |||
| 92461deb98 | |||
| bc859aa28c | |||
| 09eac96a03 | |||
| df65414a9d | |||
| 4aed231611 | |||
| c641b3b68e | |||
| f3b27c32c3 | |||
| c270d8ea76 | |||
| de6877ac48 | |||
| 3481764416 | |||
| 640814fdf6 | |||
| 6a30828fad | |||
| 786b6e2e88 | |||
| ba89967448 | |||
| b566d984b0 | |||
| c1e6092e83 | |||
| df86d4367b | |||
| 40026ab4dc | |||
| b18a82df7a | |||
| f090a2e299 | |||
| 55192a4888 | |||
| 7c28a35ad8 | |||
| b2a723ca60 | |||
| 3dca25c601 | |||
| ebc1a5f980 | |||
| b729f92cd6 | |||
| 5d6e018b81 | |||
| 5163b02f89 | |||
| a59911b38a | |||
| 810e37cacf | |||
| 8dfa45c446 | |||
| b223f69572 | |||
| 98c574c2ad | |||
| 30f1008fa9 | |||
| 41c20eaf75 | |||
| 3a97253473 | |||
| 3eab3c5946 | |||
| 0e05ad04ea | |||
| 5ac985dc0f | |||
| 865754611c | |||
| 9db326ee5d | |||
| 25faba430c | |||
| 089df67627 | |||
| 11884e712d | |||
| 6bed736cf0 | |||
| c20a079874 | |||
| 804a7fd9c1 | |||
| 3c4d00baa4 | |||
| 959ab15491 |
@@ -211,8 +211,5 @@ FakesAssemblies/
|
|||||||
.superpowers/
|
.superpowers/
|
||||||
docs/superpowers/
|
docs/superpowers/
|
||||||
|
|
||||||
# Documentation (manuals, templates, etc.)
|
|
||||||
docs/
|
|
||||||
|
|
||||||
# Launch settings
|
# Launch settings
|
||||||
**/Properties/launchSettings.json
|
**/Properties/launchSettings.json
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
|
|||||||
- `NestReader`/`NestWriter` — custom ZIP-based nest format (JSON metadata + G-code programs, v2 format).
|
- `NestReader`/`NestWriter` — custom ZIP-based nest format (JSON metadata + G-code programs, v2 format).
|
||||||
- `ProgramReader` — G-code text parser.
|
- `ProgramReader` — G-code text parser.
|
||||||
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
|
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
|
||||||
|
- `CadImporter` — shared "DXF → Drawing" service used by the UI, console, MCP, API, and training projects. Two-stage API: `Import(path, options)` loads raw entities, runs bend detection, and returns a mutable `CadImportResult`; `BuildDrawing(result, visible, bends, quantity, customer, editedProgram)` produces a fully-populated `Drawing` with `Source.Offset`, `SourceEntities`, `SuppressedEntityIds`, and bends. `ImportDrawing(path, options)` composes both stages for headless callers.
|
||||||
|
- `CadImportOptions`, `CadImportResult` — inputs and intermediate state for `CadImporter`.
|
||||||
|
|
||||||
### OpenNest.Console (console app, depends on Core + Engine + IO)
|
### OpenNest.Console (console app, depends on Core + Engine + IO)
|
||||||
Command-line interface for batch nesting. Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--autonest`).
|
Command-line interface for batch nesting. Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--autonest`).
|
||||||
@@ -117,3 +119,4 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe
|
|||||||
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
|
- `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).
|
- **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).
|
||||||
- **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity.
|
- **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity.
|
||||||
|
- **CAD import pipeline**: All "DXF → Drawing" conversion goes through `OpenNest.IO.CadImporter`. The UI form uses `Import` on file load (storing the mutable result in a `FileListItem`) and `BuildDrawing` on save (passing the user's current visible entities and bends). Console, MCP, API, and Training projects use `ImportDrawing` for headless conversion. This guarantees all callers produce drawings with the same shape: pierce-point `Source.Offset`, stable `SourceEntities` with GUIDs, `SuppressedEntityIds`, detected bends, and metadata.
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using OpenNest.Converters;
|
|
||||||
using OpenNest.Geometry;
|
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
|
|
||||||
namespace OpenNest.Api;
|
namespace OpenNest.Api;
|
||||||
@@ -30,15 +28,21 @@ public static class NestRunner
|
|||||||
if (!File.Exists(part.DxfPath))
|
if (!File.Exists(part.DxfPath))
|
||||||
throw new FileNotFoundException($"DXF file not found: {part.DxfPath}", part.DxfPath);
|
throw new FileNotFoundException($"DXF file not found: {part.DxfPath}", part.DxfPath);
|
||||||
|
|
||||||
var geometry = Dxf.GetGeometry(part.DxfPath);
|
Drawing drawing;
|
||||||
if (geometry.Count == 0)
|
try
|
||||||
|
{
|
||||||
|
drawing = CadImporter.ImportDrawing(part.DxfPath,
|
||||||
|
new CadImportOptions { Quantity = part.Quantity });
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Failed to import DXF: {part.DxfPath}", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawing.Program == null || drawing.Program.Codes.Count == 0)
|
||||||
throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}");
|
throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}");
|
||||||
|
|
||||||
var normalized = ShapeProfile.NormalizeEntities(geometry);
|
|
||||||
var pgm = ConvertGeometry.ToProgram(normalized);
|
|
||||||
var name = Path.GetFileNameWithoutExtension(part.DxfPath);
|
|
||||||
var drawing = new Drawing(name);
|
|
||||||
drawing.Program = pgm;
|
|
||||||
drawings.Add(drawing);
|
drawings.Add(drawing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using OpenNest;
|
using OpenNest;
|
||||||
using OpenNest.Converters;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
using System;
|
using System;
|
||||||
@@ -241,25 +240,15 @@ static class NestConsole
|
|||||||
|
|
||||||
static Drawing ImportDxf(string path)
|
static Drawing ImportDxf(string path)
|
||||||
{
|
{
|
||||||
var geometry = Dxf.GetGeometry(path);
|
try
|
||||||
|
|
||||||
if (geometry.Count == 0)
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Error: failed to read DXF file or no geometry found: {path}");
|
return CadImporter.ImportDrawing(path);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: failed to import DXF '{path}': {ex.Message}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalized = ShapeProfile.NormalizeEntities(geometry);
|
|
||||||
var pgm = ConvertGeometry.ToProgram(normalized);
|
|
||||||
|
|
||||||
if (pgm == null)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"Error: failed to convert geometry: {path}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(path);
|
|
||||||
return new Drawing(name, pgm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ApplyTemplate(Plate plate, Options options)
|
static void ApplyTemplate(Plate plate, Options options)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
@@ -11,6 +12,11 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
private record ContourEntry(Shape Shape, Vector Point, Entity Entity);
|
private record ContourEntry(Shape Shape, Vector Point, Entity Entity);
|
||||||
|
|
||||||
public CuttingResult Apply(Program partProgram, Vector approachPoint)
|
public CuttingResult Apply(Program partProgram, Vector approachPoint)
|
||||||
|
{
|
||||||
|
return Apply(partProgram, approachPoint, Vector.Invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CuttingResult Apply(Program partProgram, Vector approachPoint, Vector nextPartStart)
|
||||||
{
|
{
|
||||||
var entities = partProgram.ToGeometry();
|
var entities = partProgram.ToGeometry();
|
||||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||||
@@ -20,14 +26,43 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
var profile = new ShapeProfile(entities);
|
var profile = new ShapeProfile(entities);
|
||||||
|
|
||||||
// Forward pass: sequence cutouts nearest-neighbor from perimeter
|
// Start from the bounding box corner opposite the origin (max X, max Y)
|
||||||
var perimeterPoint = profile.Perimeter.ClosestPointTo(approachPoint, out _);
|
var bbox = entities.GetBoundingBox();
|
||||||
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
|
var startCorner = new Vector(bbox.Right, bbox.Top);
|
||||||
|
|
||||||
|
// Initial pass: sequence cutouts from bbox corner
|
||||||
|
var seedPoint = startCorner;
|
||||||
|
var orderedCutouts = SequenceCutouts(profile.Cutouts, seedPoint);
|
||||||
orderedCutouts.Reverse();
|
orderedCutouts.Reverse();
|
||||||
|
|
||||||
// Backward pass: walk from perimeter back through cutting order
|
var perimeterSeed = profile.Perimeter.ClosestPointTo(seedPoint, out _);
|
||||||
// so each lead-in faces the next cutout to be cut, not the previous
|
var cutoutEntries = ResolveLeadInPoints(orderedCutouts, perimeterSeed);
|
||||||
var cutoutEntries = ResolveLeadInPoints(orderedCutouts, perimeterPoint);
|
|
||||||
|
Vector perimeterPt;
|
||||||
|
Entity perimeterEntity;
|
||||||
|
|
||||||
|
if (!double.IsNaN(nextPartStart.X) && cutoutEntries.Count > 0)
|
||||||
|
{
|
||||||
|
// Iterate: each pass refines the perimeter lead-in which changes
|
||||||
|
// the internal sequence which changes the last cutout position
|
||||||
|
for (var iter = 0; iter < 3; iter++)
|
||||||
|
{
|
||||||
|
var lastCutoutPt = cutoutEntries[cutoutEntries.Count - 1].Point;
|
||||||
|
perimeterSeed = FindPerimeterIntersection(profile.Perimeter, lastCutoutPt, nextPartStart, out _);
|
||||||
|
|
||||||
|
orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterSeed);
|
||||||
|
orderedCutouts.Reverse();
|
||||||
|
cutoutEntries = ResolveLeadInPoints(orderedCutouts, perimeterSeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalLastCutout = cutoutEntries[cutoutEntries.Count - 1].Point;
|
||||||
|
perimeterPt = FindPerimeterIntersection(profile.Perimeter, finalLastCutout, nextPartStart, out perimeterEntity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var perimeterRef = cutoutEntries.Count > 0 ? cutoutEntries[0].Point : approachPoint;
|
||||||
|
perimeterPt = profile.Perimeter.ClosestPointTo(perimeterRef, out perimeterEntity);
|
||||||
|
}
|
||||||
|
|
||||||
var result = new Program(Mode.Absolute);
|
var result = new Program(Mode.Absolute);
|
||||||
|
|
||||||
@@ -36,9 +71,6 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
foreach (var entry in cutoutEntries)
|
foreach (var entry in cutoutEntries)
|
||||||
EmitContour(result, entry.Shape, entry.Point, entry.Entity);
|
EmitContour(result, entry.Shape, entry.Point, entry.Entity);
|
||||||
|
|
||||||
// Perimeter last
|
|
||||||
var lastRefPoint = cutoutEntries.Count > 0 ? cutoutEntries[cutoutEntries.Count - 1].Point : approachPoint;
|
|
||||||
var perimeterPt = profile.Perimeter.ClosestPointTo(lastRefPoint, out var perimeterEntity);
|
|
||||||
EmitContour(result, profile.Perimeter, perimeterPt, perimeterEntity, ContourType.External);
|
EmitContour(result, profile.Perimeter, perimeterPt, perimeterEntity, ContourType.External);
|
||||||
|
|
||||||
result.Mode = Mode.Incremental;
|
result.Mode = Mode.Incremental;
|
||||||
@@ -187,6 +219,40 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return new List<ContourEntry>(entries);
|
return new List<ContourEntry>(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Vector FindPerimeterIntersection(Shape perimeter, Vector lastCutout, Vector nextPartStart, out Entity entity)
|
||||||
|
{
|
||||||
|
var ray = new Line(lastCutout, nextPartStart);
|
||||||
|
|
||||||
|
if (perimeter.Intersects(ray, out var pts) && pts.Count > 0)
|
||||||
|
{
|
||||||
|
// Pick the intersection closest to the last cutout
|
||||||
|
var best = pts[0];
|
||||||
|
var bestDist = best.DistanceTo(lastCutout);
|
||||||
|
|
||||||
|
for (var i = 1; i < pts.Count; i++)
|
||||||
|
{
|
||||||
|
var dist = pts[i].DistanceTo(lastCutout);
|
||||||
|
if (dist < bestDist)
|
||||||
|
{
|
||||||
|
best = pts[i];
|
||||||
|
bestDist = dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return perimeter.ClosestPointTo(best, out entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: closest point on perimeter to the last cutout
|
||||||
|
return perimeter.ClosestPointTo(lastCutout, out entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ComputeSubProgramKey(double radius, double normalAngle)
|
||||||
|
{
|
||||||
|
var r = System.Math.Round(radius, 6);
|
||||||
|
var a = System.Math.Round(normalAngle, 6);
|
||||||
|
return HashCode.Combine(r, a);
|
||||||
|
}
|
||||||
|
|
||||||
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
|
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
|
||||||
{
|
{
|
||||||
var contourType = forceType ?? DetectContourType(shape);
|
var contourType = forceType ?? DetectContourType(shape);
|
||||||
@@ -197,16 +263,62 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
var leadOut = SelectLeadOut(contourType);
|
var leadOut = SelectLeadOut(contourType);
|
||||||
|
|
||||||
if (contourType == ContourType.ArcCircle && entity is Circle circle)
|
if (contourType == ContourType.ArcCircle && entity is Circle circle)
|
||||||
|
{
|
||||||
|
if (Parameters.RoundLeadInAngles && Parameters.LeadInAngleIncrement > 0)
|
||||||
|
{
|
||||||
|
var increment = Angle.ToRadians(Parameters.LeadInAngleIncrement);
|
||||||
|
normal = System.Math.Round(normal / increment) * increment;
|
||||||
|
normal = Angle.NormalizeRad(normal);
|
||||||
|
|
||||||
|
var outwardAngle = normal - System.Math.PI;
|
||||||
|
point = new Vector(
|
||||||
|
circle.Center.X + circle.Radius * System.Math.Cos(outwardAngle),
|
||||||
|
circle.Center.Y + circle.Radius * System.Math.Sin(outwardAngle));
|
||||||
|
}
|
||||||
|
|
||||||
leadIn = ClampLeadInForCircle(leadIn, circle, point, normal);
|
leadIn = ClampLeadInForCircle(leadIn, circle, point, normal);
|
||||||
|
|
||||||
|
// Build hole sub-program relative to (0,0)
|
||||||
|
var holeCenter = circle.Center;
|
||||||
|
var relativePoint = new Vector(point.X - holeCenter.X, point.Y - holeCenter.Y);
|
||||||
|
var relativeCircle = new Circle(new Vector(0, 0), circle.Radius) { Rotation = circle.Rotation };
|
||||||
|
var relativeShape = new Shape();
|
||||||
|
relativeShape.Entities.Add(relativeCircle);
|
||||||
|
|
||||||
|
var subPgm = new Program(Mode.Absolute);
|
||||||
|
subPgm.Codes.AddRange(leadIn.Generate(relativePoint, normal, winding));
|
||||||
|
var reindexed = relativeShape.ReindexAt(relativePoint, relativeCircle);
|
||||||
|
|
||||||
|
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
||||||
|
reindexed = TrimShapeForTab(reindexed, relativePoint, Parameters.TabConfig.Size);
|
||||||
|
|
||||||
|
subPgm.Codes.AddRange(ConvertShapeToMoves(reindexed, relativePoint));
|
||||||
|
subPgm.Codes.AddRange(leadOut.Generate(relativePoint, normal, winding));
|
||||||
|
subPgm.Mode = Mode.Incremental;
|
||||||
|
|
||||||
|
// Deduplicate: check if an identical sub-program already exists
|
||||||
|
var key = ComputeSubProgramKey(circle.Radius, normal);
|
||||||
|
if (!program.SubPrograms.ContainsKey(key))
|
||||||
|
program.SubPrograms[key] = subPgm;
|
||||||
|
|
||||||
|
program.Codes.Add(new SubProgramCall
|
||||||
|
{
|
||||||
|
Id = key,
|
||||||
|
Program = program.SubPrograms[key],
|
||||||
|
Offset = holeCenter
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
program.Codes.AddRange(leadIn.Generate(point, normal, winding));
|
program.Codes.AddRange(leadIn.Generate(point, normal, winding));
|
||||||
|
|
||||||
var reindexed = shape.ReindexAt(point, entity);
|
var reindexedShape = shape.ReindexAt(point, entity);
|
||||||
|
|
||||||
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
||||||
reindexed = TrimShapeForTab(reindexed, point, Parameters.TabConfig.Size);
|
reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size);
|
||||||
|
|
||||||
program.Codes.AddRange(ConvertShapeToMoves(reindexed, point));
|
program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point));
|
||||||
program.Codes.AddRange(leadOut.Generate(point, normal, winding));
|
program.Codes.AddRange(leadOut.Generate(point, normal, winding));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +421,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
if (shape.Entities.Count == 1 && shape.Entities[0] is Circle circle)
|
if (shape.Entities.Count == 1 && shape.Entities[0] is Circle circle)
|
||||||
return circle.Rotation;
|
return circle.Rotation;
|
||||||
|
|
||||||
return shape.ToPolygon().RotationDirection();
|
var polygon = shape.ToPolygon();
|
||||||
|
|
||||||
|
if (polygon.Vertices.Count < 3)
|
||||||
|
return RotationType.CCW;
|
||||||
|
|
||||||
|
return polygon.RotationDirection();
|
||||||
}
|
}
|
||||||
|
|
||||||
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
|
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
public double PierceClearance { get; set; } = 0.0625;
|
public double PierceClearance { get; set; } = 0.0625;
|
||||||
|
|
||||||
|
public bool RoundLeadInAngles { get; set; }
|
||||||
|
public double LeadInAngleIncrement { get; set; } = 5.0;
|
||||||
|
|
||||||
public double AutoTabMinSize { get; set; }
|
public double AutoTabMinSize { get; set; }
|
||||||
public double AutoTabMaxSize { get; set; }
|
public double AutoTabMaxSize { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
|
||||||
{
|
|
||||||
public class MicrotabLeadOut : LeadOut
|
|
||||||
{
|
|
||||||
public double GapSize { get; set; } = 0.03;
|
|
||||||
|
|
||||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
|
||||||
RotationType winding = RotationType.CW)
|
|
||||||
{
|
|
||||||
return new List<ICode>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,8 @@ namespace OpenNest.CNC
|
|||||||
|
|
||||||
public Dictionary<string, VariableDefinition> Variables { get; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, VariableDefinition> Variables { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public Dictionary<int, Program> SubPrograms { get; } = new();
|
||||||
|
|
||||||
private Mode mode;
|
private Mode mode;
|
||||||
|
|
||||||
public Program(Mode mode = Mode.Absolute)
|
public Program(Mode mode = Mode.Absolute)
|
||||||
@@ -87,6 +89,17 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var subpgm = (SubProgramCall)code;
|
var subpgm = (SubProgramCall)code;
|
||||||
|
|
||||||
|
if (subpgm.Offset.X != 0 || subpgm.Offset.Y != 0)
|
||||||
|
{
|
||||||
|
var cos = System.Math.Cos(angle);
|
||||||
|
var sin = System.Math.Sin(angle);
|
||||||
|
var dx = subpgm.Offset.X - origin.X;
|
||||||
|
var dy = subpgm.Offset.Y - origin.Y;
|
||||||
|
subpgm.Offset = new Geometry.Vector(
|
||||||
|
origin.X + dx * cos - dy * sin,
|
||||||
|
origin.Y + dx * sin + dy * cos);
|
||||||
|
}
|
||||||
|
|
||||||
if (subpgm.Program != null)
|
if (subpgm.Program != null)
|
||||||
subpgm.Program.Rotate(angle, origin);
|
subpgm.Program.Rotate(angle, origin);
|
||||||
}
|
}
|
||||||
@@ -115,6 +128,12 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var code = Codes[i];
|
var code = Codes[i];
|
||||||
|
|
||||||
|
if (code is SubProgramCall subpgm)
|
||||||
|
{
|
||||||
|
subpgm.Offset = new Geometry.Vector(
|
||||||
|
subpgm.Offset.X + x, subpgm.Offset.Y + y);
|
||||||
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
if (code is Motion == false)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -137,6 +156,12 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var code = Codes[i];
|
var code = Codes[i];
|
||||||
|
|
||||||
|
if (code is SubProgramCall subpgm)
|
||||||
|
{
|
||||||
|
subpgm.Offset = new Geometry.Vector(
|
||||||
|
subpgm.Offset.X + voffset.X, subpgm.Offset.Y + voffset.Y);
|
||||||
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
if (code is Motion == false)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -275,6 +300,10 @@ namespace OpenNest.CNC
|
|||||||
|
|
||||||
private Box BoundingBox(ref Vector pos)
|
private Box BoundingBox(ref Vector pos)
|
||||||
{
|
{
|
||||||
|
// Capture the frame origin at entry. Sub-program Offsets and
|
||||||
|
// absolute-mode endpoints are relative to this fixed origin.
|
||||||
|
var frameOrigin = pos;
|
||||||
|
|
||||||
double minX = 0.0;
|
double minX = 0.0;
|
||||||
double minY = 0.0;
|
double minY = 0.0;
|
||||||
double maxX = 0.0;
|
double maxX = 0.0;
|
||||||
@@ -290,7 +319,7 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var line = (LinearMove)code;
|
var line = (LinearMove)code;
|
||||||
var pt = Mode == Mode.Absolute ?
|
var pt = Mode == Mode.Absolute ?
|
||||||
line.EndPoint :
|
frameOrigin + line.EndPoint :
|
||||||
line.EndPoint + pos;
|
line.EndPoint + pos;
|
||||||
|
|
||||||
if (pt.X > maxX)
|
if (pt.X > maxX)
|
||||||
@@ -312,7 +341,7 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var line = (RapidMove)code;
|
var line = (RapidMove)code;
|
||||||
var pt = Mode == Mode.Absolute
|
var pt = Mode == Mode.Absolute
|
||||||
? line.EndPoint
|
? frameOrigin + line.EndPoint
|
||||||
: line.EndPoint + pos;
|
: line.EndPoint + pos;
|
||||||
|
|
||||||
if (pt.X > maxX)
|
if (pt.X > maxX)
|
||||||
@@ -345,8 +374,8 @@ namespace OpenNest.CNC
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
endpt = arc.EndPoint;
|
endpt = frameOrigin + arc.EndPoint;
|
||||||
centerpt = arc.CenterPoint;
|
centerpt = frameOrigin + arc.CenterPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
double minX1;
|
double minX1;
|
||||||
@@ -420,6 +449,12 @@ namespace OpenNest.CNC
|
|||||||
case CodeType.SubProgramCall:
|
case CodeType.SubProgramCall:
|
||||||
{
|
{
|
||||||
var subpgm = (SubProgramCall)code;
|
var subpgm = (SubProgramCall)code;
|
||||||
|
if (subpgm.Program == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Sub-program frame origin in this program's frame
|
||||||
|
// is frameOrigin + Offset, regardless of current pos.
|
||||||
|
pos = frameOrigin + subpgm.Offset;
|
||||||
var box = subpgm.Program.BoundingBox(ref pos);
|
var box = subpgm.Program.BoundingBox(ref pos);
|
||||||
|
|
||||||
if (box.Left < minX)
|
if (box.Left < minX)
|
||||||
@@ -460,6 +495,9 @@ namespace OpenNest.CNC
|
|||||||
foreach (var kvp in Variables)
|
foreach (var kvp in Variables)
|
||||||
pgm.Variables[kvp.Key] = kvp.Value;
|
pgm.Variables[kvp.Key] = kvp.Value;
|
||||||
|
|
||||||
|
foreach (var kvp in SubPrograms)
|
||||||
|
pgm.SubPrograms[kvp.Key] = (Program)kvp.Value.Clone();
|
||||||
|
|
||||||
return pgm;
|
return pgm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using OpenNest.Math;
|
using System.Text;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.CNC
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
@@ -35,6 +37,12 @@ namespace OpenNest.CNC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the offset (position) at which the sub-program is executed.
|
||||||
|
/// For hole sub-programs, this is the hole center.
|
||||||
|
/// </summary>
|
||||||
|
public Vector Offset { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the rotation of the program in degrees.
|
/// Gets or sets the rotation of the program in degrees.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -78,12 +86,18 @@ namespace OpenNest.CNC
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public ICode Clone()
|
public ICode Clone()
|
||||||
{
|
{
|
||||||
return new SubProgramCall(program, Rotation);
|
return new SubProgramCall(program, Rotation) { Id = Id, Offset = Offset };
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format("G65 P{0} R{1}", Id, Rotation);
|
var sb = new StringBuilder();
|
||||||
|
sb.Append($"G65 P{Id}");
|
||||||
|
if (Offset.X != 0 || Offset.Y != 0)
|
||||||
|
sb.Append($" X{Offset.X} Y{Offset.Y}");
|
||||||
|
if (Rotation != 0)
|
||||||
|
sb.Append($" R{Rotation}");
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ namespace OpenNest.Converters
|
|||||||
if (startpt != lastpt)
|
if (startpt != lastpt)
|
||||||
pgm.MoveTo(startpt);
|
pgm.MoveTo(startpt);
|
||||||
|
|
||||||
pgm.ArcTo(startpt, circle.Center, RotationType.CCW);
|
pgm.ArcTo(startpt, circle.Center, circle.Rotation);
|
||||||
|
|
||||||
lastpt = startpt;
|
lastpt = startpt;
|
||||||
return lastpt;
|
return lastpt;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.Converters
|
namespace OpenNest.Converters
|
||||||
@@ -9,7 +9,6 @@ namespace OpenNest.Converters
|
|||||||
/// Converts the program to absolute coordinates.
|
/// Converts the program to absolute coordinates.
|
||||||
/// Does NOT check program mode before converting.
|
/// Does NOT check program mode before converting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pgm"></param>
|
|
||||||
public static void ToAbsolute(Program pgm)
|
public static void ToAbsolute(Program pgm)
|
||||||
{
|
{
|
||||||
var pos = new Vector(0, 0);
|
var pos = new Vector(0, 0);
|
||||||
@@ -17,21 +16,27 @@ namespace OpenNest.Converters
|
|||||||
for (int i = 0; i < pgm.Codes.Count; ++i)
|
for (int i = 0; i < pgm.Codes.Count; ++i)
|
||||||
{
|
{
|
||||||
var code = pgm.Codes[i];
|
var code = pgm.Codes[i];
|
||||||
var motion = code as Motion;
|
|
||||||
|
|
||||||
if (motion != null)
|
if (code is SubProgramCall subCall && subCall.Program != null)
|
||||||
{
|
{
|
||||||
motion.Offset(pos);
|
// Sub-program is placed at Offset in this program's frame.
|
||||||
|
// After it runs, the tool is at Offset + (sub's end in its own frame).
|
||||||
|
pos = ComputeEndPosition(subCall.Program, subCall.Offset);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code is Motion motion)
|
||||||
|
{
|
||||||
|
motion.Offset(pos.X, pos.Y);
|
||||||
pos = motion.EndPoint;
|
pos = motion.EndPoint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts the program to intermental coordinates.
|
/// Converts the program to incremental coordinates.
|
||||||
/// Does NOT check program mode before converting.
|
/// Does NOT check program mode before converting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pgm"></param>
|
|
||||||
public static void ToIncremental(Program pgm)
|
public static void ToIncremental(Program pgm)
|
||||||
{
|
{
|
||||||
var pos = new Vector(0, 0);
|
var pos = new Vector(0, 0);
|
||||||
@@ -39,9 +44,16 @@ namespace OpenNest.Converters
|
|||||||
for (int i = 0; i < pgm.Codes.Count; ++i)
|
for (int i = 0; i < pgm.Codes.Count; ++i)
|
||||||
{
|
{
|
||||||
var code = pgm.Codes[i];
|
var code = pgm.Codes[i];
|
||||||
var motion = code as Motion;
|
|
||||||
|
|
||||||
if (motion != null)
|
if (code is SubProgramCall subCall && subCall.Program != null)
|
||||||
|
{
|
||||||
|
// Sub-program is placed at Offset in this program's frame,
|
||||||
|
// regardless of where the tool was before the call.
|
||||||
|
pos = ComputeEndPosition(subCall.Program, subCall.Offset);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code is Motion motion)
|
||||||
{
|
{
|
||||||
var pos2 = motion.EndPoint;
|
var pos2 = motion.EndPoint;
|
||||||
motion.Offset(-pos.X, -pos.Y);
|
motion.Offset(-pos.X, -pos.Y);
|
||||||
@@ -49,5 +61,37 @@ namespace OpenNest.Converters
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the tool position after executing <paramref name="pgm"/>,
|
||||||
|
/// given that the program's frame origin is at <paramref name="startPos"/>
|
||||||
|
/// in the caller's frame. Walks nested sub-program calls recursively.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector ComputeEndPosition(Program pgm, Vector startPos)
|
||||||
|
{
|
||||||
|
var pos = startPos;
|
||||||
|
|
||||||
|
for (int i = 0; i < pgm.Codes.Count; ++i)
|
||||||
|
{
|
||||||
|
var code = pgm.Codes[i];
|
||||||
|
|
||||||
|
if (code is SubProgramCall subCall && subCall.Program != null)
|
||||||
|
{
|
||||||
|
// Nested sub's frame origin in the caller's frame is startPos + Offset.
|
||||||
|
pos = ComputeEndPosition(subCall.Program, startPos + subCall.Offset);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code is Motion motion)
|
||||||
|
{
|
||||||
|
if (pgm.Mode == Mode.Incremental)
|
||||||
|
pos = pos + motion.EndPoint;
|
||||||
|
else
|
||||||
|
pos = startPos + motion.EndPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ namespace OpenNest.Converters
|
|||||||
|
|
||||||
private static void AddProgram(Program program, ref Mode mode, ref Vector curpos, ref List<Entity> geometry)
|
private static void AddProgram(Program program, ref Mode mode, ref Vector curpos, ref List<Entity> geometry)
|
||||||
{
|
{
|
||||||
|
// Capture the frame origin at entry. Sub-program Offsets are relative
|
||||||
|
// to this fixed origin, not to the current tool position.
|
||||||
|
var frameOrigin = curpos;
|
||||||
mode = program.Mode;
|
mode = program.Mode;
|
||||||
|
|
||||||
for (int i = 0; i < program.Length; ++i)
|
for (int i = 0; i < program.Length; ++i)
|
||||||
@@ -41,12 +44,15 @@ namespace OpenNest.Converters
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case CodeType.SubProgramCall:
|
case CodeType.SubProgramCall:
|
||||||
var tmpmode = mode;
|
|
||||||
var subpgm = (SubProgramCall)code;
|
var subpgm = (SubProgramCall)code;
|
||||||
var geoProgram = new Shape();
|
var savedMode = mode;
|
||||||
AddProgram(subpgm.Program, ref mode, ref curpos, ref geoProgram.Entities);
|
|
||||||
geometry.Add(geoProgram);
|
// The sub-program's frame origin in this program's frame is
|
||||||
mode = tmpmode;
|
// frameOrigin + Offset — independent of current tool position.
|
||||||
|
curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
|
||||||
|
|
||||||
|
AddProgram(subpgm.Program, ref mode, ref curpos, ref geometry);
|
||||||
|
mode = savedMode;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +112,7 @@ namespace OpenNest.Converters
|
|||||||
var layer = ConvertLayer(arcMove.Layer);
|
var layer = ConvertLayer(arcMove.Layer);
|
||||||
|
|
||||||
if (startAngle.IsEqualTo(endAngle))
|
if (startAngle.IsEqualTo(endAngle))
|
||||||
geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color });
|
geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color, Rotation = arcMove.Rotation });
|
||||||
else
|
else
|
||||||
geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer, Color = layer.Color });
|
geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer, Color = layer.Color });
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -90,6 +91,18 @@ namespace OpenNest
|
|||||||
|
|
||||||
public List<Bend> Bends { get; set; } = new List<Bend>();
|
public List<Bend> Bends { get; set; } = new List<Bend>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Complete set of source entities with stable GUIDs.
|
||||||
|
/// Null when the drawing was created from G-code or an older nest file.
|
||||||
|
/// </summary>
|
||||||
|
public List<Entity> SourceEntities { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IDs of entities in <see cref="SourceEntities"/> that are suppressed (hidden).
|
||||||
|
/// Suppressed entities are excluded from the active Program but preserved for re-enabling.
|
||||||
|
/// </summary>
|
||||||
|
public HashSet<Guid> SuppressedEntityIds { get; set; } = new HashSet<Guid>();
|
||||||
|
|
||||||
public double Area { get; protected set; }
|
public double Area { get; protected set; }
|
||||||
|
|
||||||
public void UpdateArea()
|
public void UpdateArea()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
|
||||||
@@ -10,10 +11,16 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
protected Entity()
|
protected Entity()
|
||||||
{
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
Layer = OpenNest.Geometry.Layer.Default;
|
Layer = OpenNest.Geometry.Layer.Default;
|
||||||
boundingBox = new Box();
|
boundingBox = new Box();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier for this entity, stable across edit sessions.
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entity color (resolved from DXF ByLayer/ByBlock to actual color).
|
/// Entity color (resolved from DXF ByLayer/ByBlock to actual color).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -605,7 +605,7 @@ namespace OpenNest.Geometry
|
|||||||
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
||||||
break;
|
break;
|
||||||
case Circle c:
|
case Circle c:
|
||||||
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer });
|
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CW });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -640,7 +640,7 @@ namespace OpenNest.Geometry
|
|||||||
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
||||||
break;
|
break;
|
||||||
case Circle c:
|
case Circle c:
|
||||||
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer });
|
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CCW });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,39 @@ namespace OpenNest.Geometry
|
|||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Solves ray-circle intersection, returning the two parametric t values.
|
||||||
|
/// Returns false if no real intersection exists.
|
||||||
|
/// </summary>
|
||||||
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool SolveRayCircle(
|
||||||
|
double vx, double vy,
|
||||||
|
double cx, double cy, double r,
|
||||||
|
double dirX, double dirY,
|
||||||
|
out double t1, out double t2)
|
||||||
|
{
|
||||||
|
var ox = vx - cx;
|
||||||
|
var oy = vy - cy;
|
||||||
|
|
||||||
|
var a = dirX * dirX + dirY * dirY;
|
||||||
|
var b = 2.0 * (ox * dirX + oy * dirY);
|
||||||
|
var c = ox * ox + oy * oy - r * r;
|
||||||
|
|
||||||
|
var discriminant = b * b - 4.0 * a * c;
|
||||||
|
if (discriminant < 0)
|
||||||
|
{
|
||||||
|
t1 = t2 = double.MaxValue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sqrtD = System.Math.Sqrt(discriminant);
|
||||||
|
var inv2a = 1.0 / (2.0 * a);
|
||||||
|
t1 = (-b - sqrtD) * inv2a;
|
||||||
|
t2 = (-b + sqrtD) * inv2a;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the distance from a point along a direction to an arc.
|
/// Computes the distance from a point along a direction to an arc.
|
||||||
/// Solves ray-circle intersection, then constrains hits to the arc's
|
/// Solves ray-circle intersection, then constrains hits to the arc's
|
||||||
@@ -117,25 +150,9 @@ namespace OpenNest.Geometry
|
|||||||
double startAngle, double endAngle, bool reversed,
|
double startAngle, double endAngle, bool reversed,
|
||||||
double dirX, double dirY)
|
double dirX, double dirY)
|
||||||
{
|
{
|
||||||
// Ray: P = (vx,vy) + t*(dirX,dirY)
|
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||||
// Circle: (x-cx)^2 + (y-cy)^2 = r^2
|
|
||||||
var ox = vx - cx;
|
|
||||||
var oy = vy - cy;
|
|
||||||
|
|
||||||
// a = dirX^2 + dirY^2 = 1 for unit direction, but handle general case
|
|
||||||
var a = dirX * dirX + dirY * dirY;
|
|
||||||
var b = 2.0 * (ox * dirX + oy * dirY);
|
|
||||||
var c = ox * ox + oy * oy - r * r;
|
|
||||||
|
|
||||||
var discriminant = b * b - 4.0 * a * c;
|
|
||||||
if (discriminant < 0)
|
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
|
|
||||||
var sqrtD = System.Math.Sqrt(discriminant);
|
|
||||||
var inv2a = 1.0 / (2.0 * a);
|
|
||||||
var t1 = (-b - sqrtD) * inv2a;
|
|
||||||
var t2 = (-b + sqrtD) * inv2a;
|
|
||||||
|
|
||||||
var best = double.MaxValue;
|
var best = double.MaxValue;
|
||||||
|
|
||||||
if (t1 > -Tolerance.Epsilon)
|
if (t1 > -Tolerance.Epsilon)
|
||||||
@@ -168,27 +185,13 @@ namespace OpenNest.Geometry
|
|||||||
double cx, double cy, double r,
|
double cx, double cy, double r,
|
||||||
double dirX, double dirY)
|
double dirX, double dirY)
|
||||||
{
|
{
|
||||||
var ox = vx - cx;
|
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||||
var oy = vy - cy;
|
|
||||||
|
|
||||||
var a = dirX * dirX + dirY * dirY;
|
|
||||||
var b = 2.0 * (ox * dirX + oy * dirY);
|
|
||||||
var c = ox * ox + oy * oy - r * r;
|
|
||||||
|
|
||||||
var discriminant = b * b - 4.0 * a * c;
|
|
||||||
if (discriminant < 0)
|
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
|
|
||||||
var sqrtD = System.Math.Sqrt(discriminant);
|
if (t1 > Tolerance.Epsilon) return t1;
|
||||||
var t = (-b - sqrtD) / (2.0 * a);
|
if (t1 >= -Tolerance.Epsilon) return 0;
|
||||||
|
if (t2 > Tolerance.Epsilon) return t2;
|
||||||
if (t > Tolerance.Epsilon) return t;
|
if (t2 >= -Tolerance.Epsilon) return 0;
|
||||||
if (t >= -Tolerance.Epsilon) return 0;
|
|
||||||
|
|
||||||
// First root is behind us, try the second
|
|
||||||
t = (-b + sqrtD) / (2.0 * a);
|
|
||||||
if (t > Tolerance.Epsilon) return t;
|
|
||||||
if (t >= -Tolerance.Epsilon) return 0;
|
|
||||||
|
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
}
|
}
|
||||||
@@ -200,57 +203,7 @@ namespace OpenNest.Geometry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
|
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
|
||||||
{
|
{
|
||||||
var minDist = double.MaxValue;
|
return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
|
||||||
|
|
||||||
// Case 1: Each moving vertex -> each stationary edge
|
|
||||||
var movingVertices = new HashSet<Vector>();
|
|
||||||
for (int i = 0; i < movingLines.Count; i++)
|
|
||||||
{
|
|
||||||
movingVertices.Add(movingLines[i].pt1);
|
|
||||||
movingVertices.Add(movingLines[i].pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count];
|
|
||||||
for (int i = 0; i < stationaryLines.Count; i++)
|
|
||||||
stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2);
|
|
||||||
|
|
||||||
// Sort edges for pruning if not already sorted (usually they aren't here)
|
|
||||||
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
|
||||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
|
||||||
else
|
|
||||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
|
||||||
|
|
||||||
foreach (var mv in movingVertices)
|
|
||||||
{
|
|
||||||
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
|
|
||||||
if (d < minDist) minDist = d;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
|
||||||
var opposite = OppositeDirection(direction);
|
|
||||||
var stationaryVertices = new HashSet<Vector>();
|
|
||||||
for (int i = 0; i < stationaryLines.Count; i++)
|
|
||||||
{
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt1);
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
var movingEdges = new (Vector start, Vector end)[movingLines.Count];
|
|
||||||
for (int i = 0; i < movingLines.Count; i++)
|
|
||||||
movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2);
|
|
||||||
|
|
||||||
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
|
|
||||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
|
||||||
else
|
|
||||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
|
||||||
|
|
||||||
foreach (var sv in stationaryVertices)
|
|
||||||
{
|
|
||||||
var d = OneWayDistance(sv, movingEdges, Vector.Zero, opposite);
|
|
||||||
if (d < minDist) minDist = d;
|
|
||||||
}
|
|
||||||
|
|
||||||
return minDist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -265,21 +218,10 @@ namespace OpenNest.Geometry
|
|||||||
var movingOffset = new Vector(movingDx, movingDy);
|
var movingOffset = new Vector(movingDx, movingDy);
|
||||||
|
|
||||||
// Case 1: Each moving vertex -> each stationary edge
|
// Case 1: Each moving vertex -> each stationary edge
|
||||||
var movingVertices = new HashSet<Vector>();
|
var movingVertices = CollectVertices(movingLines, movingOffset);
|
||||||
for (int i = 0; i < movingLines.Count; i++)
|
|
||||||
{
|
|
||||||
movingVertices.Add(movingLines[i].pt1 + movingOffset);
|
|
||||||
movingVertices.Add(movingLines[i].pt2 + movingOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count];
|
var stationaryEdges = ToEdgeArray(stationaryLines);
|
||||||
for (int i = 0; i < stationaryLines.Count; i++)
|
SortEdgesForPruning(stationaryEdges, direction);
|
||||||
stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2);
|
|
||||||
|
|
||||||
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
|
||||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
|
||||||
else
|
|
||||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
|
||||||
|
|
||||||
foreach (var mv in movingVertices)
|
foreach (var mv in movingVertices)
|
||||||
{
|
{
|
||||||
@@ -289,21 +231,10 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
var opposite = OppositeDirection(direction);
|
var opposite = OppositeDirection(direction);
|
||||||
var stationaryVertices = new HashSet<Vector>();
|
var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero);
|
||||||
for (int i = 0; i < stationaryLines.Count; i++)
|
|
||||||
{
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt1);
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
var movingEdges = new (Vector start, Vector end)[movingLines.Count];
|
var movingEdges = ToEdgeArray(movingLines);
|
||||||
for (int i = 0; i < movingLines.Count; i++)
|
SortEdgesForPruning(movingEdges, opposite);
|
||||||
movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2);
|
|
||||||
|
|
||||||
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
|
|
||||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
|
||||||
else
|
|
||||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
|
||||||
|
|
||||||
foreach (var sv in stationaryVertices)
|
foreach (var sv in stationaryVertices)
|
||||||
{
|
{
|
||||||
@@ -342,15 +273,11 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var minDist = double.MaxValue;
|
var minDist = double.MaxValue;
|
||||||
|
|
||||||
// Extract unique vertices from moving edges.
|
SortEdgesForPruning(stationaryEdges, direction);
|
||||||
var movingVertices = new HashSet<Vector>();
|
|
||||||
for (var i = 0; i < movingEdges.Length; i++)
|
|
||||||
{
|
|
||||||
movingVertices.Add(movingEdges[i].start + movingOffset);
|
|
||||||
movingVertices.Add(movingEdges[i].end + movingOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 1: Each moving vertex -> each stationary edge
|
// Case 1: Each moving vertex -> each stationary edge
|
||||||
|
var movingVertices = CollectVertices(movingEdges, movingOffset);
|
||||||
|
|
||||||
foreach (var mv in movingVertices)
|
foreach (var mv in movingVertices)
|
||||||
{
|
{
|
||||||
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
|
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
|
||||||
@@ -359,12 +286,9 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
var opposite = OppositeDirection(direction);
|
var opposite = OppositeDirection(direction);
|
||||||
var stationaryVertices = new HashSet<Vector>();
|
SortEdgesForPruning(movingEdges, opposite);
|
||||||
for (var i = 0; i < stationaryEdges.Length; i++)
|
|
||||||
{
|
var stationaryVertices = CollectVertices(stationaryEdges, stationaryOffset);
|
||||||
stationaryVertices.Add(stationaryEdges[i].start + stationaryOffset);
|
|
||||||
stationaryVertices.Add(stationaryEdges[i].end + stationaryOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var sv in stationaryVertices)
|
foreach (var sv in stationaryVertices)
|
||||||
{
|
{
|
||||||
@@ -556,12 +480,7 @@ namespace OpenNest.Geometry
|
|||||||
var dirX = direction.X;
|
var dirX = direction.X;
|
||||||
var dirY = direction.Y;
|
var dirY = direction.Y;
|
||||||
|
|
||||||
var movingVertices = new HashSet<Vector>();
|
var movingVertices = CollectVertices(movingLines, Vector.Zero);
|
||||||
for (var i = 0; i < movingLines.Count; i++)
|
|
||||||
{
|
|
||||||
movingVertices.Add(movingLines[i].pt1);
|
|
||||||
movingVertices.Add(movingLines[i].pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var mv in movingVertices)
|
foreach (var mv in movingVertices)
|
||||||
{
|
{
|
||||||
@@ -576,12 +495,7 @@ namespace OpenNest.Geometry
|
|||||||
var oppX = -dirX;
|
var oppX = -dirX;
|
||||||
var oppY = -dirY;
|
var oppY = -dirY;
|
||||||
|
|
||||||
var stationaryVertices = new HashSet<Vector>();
|
var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero);
|
||||||
for (var i = 0; i < stationaryLines.Count; i++)
|
|
||||||
{
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt1);
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var sv in stationaryVertices)
|
foreach (var sv in stationaryVertices)
|
||||||
{
|
{
|
||||||
@@ -596,6 +510,284 @@ namespace OpenNest.Geometry
|
|||||||
return minDist;
|
return minDist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum translation distance along a push direction
|
||||||
|
/// before any vertex/edge of movingEntities contacts any vertex/edge of
|
||||||
|
/// stationaryEntities. Delegates to the Vector-based overload.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(
|
||||||
|
List<Entity> movingEntities, List<Entity> stationaryEntities, PushDirection direction)
|
||||||
|
{
|
||||||
|
return DirectionalDistance(movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum translation distance along an arbitrary unit direction
|
||||||
|
/// before any vertex/edge of movingEntities contacts any vertex/edge of
|
||||||
|
/// stationaryEntities. Works with native Line, Arc, and Circle entities
|
||||||
|
/// without tessellation.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(
|
||||||
|
List<Entity> movingEntities, List<Entity> stationaryEntities, Vector direction)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
var dirX = direction.X;
|
||||||
|
var dirY = direction.Y;
|
||||||
|
|
||||||
|
var movingVertices = ExtractEntityVertices(movingEntities);
|
||||||
|
|
||||||
|
for (var v = 0; v < movingVertices.Length; v++)
|
||||||
|
{
|
||||||
|
var vx = movingVertices[v].X;
|
||||||
|
var vy = movingVertices[v].Y;
|
||||||
|
|
||||||
|
for (var j = 0; j < stationaryEntities.Count; j++)
|
||||||
|
{
|
||||||
|
var d = RayEntityDistance(vx, vy, stationaryEntities[j], dirX, dirY);
|
||||||
|
if (d < minDist)
|
||||||
|
{
|
||||||
|
minDist = d;
|
||||||
|
if (d <= 0) return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var oppX = -dirX;
|
||||||
|
var oppY = -dirY;
|
||||||
|
|
||||||
|
var stationaryVertices = ExtractEntityVertices(stationaryEntities);
|
||||||
|
|
||||||
|
for (var v = 0; v < stationaryVertices.Length; v++)
|
||||||
|
{
|
||||||
|
var vx = stationaryVertices[v].X;
|
||||||
|
var vy = stationaryVertices[v].Y;
|
||||||
|
|
||||||
|
for (var j = 0; j < movingEntities.Count; j++)
|
||||||
|
{
|
||||||
|
var d = RayEntityDistance(vx, vy, movingEntities[j], oppX, oppY);
|
||||||
|
if (d < minDist)
|
||||||
|
{
|
||||||
|
minDist = d;
|
||||||
|
if (d <= 0) return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Arc-to-line closest-point check.
|
||||||
|
// Phases 1-2 sample arc endpoints and cardinal extremes, but the actual
|
||||||
|
// closest point on a small corner arc to a straight edge may lie between
|
||||||
|
// those samples. Use ClosestPointTo to find it and fire a ray from there.
|
||||||
|
minDist = ArcToLineClosestDistance(movingEntities, stationaryEntities, dirX, dirY, minDist);
|
||||||
|
if (minDist <= 0) return 0;
|
||||||
|
minDist = ArcToLineClosestDistance(stationaryEntities, movingEntities, oppX, oppY, minDist);
|
||||||
|
if (minDist <= 0) return 0;
|
||||||
|
|
||||||
|
// Phase 4: Curve-to-curve direct distance.
|
||||||
|
// The vertex-to-entity approach misses the closest contact between two
|
||||||
|
// curved entities (circles/arcs) because only a few cardinal vertices are
|
||||||
|
// sampled. The true closest contact along the push direction is found by
|
||||||
|
// treating it as a ray from one center to an expanded circle at the other
|
||||||
|
// center (radius = r1 + r2).
|
||||||
|
for (var i = 0; i < movingEntities.Count; i++)
|
||||||
|
{
|
||||||
|
var me = movingEntities[i];
|
||||||
|
if (!TryGetCurveParams(me, out var mcx, out var mcy, out var mr))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (var j = 0; j < stationaryEntities.Count; j++)
|
||||||
|
{
|
||||||
|
var se = stationaryEntities[j];
|
||||||
|
if (!TryGetCurveParams(se, out var scx, out var scy, out var sr))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var d = RayCircleDistance(mcx, mcy, scx, scy, mr + sr, dirX, dirY);
|
||||||
|
|
||||||
|
if (d >= minDist)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// For arcs, verify the contact point falls within both arcs' angular ranges.
|
||||||
|
if (me is Arc || se is Arc)
|
||||||
|
{
|
||||||
|
var mx = mcx + d * dirX;
|
||||||
|
var my = mcy + d * dirY;
|
||||||
|
var toCx = scx - mx;
|
||||||
|
var toCy = scy - my;
|
||||||
|
|
||||||
|
if (me is Arc mArc)
|
||||||
|
{
|
||||||
|
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
||||||
|
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (se is Arc sArc)
|
||||||
|
{
|
||||||
|
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
|
||||||
|
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minDist = d;
|
||||||
|
if (d <= 0) return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ArcToLineClosestDistance(
|
||||||
|
List<Entity> arcEntities, List<Entity> lineEntities,
|
||||||
|
double dirX, double dirY, double minDist)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < arcEntities.Count; i++)
|
||||||
|
{
|
||||||
|
if (arcEntities[i] is Arc arc)
|
||||||
|
{
|
||||||
|
for (var j = 0; j < lineEntities.Count; j++)
|
||||||
|
{
|
||||||
|
if (lineEntities[j] is Line line)
|
||||||
|
{
|
||||||
|
var linePt = line.ClosestPointTo(arc.Center);
|
||||||
|
var arcPt = arc.ClosestPointTo(linePt);
|
||||||
|
var d = RayEdgeDistance(arcPt.X, arcPt.Y,
|
||||||
|
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
|
||||||
|
dirX, dirY);
|
||||||
|
if (d < minDist) { minDist = d; if (d <= 0) return 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double RayEntityDistance(
|
||||||
|
double vx, double vy, Entity entity, double dirX, double dirY)
|
||||||
|
{
|
||||||
|
if (entity is Line line)
|
||||||
|
{
|
||||||
|
return RayEdgeDistance(vx, vy,
|
||||||
|
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
|
||||||
|
dirX, dirY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
return RayArcDistance(vx, vy,
|
||||||
|
arc.Center.X, arc.Center.Y, arc.Radius,
|
||||||
|
arc.StartAngle, arc.EndAngle, arc.IsReversed,
|
||||||
|
dirX, dirY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity is Circle circle)
|
||||||
|
{
|
||||||
|
return RayCircleDistance(vx, vy,
|
||||||
|
circle.Center.X, circle.Center.Y, circle.Radius,
|
||||||
|
dirX, dirY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return double.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector[] ExtractEntityVertices(List<Entity> entities)
|
||||||
|
{
|
||||||
|
var vertices = new HashSet<Vector>();
|
||||||
|
|
||||||
|
for (var i = 0; i < entities.Count; i++)
|
||||||
|
{
|
||||||
|
var entity = entities[i];
|
||||||
|
|
||||||
|
if (entity is Line line)
|
||||||
|
{
|
||||||
|
vertices.Add(line.pt1);
|
||||||
|
vertices.Add(line.pt2);
|
||||||
|
}
|
||||||
|
else if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
vertices.Add(arc.StartPoint());
|
||||||
|
vertices.Add(arc.EndPoint());
|
||||||
|
AddArcExtremeVertices(vertices, arc);
|
||||||
|
}
|
||||||
|
else if (entity is Circle circle)
|
||||||
|
{
|
||||||
|
vertices.Add(new Vector(circle.Center.X + circle.Radius, circle.Center.Y));
|
||||||
|
vertices.Add(new Vector(circle.Center.X - circle.Radius, circle.Center.Y));
|
||||||
|
vertices.Add(new Vector(circle.Center.X, circle.Center.Y + circle.Radius));
|
||||||
|
vertices.Add(new Vector(circle.Center.X, circle.Center.Y - circle.Radius));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vertices.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddArcExtremeVertices(HashSet<Vector> points, Arc arc)
|
||||||
|
{
|
||||||
|
var a1 = arc.StartAngle;
|
||||||
|
var a2 = arc.EndAngle;
|
||||||
|
|
||||||
|
if (arc.IsReversed)
|
||||||
|
Generic.Swap(ref a1, ref a2);
|
||||||
|
|
||||||
|
if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y));
|
||||||
|
if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius));
|
||||||
|
if (Angle.IsBetweenRad(System.Math.PI, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y));
|
||||||
|
if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<Vector> CollectVertices(List<Line> lines, Vector offset)
|
||||||
|
{
|
||||||
|
return CollectVertices(ToEdgeArray(lines), offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
|
||||||
|
{
|
||||||
|
var vertices = new HashSet<Vector>();
|
||||||
|
for (var i = 0; i < edges.Length; i++)
|
||||||
|
{
|
||||||
|
vertices.Add(edges[i].start + offset);
|
||||||
|
vertices.Add(edges[i].end + offset);
|
||||||
|
}
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Vector start, Vector end)[] ToEdgeArray(List<Line> lines)
|
||||||
|
{
|
||||||
|
var edges = new (Vector start, Vector end)[lines.Count];
|
||||||
|
for (var i = 0; i < lines.Count; i++)
|
||||||
|
edges[i] = (lines[i].pt1, lines[i].pt2);
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SortEdgesForPruning((Vector start, Vector end)[] edges, PushDirection direction)
|
||||||
|
{
|
||||||
|
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||||
|
System.Array.Sort(edges, (a, b) =>
|
||||||
|
System.Math.Min(a.start.Y, a.end.Y).CompareTo(System.Math.Min(b.start.Y, b.end.Y)));
|
||||||
|
else
|
||||||
|
System.Array.Sort(edges, (a, b) =>
|
||||||
|
System.Math.Min(a.start.X, a.end.X).CompareTo(System.Math.Min(b.start.X, b.end.X)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetCurveParams(Entity entity, out double cx, out double cy, out double r)
|
||||||
|
{
|
||||||
|
if (entity is Circle circle)
|
||||||
|
{
|
||||||
|
cx = circle.Center.X; cy = circle.Center.Y; r = circle.Radius;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
cx = arc.Center.X; cy = arc.Center.Y; r = arc.Radius;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
cx = cy = r = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static double BoxProjectionMin(Box box, double dx, double dy)
|
private static double BoxProjectionMin(Box box, double dx, double dy)
|
||||||
{
|
{
|
||||||
var x = dx >= 0 ? box.Left : box.Right;
|
var x = dx >= 0 ? box.Left : box.Right;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public interface IMaterialProvidingPostProcessor
|
||||||
|
{
|
||||||
|
IEnumerable<string> GetMaterialNames();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public interface IPostProcessorNestAware
|
||||||
|
{
|
||||||
|
void PrepareForNest(Nest nest);
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-2
@@ -62,10 +62,15 @@ namespace OpenNest
|
|||||||
public CNC.CuttingStrategy.CuttingParameters CuttingParameters { get; set; }
|
public CNC.CuttingStrategy.CuttingParameters CuttingParameters { get; set; }
|
||||||
|
|
||||||
public void ApplyLeadIns(CNC.CuttingStrategy.CuttingParameters parameters, Vector approachPoint)
|
public void ApplyLeadIns(CNC.CuttingStrategy.CuttingParameters parameters, Vector approachPoint)
|
||||||
|
{
|
||||||
|
ApplyLeadIns(parameters, approachPoint, Geometry.Vector.Invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyLeadIns(CNC.CuttingStrategy.CuttingParameters parameters, Vector approachPoint, Vector nextPartStart)
|
||||||
{
|
{
|
||||||
preLeadInRotation = Rotation;
|
preLeadInRotation = Rotation;
|
||||||
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy { Parameters = parameters };
|
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy { Parameters = parameters };
|
||||||
var result = strategy.Apply(Program, approachPoint);
|
var result = strategy.Apply(Program, approachPoint, nextPartStart);
|
||||||
Program = result.Program;
|
Program = result.Program;
|
||||||
CuttingParameters = parameters;
|
CuttingParameters = parameters;
|
||||||
HasManualLeadIns = true;
|
HasManualLeadIns = true;
|
||||||
@@ -190,7 +195,14 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var rotation = Rotation;
|
var rotation = Rotation;
|
||||||
Program = BaseDrawing.Program.Clone() as Program;
|
Program = BaseDrawing.Program.Clone() as Program;
|
||||||
Program.Rotate(Program.Rotation - rotation);
|
|
||||||
|
if (!Math.Tolerance.IsEqualTo(rotation, 0))
|
||||||
|
Program.Rotate(rotation);
|
||||||
|
|
||||||
|
HasManualLeadIns = false;
|
||||||
|
LeadInsLocked = false;
|
||||||
|
CuttingParameters = null;
|
||||||
|
UpdateBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -61,6 +61,91 @@ namespace OpenNest
|
|||||||
return offsetShape.Entities;
|
return offsetShape.Entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all entities (perimeter + cutouts) with spacing offset applied,
|
||||||
|
/// without tessellation. Perimeter is offset outward, cutouts inward.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Entity> GetOffsetPartEntities(Part part, double spacing)
|
||||||
|
{
|
||||||
|
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var profile = new ShapeProfile(
|
||||||
|
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
var entities = new List<Entity>();
|
||||||
|
|
||||||
|
var perimeter = profile.Perimeter.OffsetOutward(spacing);
|
||||||
|
if (perimeter != null)
|
||||||
|
{
|
||||||
|
foreach (var entity in perimeter.Entities)
|
||||||
|
entity.Offset(part.Location);
|
||||||
|
entities.AddRange(perimeter.Entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
{
|
||||||
|
var inset = cutout.OffsetInward(spacing);
|
||||||
|
if (inset == null) continue;
|
||||||
|
foreach (var entity in inset.Entities)
|
||||||
|
entity.Offset(part.Location);
|
||||||
|
entities.AddRange(inset.Entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns perimeter entities at the part's world location, without tessellation
|
||||||
|
/// or spacing offset.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Entity> GetPerimeterEntities(Part part)
|
||||||
|
{
|
||||||
|
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var profile = new ShapeProfile(
|
||||||
|
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
|
||||||
|
return CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all entities (perimeter + cutouts) at the part's world location,
|
||||||
|
/// without tessellation or spacing offset.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Entity> GetPartEntities(Part part)
|
||||||
|
{
|
||||||
|
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var profile = new ShapeProfile(
|
||||||
|
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
var entities = CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
|
||||||
|
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
entities.AddRange(CopyEntitiesAtLocation(cutout.Entities, part.Location));
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Entity> CopyEntitiesAtLocation(List<Entity> source, Vector location)
|
||||||
|
{
|
||||||
|
var result = new List<Entity>(source.Count);
|
||||||
|
|
||||||
|
for (var i = 0; i < source.Count; i++)
|
||||||
|
{
|
||||||
|
var entity = source[i];
|
||||||
|
Entity copy;
|
||||||
|
|
||||||
|
if (entity is Line line)
|
||||||
|
copy = new Line(line.StartPoint + location, line.EndPoint + location);
|
||||||
|
else if (entity is Arc arc)
|
||||||
|
copy = new Arc(arc.Center + location, arc.Radius, arc.StartAngle, arc.EndAngle, arc.IsReversed);
|
||||||
|
else if (entity is Circle circle)
|
||||||
|
copy = new Circle(circle.Center + location, circle.Radius);
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
|
||||||
|
result.Add(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public static List<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001,
|
public static List<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001,
|
||||||
bool perimeterOnly = false)
|
bool perimeterOnly = false)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ namespace OpenNest.Shapes
|
|||||||
{
|
{
|
||||||
public double Diameter { get; set; }
|
public double Diameter { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
Diameter = 8;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var entities = new List<Entity>
|
var entities = new List<Entity>
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ namespace OpenNest.Shapes
|
|||||||
public double HolePatternDiameter { get; set; }
|
public double HolePatternDiameter { get; set; }
|
||||||
public int HoleCount { get; set; }
|
public int HoleCount { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
NominalPipeSize = 2;
|
||||||
|
OD = 7.5;
|
||||||
|
HoleDiameter = 0.875;
|
||||||
|
HolePatternDiameter = 5.5;
|
||||||
|
HoleCount = 8;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var entities = new List<Entity>();
|
var entities = new List<Entity>();
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
|
|||||||
public double Base { get; set; }
|
public double Base { get; set; }
|
||||||
public double Height { get; set; }
|
public double Height { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
Base = 8;
|
||||||
|
Height = 10;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var midX = Base / 2.0;
|
var midX = Base / 2.0;
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ namespace OpenNest.Shapes
|
|||||||
public double LegWidth { get; set; }
|
public double LegWidth { get; set; }
|
||||||
public double LegHeight { get; set; }
|
public double LegHeight { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
Width = 8;
|
||||||
|
Height = 10;
|
||||||
|
LegWidth = 3;
|
||||||
|
LegHeight = 3;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var lw = LegWidth > 0 ? LegWidth : Width / 2.0;
|
var lw = LegWidth > 0 ? LegWidth : Width / 2.0;
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ namespace OpenNest.Shapes
|
|||||||
{
|
{
|
||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
Width = 8;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var center = Width / 2.0;
|
var center = Width / 2.0;
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
|
|||||||
public double Length { get; set; }
|
public double Length { get; set; }
|
||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
Length = 12;
|
||||||
|
Width = 6;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var entities = new List<Entity>
|
var entities = new List<Entity>
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
|
|||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
public double Height { get; set; }
|
public double Height { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
Width = 8;
|
||||||
|
Height = 6;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var entities = new List<Entity>
|
var entities = new List<Entity>
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
|
|||||||
public double OuterDiameter { get; set; }
|
public double OuterDiameter { get; set; }
|
||||||
public double InnerDiameter { get; set; }
|
public double InnerDiameter { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
OuterDiameter = 10;
|
||||||
|
InnerDiameter = 6;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var entities = new List<Entity>
|
var entities = new List<Entity>
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ namespace OpenNest.Shapes
|
|||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
public double Radius { get; set; }
|
public double Radius { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
Length = 12;
|
||||||
|
Width = 6;
|
||||||
|
Radius = 1;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var r = Radius;
|
var r = Radius;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ namespace OpenNest.Shapes
|
|||||||
|
|
||||||
public abstract Drawing GetDrawing();
|
public abstract Drawing GetDrawing();
|
||||||
|
|
||||||
|
public virtual void SetPreviewDefaults() { }
|
||||||
|
|
||||||
public static List<T> LoadFromJson<T>(string path) where T : ShapeDefinition
|
public static List<T> LoadFromJson<T>(string path) where T : ShapeDefinition
|
||||||
{
|
{
|
||||||
var json = File.ReadAllText(path);
|
var json = File.ReadAllText(path);
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ namespace OpenNest.Shapes
|
|||||||
public double StemWidth { get; set; }
|
public double StemWidth { get; set; }
|
||||||
public double BarHeight { get; set; }
|
public double BarHeight { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
Width = 10;
|
||||||
|
Height = 8;
|
||||||
|
StemWidth = 3;
|
||||||
|
BarHeight = 3;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var sw = StemWidth > 0 ? StemWidth : Width / 3.0;
|
var sw = StemWidth > 0 ? StemWidth : Width / 3.0;
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ namespace OpenNest.Shapes
|
|||||||
public double BottomWidth { get; set; }
|
public double BottomWidth { get; set; }
|
||||||
public double Height { get; set; }
|
public double Height { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
TopWidth = 6;
|
||||||
|
BottomWidth = 10;
|
||||||
|
Height = 6;
|
||||||
|
}
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var offset = (BottomWidth - TopWidth) / 2.0;
|
var offset = (BottomWidth - TopWidth) / 2.0;
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ namespace OpenNest.Engine.BestFit
|
|||||||
if (!result.Keep)
|
if (!result.Keep)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (result.ShortestSide > System.Math.Min(MaxPlateWidth, MaxPlateHeight))
|
if (result.ShortestSide > System.Math.Min(MaxPlateWidth, MaxPlateHeight) ||
|
||||||
|
result.LongestSide > System.Math.Max(MaxPlateWidth, MaxPlateHeight))
|
||||||
{
|
{
|
||||||
result.Keep = false;
|
result.Keep = false;
|
||||||
result.Reason = "Exceeds plate dimensions";
|
result.Reason = "Exceeds plate dimensions";
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ namespace OpenNest.Engine.BestFit
|
|||||||
var allMovingVerts = ExtractVerticesFromEntities(movingEntities);
|
var allMovingVerts = ExtractVerticesFromEntities(movingEntities);
|
||||||
var allStationaryVerts = ExtractVerticesFromEntities(stationaryEntities);
|
var allStationaryVerts = ExtractVerticesFromEntities(stationaryEntities);
|
||||||
|
|
||||||
|
var movingCurves = ExtractCurveParams(movingEntities);
|
||||||
|
var stationaryCurves = ExtractCurveParams(stationaryEntities);
|
||||||
|
|
||||||
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
||||||
|
|
||||||
foreach (var offset in offsets)
|
foreach (var offset in offsets)
|
||||||
@@ -165,12 +168,84 @@ namespace OpenNest.Engine.BestFit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 3: Curve-to-curve direct distance.
|
||||||
|
// Vertex sampling misses the true contact between two curved entities
|
||||||
|
// when the approach angle doesn't align with a sampled vertex.
|
||||||
|
for (var m = 0; m < movingCurves.Length; m++)
|
||||||
|
{
|
||||||
|
var mc = movingCurves[m];
|
||||||
|
var mcx = mc.Cx + offset.Dx;
|
||||||
|
var mcy = mc.Cy + offset.Dy;
|
||||||
|
|
||||||
|
for (var s = 0; s < stationaryCurves.Length; s++)
|
||||||
|
{
|
||||||
|
var sc = stationaryCurves[s];
|
||||||
|
var d = SpatialQuery.RayCircleDistance(
|
||||||
|
mcx, mcy, sc.Cx, sc.Cy, mc.Radius + sc.Radius, dirX, dirY);
|
||||||
|
|
||||||
|
if (d >= minDist || d == double.MaxValue)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (mc.Entity is Arc || sc.Entity is Arc)
|
||||||
|
{
|
||||||
|
var mx = mcx + d * dirX;
|
||||||
|
var my = mcy + d * dirY;
|
||||||
|
var toCx = sc.Cx - mx;
|
||||||
|
var toCy = sc.Cy - my;
|
||||||
|
|
||||||
|
if (mc.Entity is Arc mArc)
|
||||||
|
{
|
||||||
|
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
||||||
|
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sc.Entity is Arc sArc)
|
||||||
|
{
|
||||||
|
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
|
||||||
|
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minDist = d;
|
||||||
|
if (d <= 0) { results[i] = 0; return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
results[i] = minDist;
|
results[i] = minDist;
|
||||||
});
|
});
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly struct CurveParams
|
||||||
|
{
|
||||||
|
public readonly Entity Entity;
|
||||||
|
public readonly double Cx, Cy, Radius;
|
||||||
|
|
||||||
|
public CurveParams(Entity entity, double cx, double cy, double radius)
|
||||||
|
{
|
||||||
|
Entity = entity;
|
||||||
|
Cx = cx;
|
||||||
|
Cy = cy;
|
||||||
|
Radius = radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CurveParams[] ExtractCurveParams(List<Entity> entities)
|
||||||
|
{
|
||||||
|
var curves = new List<CurveParams>();
|
||||||
|
for (var i = 0; i < entities.Count; i++)
|
||||||
|
{
|
||||||
|
if (entities[i] is Circle circle)
|
||||||
|
curves.Add(new CurveParams(circle, circle.Center.X, circle.Center.Y, circle.Radius));
|
||||||
|
else if (entities[i] is Arc arc)
|
||||||
|
curves.Add(new CurveParams(arc, arc.Center.X, arc.Center.Y, arc.Radius));
|
||||||
|
}
|
||||||
|
return curves.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
private static double RayEntityDistance(
|
private static double RayEntityDistance(
|
||||||
double vx, double vy, Entity entity,
|
double vx, double vy, Entity entity,
|
||||||
double entityOffsetX, double entityOffsetY,
|
double entityOffsetX, double entityOffsetY,
|
||||||
|
|||||||
@@ -15,11 +15,18 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
public List<BestFitResult> EvaluateAll(List<PairCandidate> candidates)
|
public List<BestFitResult> EvaluateAll(List<PairCandidate> candidates)
|
||||||
{
|
{
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
return new List<BestFitResult>();
|
||||||
|
|
||||||
|
// Build a perimeter-only drawing once — all candidates share the same drawing.
|
||||||
|
// This avoids cloning the full program (with all cutouts) for every candidate.
|
||||||
|
var perimeterDrawing = CreatePerimeterDrawing(candidates[0].Drawing);
|
||||||
|
|
||||||
var resultBag = new ConcurrentBag<BestFitResult>();
|
var resultBag = new ConcurrentBag<BestFitResult>();
|
||||||
|
|
||||||
Parallel.ForEach(candidates, c =>
|
Parallel.ForEach(candidates, c =>
|
||||||
{
|
{
|
||||||
resultBag.Add(Evaluate(c));
|
resultBag.Add(Evaluate(c, perimeterDrawing));
|
||||||
});
|
});
|
||||||
|
|
||||||
return resultBag.ToList();
|
return resultBag.ToList();
|
||||||
@@ -27,18 +34,24 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
public BestFitResult Evaluate(PairCandidate candidate)
|
public BestFitResult Evaluate(PairCandidate candidate)
|
||||||
{
|
{
|
||||||
var drawing = candidate.Drawing;
|
var perimeterDrawing = CreatePerimeterDrawing(candidate.Drawing);
|
||||||
|
return Evaluate(candidate, perimeterDrawing);
|
||||||
|
}
|
||||||
|
|
||||||
var part1 = Part.CreateAtOrigin(drawing);
|
private BestFitResult Evaluate(PairCandidate candidate, Drawing perimeterDrawing)
|
||||||
|
{
|
||||||
|
var part1 = Part.CreateAtOrigin(perimeterDrawing);
|
||||||
|
|
||||||
var part2 = Part.CreateAtOrigin(drawing, candidate.Part2Rotation);
|
var part2 = Part.CreateAtOrigin(perimeterDrawing, candidate.Part2Rotation);
|
||||||
part2.Location = candidate.Part2Offset;
|
part2.Location = candidate.Part2Offset;
|
||||||
part2.UpdateBounds();
|
part2.UpdateBounds();
|
||||||
|
|
||||||
// Check overlap via shape intersection
|
// Overlap check — perimeter vs perimeter
|
||||||
var overlaps = CheckOverlap(part1, part2);
|
var shape1 = GetPerimeterShape(part1);
|
||||||
|
var shape2 = GetPerimeterShape(part2);
|
||||||
|
var overlaps = shape1 != null && shape2 != null && shape1.Intersects(shape2, out _);
|
||||||
|
|
||||||
// Collect all polygon vertices for convex hull / optimal rotation
|
// Convex hull vertices from perimeter polygons only
|
||||||
var allPoints = GetPartVertices(part1);
|
var allPoints = GetPartVertices(part1);
|
||||||
allPoints.AddRange(GetPartVertices(part2));
|
allPoints.AddRange(GetPartVertices(part2));
|
||||||
|
|
||||||
@@ -66,7 +79,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
hullAngles = new List<double> { 0 };
|
hullAngles = new List<double> { 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
var trueArea = drawing.Area * 2;
|
var trueArea = candidate.Drawing.Area * 2;
|
||||||
|
|
||||||
// Normalize to landscape (width >= height) for consistent display.
|
// Normalize to landscape (width >= height) for consistent display.
|
||||||
if (bestHeight > bestWidth)
|
if (bestHeight > bestWidth)
|
||||||
@@ -91,38 +104,29 @@ namespace OpenNest.Engine.BestFit
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CheckOverlap(Part part1, Part part2)
|
private static Drawing CreatePerimeterDrawing(Drawing source)
|
||||||
{
|
{
|
||||||
var shapes1 = GetPartShapes(part1);
|
var entities = ConvertProgram.ToGeometry(source.Program)
|
||||||
var shapes2 = GetPartShapes(part2);
|
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||||
|
var profile = new ShapeProfile(entities);
|
||||||
for (var i = 0; i < shapes1.Count; i++)
|
var program = ConvertGeometry.ToProgram(profile.Perimeter);
|
||||||
{
|
return new Drawing(source.Name, program);
|
||||||
for (var j = 0; j < shapes2.Count; j++)
|
|
||||||
{
|
|
||||||
List<Vector> pts;
|
|
||||||
|
|
||||||
if (shapes1[i].Intersects(shapes2[j], out pts))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Shape> GetPartShapes(Part part)
|
private static Shape GetPerimeterShape(Part part)
|
||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||||
var shapes = ShapeBuilder.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
shapes.ForEach(s => s.Offset(part.Location));
|
if (shapes.Count == 0) return null;
|
||||||
return shapes;
|
shapes[0].Offset(part.Location);
|
||||||
|
return shapes[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Vector> GetPartVertices(Part part)
|
private static List<Vector> GetPartVertices(Part part)
|
||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||||
var shapes = ShapeBuilder.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
var points = new List<Vector>();
|
var points = new List<Vector>();
|
||||||
|
|
||||||
@@ -130,9 +134,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
{
|
{
|
||||||
var polygon = shape.ToPolygonWithTolerance(ChordTolerance);
|
var polygon = shape.ToPolygonWithTolerance(ChordTolerance);
|
||||||
polygon.Offset(part.Location);
|
polygon.Offset(part.Location);
|
||||||
|
points.AddRange(polygon.Vertices);
|
||||||
foreach (var vertex in polygon.Vertices)
|
|
||||||
points.Add(vertex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return points;
|
return points;
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ namespace OpenNest.Engine.Fill
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Compactor
|
public static class Compactor
|
||||||
{
|
{
|
||||||
private const double ChordTolerance = 0.001;
|
|
||||||
|
|
||||||
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||||
{
|
{
|
||||||
var obstacleParts = plate.Parts
|
var obstacleParts = plate.Parts
|
||||||
@@ -44,7 +42,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
var opposite = -direction;
|
var opposite = -direction;
|
||||||
|
|
||||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||||
var obstacleLines = new List<Line>[obstacleParts.Count];
|
var obstacleEntities = new List<Entity>[obstacleParts.Count];
|
||||||
|
|
||||||
for (var i = 0; i < obstacleParts.Count; i++)
|
for (var i = 0; i < obstacleParts.Count; i++)
|
||||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||||
@@ -61,7 +59,19 @@ namespace OpenNest.Engine.Fill
|
|||||||
distance = edgeDist;
|
distance = edgeDist;
|
||||||
|
|
||||||
var movingBox = moving.BoundingBox;
|
var movingBox = moving.BoundingBox;
|
||||||
List<Line> movingLines = null;
|
List<Entity> movingEntities = null;
|
||||||
|
|
||||||
|
// Check if any obstacle is inside the moving part — only then
|
||||||
|
// do we need cutout entities on the moving part.
|
||||||
|
var needCutouts = false;
|
||||||
|
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||||
|
{
|
||||||
|
if (movingBox.Contains(obstacleBoxes[i]))
|
||||||
|
{
|
||||||
|
needCutouts = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -76,15 +86,19 @@ namespace OpenNest.Engine.Fill
|
|||||||
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
|
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
movingLines ??= halfSpacing > 0
|
movingEntities ??= halfSpacing > 0
|
||||||
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
|
? (needCutouts
|
||||||
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
|
? PartGeometry.GetOffsetPartEntities(moving, halfSpacing)
|
||||||
|
: PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing))
|
||||||
|
: (needCutouts
|
||||||
|
? PartGeometry.GetPartEntities(moving)
|
||||||
|
: PartGeometry.GetPerimeterEntities(moving));
|
||||||
|
|
||||||
obstacleLines[i] ??= halfSpacing > 0
|
obstacleEntities[i] ??= halfSpacing > 0
|
||||||
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
|
? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing)
|
||||||
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
|
: PartGeometry.GetPerimeterEntities(obstacleParts[i]);
|
||||||
|
|
||||||
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
|
var d = SpatialQuery.DirectionalDistance(movingEntities, obstacleEntities[i], direction);
|
||||||
if (d < distance)
|
if (d < distance)
|
||||||
distance = d;
|
distance = d;
|
||||||
}
|
}
|
||||||
@@ -157,7 +171,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||||
var d = gap - partSpacing - 2 * ChordTolerance;
|
var d = gap - partSpacing - 0.002;
|
||||||
if (d < 0) d = 0;
|
if (d < 0) d = 0;
|
||||||
if (d < distance)
|
if (d < distance)
|
||||||
distance = d;
|
distance = d;
|
||||||
|
|||||||
@@ -119,10 +119,11 @@ namespace OpenNest.Engine.Fill
|
|||||||
var maxCopyDistance = FindMaxPairDistance(
|
var maxCopyDistance = FindMaxPairDistance(
|
||||||
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
|
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
|
||||||
|
|
||||||
if (maxCopyDistance < Tolerance.Epsilon)
|
// The copy distance must be at least bboxDim + PartSpacing to prevent
|
||||||
return bboxDim + PartSpacing;
|
// bounding box overlap. Cross-pair slides can underestimate when the
|
||||||
|
// circumscribed polygon boundary overshoots the true arc, creating
|
||||||
return maxCopyDistance;
|
// spurious contacts between diagonal parts in adjacent copies.
|
||||||
|
return System.Math.Max(maxCopyDistance, bboxDim + PartSpacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
using OpenNest.Engine.Sequencing;
|
using OpenNest.Engine.Sequencing;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenNest.Engine
|
namespace OpenNest.Engine
|
||||||
@@ -15,14 +17,28 @@ namespace OpenNest.Engine
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate);
|
var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
var currentPoint = PlateHelper.GetExitPoint(plate);
|
var exitPoint = PlateHelper.GetExitPoint(plate);
|
||||||
|
|
||||||
foreach (var sp in sequenced)
|
// Pass 1: assign lead-ins to establish pierce points
|
||||||
|
var piercePoints = AssignPass(sequenced, parameters, exitPoint, nextPiercePoints: null);
|
||||||
|
|
||||||
|
// Pass 2: re-assign with knowledge of next part's start point
|
||||||
|
AssignPass(sequenced, parameters, exitPoint, nextPiercePoints: piercePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector[] AssignPass(List<SequencedPart> sequenced, CuttingParameters parameters,
|
||||||
|
Vector exitPoint, Vector[] nextPiercePoints)
|
||||||
|
{
|
||||||
|
var piercePoints = new Vector[sequenced.Count];
|
||||||
|
var currentPoint = exitPoint;
|
||||||
|
|
||||||
|
for (var i = 0; i < sequenced.Count; i++)
|
||||||
{
|
{
|
||||||
var part = sp.Part;
|
var part = sequenced[i].Part;
|
||||||
|
|
||||||
if (part.LeadInsLocked)
|
if (part.LeadInsLocked)
|
||||||
{
|
{
|
||||||
|
piercePoints[i] = GetPiercePoint(part);
|
||||||
currentPoint = part.Location;
|
currentPoint = part.Location;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -31,10 +47,33 @@ namespace OpenNest.Engine
|
|||||||
part.RemoveLeadIns();
|
part.RemoveLeadIns();
|
||||||
|
|
||||||
var localApproach = currentPoint - part.Location;
|
var localApproach = currentPoint - part.Location;
|
||||||
part.ApplyLeadIns(parameters, localApproach);
|
|
||||||
|
|
||||||
|
if (nextPiercePoints != null && i + 1 < sequenced.Count)
|
||||||
|
{
|
||||||
|
var nextStart = nextPiercePoints[i + 1] - part.Location;
|
||||||
|
part.ApplyLeadIns(parameters, localApproach, nextStart);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
part.ApplyLeadIns(parameters, localApproach);
|
||||||
|
}
|
||||||
|
|
||||||
|
piercePoints[i] = GetPiercePoint(part);
|
||||||
currentPoint = part.Location;
|
currentPoint = part.Location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return piercePoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector GetPiercePoint(Part part)
|
||||||
|
{
|
||||||
|
foreach (var code in part.Program.Codes)
|
||||||
|
{
|
||||||
|
if (code is CNC.Motion motion)
|
||||||
|
return motion.EndPoint + part.Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
return part.Location;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+224
-160
@@ -19,22 +19,27 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
private readonly Plate _template;
|
private readonly Plate _template;
|
||||||
private readonly List<PlateOption> _plateOptions;
|
private readonly List<PlateOption> _plateOptions;
|
||||||
|
private readonly List<PlateOption> _sortedOptions;
|
||||||
private readonly double _salvageRate;
|
private readonly double _salvageRate;
|
||||||
private readonly double _minRemnantSize;
|
private readonly double _minRemnantSize;
|
||||||
private readonly List<PlateResult> _platePool;
|
private readonly List<PlateResult> _platePool;
|
||||||
private readonly IProgress<NestProgress> _progress;
|
private readonly IProgress<NestProgress> _progress;
|
||||||
private readonly CancellationToken _token;
|
private readonly CancellationToken _token;
|
||||||
|
private readonly MultiPlateNestOptions _options;
|
||||||
|
|
||||||
|
private bool HasPlateOptions => _plateOptions != null && _plateOptions.Count > 0;
|
||||||
|
|
||||||
private MultiPlateNester(
|
private MultiPlateNester(
|
||||||
Plate template, List<PlateOption> plateOptions,
|
MultiPlateNestOptions options,
|
||||||
double salvageRate, double minRemnantSize,
|
|
||||||
List<Plate> existingPlates,
|
List<Plate> existingPlates,
|
||||||
IProgress<NestProgress> progress, CancellationToken token)
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
{
|
{
|
||||||
_template = template;
|
_options = options;
|
||||||
_plateOptions = plateOptions;
|
_template = options.Template;
|
||||||
_salvageRate = salvageRate;
|
_plateOptions = options.PlateOptions;
|
||||||
_minRemnantSize = minRemnantSize;
|
_sortedOptions = options.PlateOptions?.OrderBy(o => o.Cost).ToList();
|
||||||
|
_salvageRate = options.SalvageRate;
|
||||||
|
_minRemnantSize = options.MinRemnantSize;
|
||||||
_platePool = InitializePlatePool(existingPlates);
|
_platePool = InitializePlatePool(existingPlates);
|
||||||
_progress = progress;
|
_progress = progress;
|
||||||
_token = token;
|
_token = token;
|
||||||
@@ -42,26 +47,31 @@ namespace OpenNest
|
|||||||
|
|
||||||
// --- Static Utility Methods ---
|
// --- Static Utility Methods ---
|
||||||
|
|
||||||
|
public static bool FitsBounds(Box container, Box part)
|
||||||
|
{
|
||||||
|
var fitsNormal = container.Width >= part.Width - Tolerance.Epsilon
|
||||||
|
&& container.Length >= part.Length - Tolerance.Epsilon;
|
||||||
|
var fitsRotated = container.Width >= part.Length - Tolerance.Epsilon
|
||||||
|
&& container.Length >= part.Width - Tolerance.Epsilon;
|
||||||
|
return fitsNormal || fitsRotated;
|
||||||
|
}
|
||||||
|
|
||||||
public static List<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder)
|
public static List<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder)
|
||||||
{
|
{
|
||||||
|
var withBounds = items.Select(i => (Item: i, Bounds: i.Drawing.Program.BoundingBox())).ToList();
|
||||||
|
|
||||||
switch (sortOrder)
|
switch (sortOrder)
|
||||||
{
|
{
|
||||||
case PartSortOrder.BoundingBoxArea:
|
case PartSortOrder.BoundingBoxArea:
|
||||||
return items
|
return withBounds
|
||||||
.OrderByDescending(i =>
|
.OrderByDescending(x => x.Bounds.Width * x.Bounds.Length)
|
||||||
{
|
.Select(x => x.Item)
|
||||||
var bb = i.Drawing.Program.BoundingBox();
|
|
||||||
return bb.Width * bb.Length;
|
|
||||||
})
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
case PartSortOrder.Size:
|
case PartSortOrder.Size:
|
||||||
return items
|
return withBounds
|
||||||
.OrderByDescending(i =>
|
.OrderByDescending(x => System.Math.Max(x.Bounds.Width, x.Bounds.Length))
|
||||||
{
|
.Select(x => x.Item)
|
||||||
var bb = i.Drawing.Program.BoundingBox();
|
|
||||||
return System.Math.Max(bb.Width, bb.Length);
|
|
||||||
})
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -126,15 +136,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
foreach (var option in sorted)
|
foreach (var option in sorted)
|
||||||
{
|
{
|
||||||
var workW = option.Width - template.EdgeSpacing.Left - template.EdgeSpacing.Right;
|
if (FitsBounds(OptionWorkArea(option, template), minBounds))
|
||||||
var workL = option.Length - template.EdgeSpacing.Top - template.EdgeSpacing.Bottom;
|
|
||||||
|
|
||||||
var fitsNormal = workW >= minBounds.Width - Tolerance.Epsilon
|
|
||||||
&& workL >= minBounds.Length - Tolerance.Epsilon;
|
|
||||||
var fitsRotated = workW >= minBounds.Length - Tolerance.Epsilon
|
|
||||||
&& workL >= minBounds.Width - Tolerance.Epsilon;
|
|
||||||
|
|
||||||
if (fitsNormal || fitsRotated)
|
|
||||||
{
|
{
|
||||||
plate.Size = new Size(option.Width, option.Length);
|
plate.Size = new Size(option.Width, option.Length);
|
||||||
return plate;
|
return plate;
|
||||||
@@ -170,32 +172,47 @@ namespace OpenNest
|
|||||||
|
|
||||||
public static MultiPlateResult Nest(
|
public static MultiPlateResult Nest(
|
||||||
List<NestItem> items,
|
List<NestItem> items,
|
||||||
Plate template,
|
MultiPlateNestOptions options,
|
||||||
List<PlateOption> plateOptions,
|
List<Plate> existingPlates = null,
|
||||||
double salvageRate,
|
IProgress<NestProgress> progress = null,
|
||||||
PartSortOrder sortOrder,
|
CancellationToken token = default)
|
||||||
double minRemnantSize,
|
|
||||||
bool allowPlateCreation,
|
|
||||||
List<Plate> existingPlates,
|
|
||||||
IProgress<NestProgress> progress,
|
|
||||||
CancellationToken token)
|
|
||||||
{
|
{
|
||||||
var nester = new MultiPlateNester(template, plateOptions, salvageRate,
|
var nester = new MultiPlateNester(options, existingPlates, progress, token);
|
||||||
minRemnantSize, existingPlates, progress, token);
|
return nester.Run(items, options.SortOrder, options.AllowPlateCreation);
|
||||||
return nester.Run(items, sortOrder, allowPlateCreation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Private Helpers ---
|
// --- Private Helpers ---
|
||||||
|
|
||||||
|
private static Box OptionWorkArea(PlateOption option, Plate template)
|
||||||
|
{
|
||||||
|
var w = option.Width - template.EdgeSpacing.Left - template.EdgeSpacing.Right;
|
||||||
|
var h = option.Length - template.EdgeSpacing.Top - template.EdgeSpacing.Bottom;
|
||||||
|
return new Box(0, 0, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
private static double ScoreZone(Box zone, Box partBounds)
|
private static double ScoreZone(Box zone, Box partBounds)
|
||||||
{
|
{
|
||||||
var fitsNormal = zone.Length >= partBounds.Length && zone.Width >= partBounds.Width;
|
if (!FitsBounds(zone, partBounds))
|
||||||
var fitsRotated = zone.Length >= partBounds.Width && zone.Width >= partBounds.Length;
|
|
||||||
|
|
||||||
if (!fitsNormal && !fitsRotated)
|
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
return (partBounds.Length * partBounds.Width) / zone.Area();
|
var cols = (int)(zone.Width / partBounds.Width);
|
||||||
|
var rows = (int)(zone.Length / partBounds.Length);
|
||||||
|
var colsR = (int)(zone.Width / partBounds.Length);
|
||||||
|
var rowsR = (int)(zone.Length / partBounds.Width);
|
||||||
|
var estimatedCount = System.Math.Max(cols * rows, colsR * rowsR);
|
||||||
|
|
||||||
|
var utilization = (estimatedCount * partBounds.Width * partBounds.Length) / zone.Area();
|
||||||
|
|
||||||
|
var zoneAspect = zone.Width / zone.Length;
|
||||||
|
var partAspect = partBounds.Width / partBounds.Length;
|
||||||
|
var aspectMatch = System.Math.Min(zoneAspect, partAspect) / System.Math.Max(zoneAspect, partAspect);
|
||||||
|
|
||||||
|
return utilization * 0.7 + aspectMatch * 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DecrementQuantity(NestItem item, int placed)
|
||||||
|
{
|
||||||
|
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int FillAndPlace(PlateResult pr, Box zone, NestItem item)
|
private int FillAndPlace(PlateResult pr, Box zone, NestItem item)
|
||||||
@@ -206,9 +223,8 @@ namespace OpenNest
|
|||||||
|
|
||||||
if (parts.Count > 0)
|
if (parts.Count > 0)
|
||||||
{
|
{
|
||||||
pr.Plate.Parts.AddRange(parts);
|
pr.AddParts(parts);
|
||||||
pr.Parts.AddRange(parts);
|
DecrementQuantity(item, parts.Count);
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.Count;
|
return parts.Count;
|
||||||
@@ -218,7 +234,7 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var pr = new PlateResult { Plate = plate, IsNew = true };
|
var pr = new PlateResult { Plate = plate, IsNew = true };
|
||||||
|
|
||||||
if (_plateOptions != null)
|
if (HasPlateOptions)
|
||||||
{
|
{
|
||||||
pr.ChosenSize = _plateOptions.FirstOrDefault(o =>
|
pr.ChosenSize = _plateOptions.FirstOrDefault(o =>
|
||||||
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length));
|
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length));
|
||||||
@@ -253,6 +269,29 @@ namespace OpenNest
|
|||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryWithUpgradedSize(PlateResult pr, PlateOption upgradeOption, Func<List<Box>, bool> tryFill)
|
||||||
|
{
|
||||||
|
var oldSize = pr.Plate.Size;
|
||||||
|
var oldChosenSize = pr.ChosenSize;
|
||||||
|
|
||||||
|
pr.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length);
|
||||||
|
pr.ChosenSize = upgradeOption;
|
||||||
|
|
||||||
|
var remnants = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
|
||||||
|
|
||||||
|
if (remnants.Count > 0 && tryFill(remnants))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
pr.Plate.Size = oldSize;
|
||||||
|
pr.ChosenSize = oldChosenSize;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlateOption FindSmallestFittingOption(Box partBounds)
|
||||||
|
{
|
||||||
|
return _sortedOptions?.FirstOrDefault(o => FitsBounds(OptionWorkArea(o, _template), partBounds));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Orchestration ---
|
// --- Orchestration ---
|
||||||
|
|
||||||
private MultiPlateResult Run(List<NestItem> items, PartSortOrder sortOrder, bool allowPlateCreation)
|
private MultiPlateResult Run(List<NestItem> items, PartSortOrder sortOrder, bool allowPlateCreation)
|
||||||
@@ -279,7 +318,7 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
PlaceOnNewPlates(item, bb);
|
PlaceOnNewPlates(item, bb);
|
||||||
|
|
||||||
if (item.Quantity > 0 && _plateOptions != null && _plateOptions.Count > 0)
|
if (item.Quantity > 0 && HasPlateOptions)
|
||||||
TryUpgradeOrNewPlate(item, bb);
|
TryUpgradeOrNewPlate(item, bb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +331,7 @@ namespace OpenNest
|
|||||||
CreateSharedPlates(leftovers);
|
CreateSharedPlates(leftovers);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_plateOptions != null && _plateOptions.Count > 0 && !_token.IsCancellationRequested)
|
if (HasPlateOptions && !_token.IsCancellationRequested)
|
||||||
TryConsolidateTailPlates();
|
TryConsolidateTailPlates();
|
||||||
|
|
||||||
foreach (var item in sorted.Where(i => i.Quantity > 0))
|
foreach (var item in sorted.Where(i => i.Quantity > 0))
|
||||||
@@ -323,19 +362,26 @@ namespace OpenNest
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(pr.Plate);
|
var engine = NestEngineRegistry.Create(pr.Plate);
|
||||||
var cloned = remaining.Select(CloneItem).ToList();
|
|
||||||
var parts = engine.PackArea(remnants[0], cloned, _progress, _token);
|
|
||||||
|
|
||||||
if (parts.Count > 0)
|
foreach (var remnant in remnants)
|
||||||
{
|
{
|
||||||
pr.Plate.Parts.AddRange(parts);
|
remaining = leftovers.Where(i => i.Quantity > 0).ToList();
|
||||||
pr.Parts.AddRange(parts);
|
if (remaining.Count == 0)
|
||||||
anyPlaced = true;
|
break;
|
||||||
|
|
||||||
foreach (var item in remaining)
|
var cloned = remaining.Select(CloneItem).ToList();
|
||||||
|
var parts = engine.PackArea(remnant, cloned, _progress, _token);
|
||||||
|
|
||||||
|
if (parts.Count > 0)
|
||||||
{
|
{
|
||||||
var placed = parts.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
|
pr.AddParts(parts);
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
anyPlaced = true;
|
||||||
|
|
||||||
|
foreach (var item in remaining)
|
||||||
|
{
|
||||||
|
var placed = parts.Count(p => p.BaseDrawing == item.Drawing);
|
||||||
|
DecrementQuantity(item, placed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,6 +395,7 @@ namespace OpenNest
|
|||||||
while (leftovers.Count > 0 && !_token.IsCancellationRequested)
|
while (leftovers.Count > 0 && !_token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var plate = CreatePlate(_template, _plateOptions, null);
|
var plate = CreatePlate(_template, _plateOptions, null);
|
||||||
|
var pr = CreateNewPlateResult(plate);
|
||||||
var placedAny = false;
|
var placedAny = false;
|
||||||
|
|
||||||
foreach (var item in leftovers)
|
foreach (var item in leftovers)
|
||||||
@@ -364,22 +411,27 @@ namespace OpenNest
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(plate);
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
var clonedItem = CloneItem(item);
|
|
||||||
var parts = engine.Fill(clonedItem, remnants[0], _progress, _token);
|
|
||||||
|
|
||||||
if (parts.Count > 0)
|
foreach (var remnant in remnants)
|
||||||
{
|
{
|
||||||
plate.Parts.AddRange(parts);
|
if (item.Quantity <= 0)
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
break;
|
||||||
placedAny = true;
|
|
||||||
|
var clonedItem = CloneItem(item);
|
||||||
|
var parts = engine.Fill(clonedItem, remnant, _progress, _token);
|
||||||
|
|
||||||
|
if (parts.Count > 0)
|
||||||
|
{
|
||||||
|
pr.AddParts(parts);
|
||||||
|
DecrementQuantity(item, parts.Count);
|
||||||
|
placedAny = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!placedAny)
|
if (!placedAny)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var pr = CreateNewPlateResult(plate);
|
|
||||||
pr.Parts.AddRange(plate.Parts);
|
|
||||||
_platePool.Add(pr);
|
_platePool.Add(pr);
|
||||||
leftovers.RemoveAll(i => i.Quantity <= 0);
|
leftovers.RemoveAll(i => i.Quantity <= 0);
|
||||||
}
|
}
|
||||||
@@ -388,6 +440,8 @@ namespace OpenNest
|
|||||||
private bool TryPlaceOnExistingPlates(NestItem item, Box partBounds)
|
private bool TryPlaceOnExistingPlates(NestItem item, Box partBounds)
|
||||||
{
|
{
|
||||||
var anyPlaced = false;
|
var anyPlaced = false;
|
||||||
|
var remnantCache = new Dictionary<PlateResult, List<Box>>();
|
||||||
|
PlateResult lastModified = null;
|
||||||
|
|
||||||
while (item.Quantity > 0 && !_token.IsCancellationRequested)
|
while (item.Quantity > 0 && !_token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -400,14 +454,17 @@ namespace OpenNest
|
|||||||
if (_token.IsCancellationRequested)
|
if (_token.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var workArea = pr.Plate.WorkArea();
|
if (pr == lastModified || !remnantCache.ContainsKey(pr))
|
||||||
var classification = Classify(partBounds, workArea);
|
{
|
||||||
|
var workArea = pr.Plate.WorkArea();
|
||||||
|
var classification = Classify(partBounds, workArea);
|
||||||
|
|
||||||
var remnants = classification == PartClass.Small
|
remnantCache[pr] = classification == PartClass.Small
|
||||||
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
|
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
|
||||||
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
|
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var zone in remnants)
|
foreach (var zone in remnantCache[pr])
|
||||||
{
|
{
|
||||||
var score = ScoreZone(zone, partBounds);
|
var score = ScoreZone(zone, partBounds);
|
||||||
if (score > bestScore)
|
if (score > bestScore)
|
||||||
@@ -425,6 +482,7 @@ namespace OpenNest
|
|||||||
if (FillAndPlace(bestPlate, bestZone, item) == 0)
|
if (FillAndPlace(bestPlate, bestZone, item) == 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
lastModified = bestPlate;
|
||||||
anyPlaced = true;
|
anyPlaced = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,9 +498,7 @@ namespace OpenNest
|
|||||||
var plate = CreatePlate(_template, _plateOptions, partBounds);
|
var plate = CreatePlate(_template, _plateOptions, partBounds);
|
||||||
var workArea = plate.WorkArea();
|
var workArea = plate.WorkArea();
|
||||||
|
|
||||||
if (partBounds.Length > workArea.Length && partBounds.Length > workArea.Width)
|
if (!FitsBounds(workArea, partBounds))
|
||||||
break;
|
|
||||||
if (partBounds.Width > workArea.Width && partBounds.Width > workArea.Length)
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var pr = CreateNewPlateResult(plate);
|
var pr = CreateNewPlateResult(plate);
|
||||||
@@ -459,36 +515,27 @@ namespace OpenNest
|
|||||||
|
|
||||||
private bool TryUpgradeOrNewPlate(NestItem item, Box partBounds)
|
private bool TryUpgradeOrNewPlate(NestItem item, Box partBounds)
|
||||||
{
|
{
|
||||||
if (_plateOptions == null || _plateOptions.Count == 0)
|
if (!HasPlateOptions)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var sortedOptions = _plateOptions.OrderBy(o => o.Cost).ToList();
|
|
||||||
|
|
||||||
foreach (var pr in _platePool.Where(p => p.IsNew && p.ChosenSize != null))
|
foreach (var pr in _platePool.Where(p => p.IsNew && p.ChosenSize != null))
|
||||||
{
|
{
|
||||||
var currentOption = pr.ChosenSize;
|
var currentOption = pr.ChosenSize;
|
||||||
var currentIdx = sortedOptions.FindIndex(o =>
|
var currentIdx = _sortedOptions.FindIndex(o =>
|
||||||
o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length));
|
o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length));
|
||||||
|
|
||||||
if (currentIdx < 0 || currentIdx >= sortedOptions.Count - 1)
|
if (currentIdx < 0 || currentIdx >= _sortedOptions.Count - 1)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
for (var i = currentIdx + 1; i < sortedOptions.Count; i++)
|
for (var i = currentIdx + 1; i < _sortedOptions.Count; i++)
|
||||||
{
|
{
|
||||||
var upgradeOption = sortedOptions[i];
|
var upgradeOption = _sortedOptions[i];
|
||||||
|
|
||||||
// Only consider options that are at least as large in both dimensions.
|
|
||||||
if (upgradeOption.Width < currentOption.Width - Tolerance.Epsilon
|
if (upgradeOption.Width < currentOption.Width - Tolerance.Epsilon
|
||||||
|| upgradeOption.Length < currentOption.Length - Tolerance.Epsilon)
|
|| upgradeOption.Length < currentOption.Length - Tolerance.Epsilon)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var smallestNew = sortedOptions.FirstOrDefault(o =>
|
var smallestNew = FindSmallestFittingOption(partBounds);
|
||||||
{
|
|
||||||
var ww = o.Width - _template.EdgeSpacing.Left - _template.EdgeSpacing.Right;
|
|
||||||
var wl = o.Length - _template.EdgeSpacing.Top - _template.EdgeSpacing.Bottom;
|
|
||||||
return (ww >= partBounds.Width && wl >= partBounds.Length)
|
|
||||||
|| (ww >= partBounds.Length && wl >= partBounds.Width);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (smallestNew == null)
|
if (smallestNew == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -499,22 +546,19 @@ namespace OpenNest
|
|||||||
|
|
||||||
if (decision.ShouldUpgrade)
|
if (decision.ShouldUpgrade)
|
||||||
{
|
{
|
||||||
var oldSize = pr.Plate.Size;
|
var placed = TryWithUpgradedSize(pr, upgradeOption, remnants =>
|
||||||
var oldChosenSize = pr.ChosenSize;
|
{
|
||||||
|
foreach (var remnant in remnants)
|
||||||
|
{
|
||||||
|
if (FillAndPlace(pr, remnant, item) > 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
pr.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length);
|
if (placed)
|
||||||
pr.ChosenSize = upgradeOption;
|
|
||||||
|
|
||||||
var remainingArea = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
|
|
||||||
|
|
||||||
if (remainingArea.Count > 0 && FillAndPlace(pr, remainingArea[0], item) > 0)
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Revert if nothing was placed.
|
|
||||||
pr.Plate.Size = oldSize;
|
|
||||||
pr.ChosenSize = oldChosenSize;
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,73 +567,93 @@ namespace OpenNest
|
|||||||
|
|
||||||
private void TryConsolidateTailPlates()
|
private void TryConsolidateTailPlates()
|
||||||
{
|
{
|
||||||
var activePlates = _platePool.Where(p => p.Parts.Count > 0 && p.IsNew).ToList();
|
var consolidated = true;
|
||||||
if (activePlates.Count < 2)
|
while (consolidated)
|
||||||
return;
|
|
||||||
|
|
||||||
var sortedOptions = _plateOptions.OrderBy(o => o.Cost).ToList();
|
|
||||||
|
|
||||||
// Try to absorb the smallest-utilization new plate into another plate via upgrade.
|
|
||||||
var donor = activePlates.OrderBy(p => p.Plate.Utilization()).First();
|
|
||||||
var donorParts = donor.Parts.ToList();
|
|
||||||
|
|
||||||
foreach (var target in activePlates)
|
|
||||||
{
|
{
|
||||||
if (target == donor || target.ChosenSize == null)
|
consolidated = false;
|
||||||
continue;
|
|
||||||
|
|
||||||
var currentOption = target.ChosenSize;
|
var activePlates = _platePool.Where(p => p.Parts.Count > 0 && p.IsNew).ToList();
|
||||||
|
if (activePlates.Count < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
// Try each larger option that doesn't shrink any dimension.
|
var donors = activePlates.OrderBy(p => p.Plate.Utilization()).ToList();
|
||||||
foreach (var upgradeOption in sortedOptions.Where(o =>
|
|
||||||
o.Width >= currentOption.Width - Tolerance.Epsilon
|
foreach (var donor in donors)
|
||||||
&& o.Length >= currentOption.Length - Tolerance.Epsilon
|
|
||||||
&& (o.Width > currentOption.Width + Tolerance.Epsilon
|
|
||||||
|| o.Length > currentOption.Length + Tolerance.Epsilon)))
|
|
||||||
{
|
{
|
||||||
var oldSize = target.Plate.Size;
|
if (donor.Parts.Count == 0)
|
||||||
var oldChosenSize = target.ChosenSize;
|
|
||||||
|
|
||||||
target.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length);
|
|
||||||
target.ChosenSize = upgradeOption;
|
|
||||||
|
|
||||||
var remnants = RemnantFinder.FromPlate(target.Plate).FindRemnants();
|
|
||||||
if (remnants.Count == 0)
|
|
||||||
{
|
|
||||||
target.Plate.Size = oldSize;
|
|
||||||
target.ChosenSize = oldChosenSize;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
// Try to pack all donor parts into the remnant space.
|
var donorParts = donor.Parts.ToList();
|
||||||
var engine = NestEngineRegistry.Create(target.Plate);
|
var absorbed = false;
|
||||||
var tempItems = donorParts
|
|
||||||
.GroupBy(p => p.BaseDrawing.Name)
|
|
||||||
.Select(g => new NestItem
|
|
||||||
{
|
|
||||||
Drawing = g.First().BaseDrawing,
|
|
||||||
Quantity = g.Count(),
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var placed = engine.PackArea(remnants[0], tempItems, _progress, _token);
|
foreach (var target in activePlates)
|
||||||
|
|
||||||
if (placed.Count >= donorParts.Count)
|
|
||||||
{
|
{
|
||||||
// All donor parts fit — absorb them.
|
if (target == donor || target.ChosenSize == null || target.Parts.Count == 0)
|
||||||
target.Plate.Parts.AddRange(placed);
|
continue;
|
||||||
target.Parts.AddRange(placed);
|
|
||||||
|
|
||||||
foreach (var p in donorParts)
|
var currentOption = target.ChosenSize;
|
||||||
donor.Plate.Parts.Remove(p);
|
|
||||||
donor.Parts.Clear();
|
foreach (var upgradeOption in _sortedOptions.Where(o =>
|
||||||
_platePool.Remove(donor);
|
o.Width >= currentOption.Width - Tolerance.Epsilon
|
||||||
return;
|
&& o.Length >= currentOption.Length - Tolerance.Epsilon
|
||||||
|
&& (o.Width > currentOption.Width + Tolerance.Epsilon
|
||||||
|
|| o.Length > currentOption.Length + Tolerance.Epsilon)))
|
||||||
|
{
|
||||||
|
absorbed = TryWithUpgradedSize(target, upgradeOption, remnants =>
|
||||||
|
{
|
||||||
|
var engine = NestEngineRegistry.Create(target.Plate);
|
||||||
|
var tempItems = donorParts
|
||||||
|
.GroupBy(p => p.BaseDrawing)
|
||||||
|
.Select(g => new NestItem
|
||||||
|
{
|
||||||
|
Drawing = g.Key,
|
||||||
|
Quantity = g.Count(),
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var totalPlaced = new List<Part>();
|
||||||
|
foreach (var remnant in remnants)
|
||||||
|
{
|
||||||
|
var placed = engine.PackArea(remnant, tempItems, _progress, _token);
|
||||||
|
totalPlaced.AddRange(placed);
|
||||||
|
|
||||||
|
foreach (var ti in tempItems)
|
||||||
|
{
|
||||||
|
var count = placed.Count(p => p.BaseDrawing == ti.Drawing);
|
||||||
|
ti.Quantity = System.Math.Max(0, ti.Quantity - count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempItems.All(ti => ti.Quantity <= 0))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalPlaced.Count >= donorParts.Count)
|
||||||
|
{
|
||||||
|
target.AddParts(totalPlaced);
|
||||||
|
|
||||||
|
foreach (var p in donorParts)
|
||||||
|
donor.Plate.Parts.Remove(p);
|
||||||
|
donor.Parts.Clear();
|
||||||
|
_platePool.Remove(donor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (absorbed)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absorbed)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Didn't fit all parts — revert.
|
if (absorbed)
|
||||||
target.Plate.Size = oldSize;
|
{
|
||||||
target.ChosenSize = oldChosenSize;
|
consolidated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
|
public class MultiPlateNestOptions
|
||||||
|
{
|
||||||
|
public Plate Template { get; set; }
|
||||||
|
public List<PlateOption> PlateOptions { get; set; }
|
||||||
|
public double SalvageRate { get; set; } = 0.5;
|
||||||
|
public PartSortOrder SortOrder { get; set; } = PartSortOrder.BoundingBoxArea;
|
||||||
|
public double MinRemnantSize { get; set; } = 12.0;
|
||||||
|
public bool AllowPlateCreation { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
public class MultiPlateResult
|
public class MultiPlateResult
|
||||||
{
|
{
|
||||||
public List<PlateResult> Plates { get; set; } = new();
|
public List<PlateResult> Plates { get; set; } = new();
|
||||||
@@ -14,5 +24,11 @@ namespace OpenNest
|
|||||||
public List<Part> Parts { get; set; } = new();
|
public List<Part> Parts { get; set; } = new();
|
||||||
public PlateOption ChosenSize { get; set; }
|
public PlateOption ChosenSize { get; set; }
|
||||||
public bool IsNew { get; set; }
|
public bool IsNew { get; set; }
|
||||||
|
|
||||||
|
public void AddParts(IList<Part> parts)
|
||||||
|
{
|
||||||
|
Plate.Parts.AddRange(parts);
|
||||||
|
Parts.AddRange(parts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace OpenNest.Engine
|
namespace OpenNest.Engine
|
||||||
{
|
{
|
||||||
public class PlateResult
|
public class PlateProcessingResult
|
||||||
{
|
{
|
||||||
public List<ProcessedPart> Parts { get; init; }
|
public List<ProcessedPart> Parts { get; init; }
|
||||||
}
|
}
|
||||||
@@ -14,18 +14,41 @@ namespace OpenNest.Engine
|
|||||||
public ContourCuttingStrategy CuttingStrategy { get; set; }
|
public ContourCuttingStrategy CuttingStrategy { get; set; }
|
||||||
public IRapidPlanner RapidPlanner { get; set; }
|
public IRapidPlanner RapidPlanner { get; set; }
|
||||||
|
|
||||||
public PlateResult Process(Plate plate)
|
public PlateProcessingResult Process(Plate plate)
|
||||||
{
|
{
|
||||||
var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate);
|
var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
var exitPoint = PlateHelper.GetExitPoint(plate);
|
||||||
|
|
||||||
|
// Pass 1: process each part to collect pierce points
|
||||||
|
var piercePoints = new Vector[sequenced.Count];
|
||||||
|
var currentPoint = exitPoint;
|
||||||
|
|
||||||
|
for (var i = 0; i < sequenced.Count; i++)
|
||||||
|
{
|
||||||
|
var part = sequenced[i].Part;
|
||||||
|
|
||||||
|
if (!part.HasManualLeadIns && CuttingStrategy != null)
|
||||||
|
{
|
||||||
|
var localApproach = ToPartLocal(currentPoint, part);
|
||||||
|
var result = CuttingStrategy.Apply(part.Program, localApproach);
|
||||||
|
piercePoints[i] = ToPlateSpace(GetProgramStartPoint(result.Program), part);
|
||||||
|
currentPoint = ToPlateSpace(result.LastCutPoint, part);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
piercePoints[i] = ToPlateSpace(GetProgramStartPoint(part.Program), part);
|
||||||
|
currentPoint = ToPlateSpace(GetProgramEndPoint(part.Program), part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2: re-process with next part's start point for perimeter lead-in refinement
|
||||||
var results = new List<ProcessedPart>(sequenced.Count);
|
var results = new List<ProcessedPart>(sequenced.Count);
|
||||||
var cutAreas = new List<Shape>();
|
var cutAreas = new List<Shape>();
|
||||||
var currentPoint = PlateHelper.GetExitPoint(plate);
|
currentPoint = exitPoint;
|
||||||
|
|
||||||
foreach (var sp in sequenced)
|
for (var i = 0; i < sequenced.Count; i++)
|
||||||
{
|
{
|
||||||
var part = sp.Part;
|
var part = sequenced[i].Part;
|
||||||
|
|
||||||
// Compute approach point in part-local space
|
|
||||||
var localApproach = ToPartLocal(currentPoint, part);
|
var localApproach = ToPartLocal(currentPoint, part);
|
||||||
|
|
||||||
Program processedProgram;
|
Program processedProgram;
|
||||||
@@ -33,7 +56,18 @@ namespace OpenNest.Engine
|
|||||||
|
|
||||||
if (!part.HasManualLeadIns && CuttingStrategy != null)
|
if (!part.HasManualLeadIns && CuttingStrategy != null)
|
||||||
{
|
{
|
||||||
var cuttingResult = CuttingStrategy.Apply(part.Program, localApproach);
|
CuttingResult cuttingResult;
|
||||||
|
|
||||||
|
if (i + 1 < sequenced.Count)
|
||||||
|
{
|
||||||
|
var nextStart = ToPartLocal(piercePoints[i + 1], part);
|
||||||
|
cuttingResult = CuttingStrategy.Apply(part.Program, localApproach, nextStart);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cuttingResult = CuttingStrategy.Apply(part.Program, localApproach);
|
||||||
|
}
|
||||||
|
|
||||||
processedProgram = cuttingResult.Program;
|
processedProgram = cuttingResult.Program;
|
||||||
lastCutLocal = cuttingResult.LastCutPoint;
|
lastCutLocal = cuttingResult.LastCutPoint;
|
||||||
}
|
}
|
||||||
@@ -43,11 +77,9 @@ namespace OpenNest.Engine
|
|||||||
lastCutLocal = GetProgramEndPoint(part.Program);
|
lastCutLocal = GetProgramEndPoint(part.Program);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pierce point: program start point in plate space
|
|
||||||
var pierceLocal = GetProgramStartPoint(processedProgram);
|
var pierceLocal = GetProgramStartPoint(processedProgram);
|
||||||
var piercePoint = ToPlateSpace(pierceLocal, part);
|
var piercePoint = ToPlateSpace(pierceLocal, part);
|
||||||
|
|
||||||
// Plan rapid from currentPoint to pierce point
|
|
||||||
var rapidPath = RapidPlanner.Plan(currentPoint, piercePoint, cutAreas);
|
var rapidPath = RapidPlanner.Plan(currentPoint, piercePoint, cutAreas);
|
||||||
|
|
||||||
results.Add(new ProcessedPart
|
results.Add(new ProcessedPart
|
||||||
@@ -57,16 +89,14 @@ namespace OpenNest.Engine
|
|||||||
RapidPath = rapidPath
|
RapidPath = rapidPath
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update cut areas with part perimeter
|
|
||||||
var perimeter = GetPartPerimeter(part);
|
var perimeter = GetPartPerimeter(part);
|
||||||
if (perimeter != null)
|
if (perimeter != null)
|
||||||
cutAreas.Add(perimeter);
|
cutAreas.Add(perimeter);
|
||||||
|
|
||||||
// Update current point to last cut point in plate space
|
|
||||||
currentPoint = ToPlateSpace(lastCutLocal, part);
|
currentPoint = ToPlateSpace(lastCutLocal, part);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PlateResult { Parts = results };
|
return new PlateProcessingResult { Parts = results };
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector ToPartLocal(Vector platePoint, Part part)
|
private static Vector ToPartLocal(Vector platePoint, Part part)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace OpenNest.Engine.Strategies
|
|||||||
public int PlateNumber { get; init; }
|
public int PlateNumber { get; init; }
|
||||||
public CancellationToken Token { get; init; }
|
public CancellationToken Token { get; init; }
|
||||||
public IProgress<NestProgress> Progress { get; init; }
|
public IProgress<NestProgress> Progress { get; init; }
|
||||||
public FillPolicy Policy { get; init; }
|
public FillPolicy Policy { get; init; } = new FillPolicy(new DefaultFillComparer());
|
||||||
public int MaxQuantity { get; init; }
|
public int MaxQuantity { get; init; }
|
||||||
public PartType PartType { get; set; }
|
public PartType PartType { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options controlling how <see cref="CadImporter"/> loads a CAD file
|
||||||
|
/// and builds a <see cref="Drawing"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class CadImportOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Detector name to use for bend detection. Null = auto-detect.
|
||||||
|
/// </summary>
|
||||||
|
public string BendDetectorName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When false, skips bend detection entirely. Default true.
|
||||||
|
/// </summary>
|
||||||
|
public bool DetectBends { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override the drawing name. Null = filename without extension.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Required quantity on the produced drawing. Default 1.
|
||||||
|
/// </summary>
|
||||||
|
public int Quantity { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Customer name on the produced drawing. Default null.
|
||||||
|
/// </summary>
|
||||||
|
public string Customer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a default options instance.
|
||||||
|
/// </summary>
|
||||||
|
public static CadImportOptions Default => new CadImportOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Bending;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Intermediate result of <see cref="CadImporter.Import"/>. Holds raw loaded
|
||||||
|
/// geometry and detected bends. Callers may mutate <see cref="Entities"/> and
|
||||||
|
/// <see cref="Bends"/> before passing to <see cref="CadImporter.BuildDrawing"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class CadImportResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All entities loaded from the source file, including promoted bend
|
||||||
|
/// source entities. Mutable.
|
||||||
|
/// </summary>
|
||||||
|
public List<Entity> Entities { get; set; } = new List<Entity>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bends detected during import. Mutable — callers may add, remove,
|
||||||
|
/// or replace entries before building the drawing.
|
||||||
|
/// </summary>
|
||||||
|
public List<Bend> Bends { get; set; } = new List<Bend>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bounding box of <see cref="Entities"/> at import time. May be stale
|
||||||
|
/// if callers mutate <see cref="Entities"/>; recompute if needed.
|
||||||
|
/// </summary>
|
||||||
|
public Box Bounds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Absolute path to the source file.
|
||||||
|
/// </summary>
|
||||||
|
public string SourcePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default drawing name (filename without extension, unless overridden).
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Bending;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO.Bending;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shared service that converts a CAD source file into a fully-populated
|
||||||
|
/// <see cref="Drawing"/>. Used by the UI, console, MCP, API, and training
|
||||||
|
/// tools so all code paths produce identical drawings.
|
||||||
|
/// </summary>
|
||||||
|
public static class CadImporter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Load a DXF file, run bend detection, and return a mutable result
|
||||||
|
/// ready for interactive editing or direct conversion to a Drawing.
|
||||||
|
/// </summary>
|
||||||
|
public static CadImportResult Import(string path, CadImportOptions options = null)
|
||||||
|
{
|
||||||
|
options ??= CadImportOptions.Default;
|
||||||
|
|
||||||
|
var dxf = Dxf.Import(path);
|
||||||
|
|
||||||
|
var bends = new List<Bend>();
|
||||||
|
if (options.DetectBends && dxf.Document != null)
|
||||||
|
{
|
||||||
|
bends = options.BendDetectorName == null
|
||||||
|
? BendDetectorRegistry.AutoDetect(dxf.Document)
|
||||||
|
: BendDetectorRegistry.GetByName(options.BendDetectorName)
|
||||||
|
?.DetectBends(dxf.Document)
|
||||||
|
?? new List<Bend>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Bend.UpdateEtchEntities(dxf.Entities, bends);
|
||||||
|
|
||||||
|
return new CadImportResult
|
||||||
|
{
|
||||||
|
Entities = dxf.Entities,
|
||||||
|
Bends = bends,
|
||||||
|
Bounds = dxf.Entities.GetBoundingBox(),
|
||||||
|
SourcePath = path,
|
||||||
|
Name = options.Name ?? Path.GetFileNameWithoutExtension(path),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convenience for headless callers: Import a file and build a Drawing
|
||||||
|
/// in a single call, using all loaded entities and detected bends.
|
||||||
|
/// </summary>
|
||||||
|
public static Drawing ImportDrawing(string path, CadImportOptions options = null)
|
||||||
|
{
|
||||||
|
options ??= CadImportOptions.Default;
|
||||||
|
var result = Import(path, options);
|
||||||
|
return BuildDrawing(
|
||||||
|
result,
|
||||||
|
result.Entities,
|
||||||
|
result.Bends,
|
||||||
|
options.Quantity,
|
||||||
|
options.Customer,
|
||||||
|
editedProgram: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build a fully-populated <see cref="Drawing"/> from an import result plus
|
||||||
|
/// the caller's current entity and bend state. UI callers pass the currently
|
||||||
|
/// visible subset; headless callers pass the full lists.
|
||||||
|
///
|
||||||
|
/// The produced drawing has:
|
||||||
|
/// - Program generated from the visible entities, with its first rapid moved
|
||||||
|
/// to the origin and the pierce location stored in Source.Offset
|
||||||
|
/// - SourceEntities containing all non-bend-source entities from the result
|
||||||
|
/// - SuppressedEntityIds containing entities whose layer or IsVisible is false
|
||||||
|
/// - Bends copied from the provided list
|
||||||
|
/// - Customer, Quantity, Source.Path from options / result
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">Import result from <see cref="Import"/>.</param>
|
||||||
|
/// <param name="entities">
|
||||||
|
/// Entities to build the program from. Typically the currently visible subset.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="bends">Bends to attach to the drawing.</param>
|
||||||
|
/// <param name="quantity">Required quantity.</param>
|
||||||
|
/// <param name="customer">Customer name, or null.</param>
|
||||||
|
/// <param name="editedProgram">
|
||||||
|
/// When non-null, replaces the generated program (used by the UI to honor
|
||||||
|
/// in-place G-code edits). Source.Offset is still populated from the
|
||||||
|
/// generated program so round-trips stay consistent.
|
||||||
|
/// </param>
|
||||||
|
public static Drawing BuildDrawing(
|
||||||
|
CadImportResult result,
|
||||||
|
IEnumerable<Entity> entities,
|
||||||
|
IEnumerable<Bend> bends,
|
||||||
|
int quantity,
|
||||||
|
string customer,
|
||||||
|
OpenNest.CNC.Program editedProgram)
|
||||||
|
{
|
||||||
|
var visible = entities as IList<Entity> ?? new List<Entity>(entities);
|
||||||
|
var bendList = bends as IList<Bend> ?? new List<Bend>(bends);
|
||||||
|
|
||||||
|
var normalized = ShapeProfile.NormalizeEntities(visible);
|
||||||
|
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||||
|
|
||||||
|
var offset = Vector.Zero;
|
||||||
|
if (pgm != null && pgm.Codes.Count > 0 && pgm[0].Type == OpenNest.CNC.CodeType.RapidMove)
|
||||||
|
{
|
||||||
|
var rapid = (OpenNest.CNC.RapidMove)pgm[0];
|
||||||
|
offset = rapid.EndPoint;
|
||||||
|
pgm.Offset(-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
var drawing = new Drawing(result.Name)
|
||||||
|
{
|
||||||
|
Color = Drawing.GetNextColor(),
|
||||||
|
Customer = customer,
|
||||||
|
};
|
||||||
|
drawing.Source.Path = result.SourcePath;
|
||||||
|
drawing.Source.Offset = offset;
|
||||||
|
drawing.Quantity.Required = quantity;
|
||||||
|
drawing.Bends.AddRange(bendList);
|
||||||
|
drawing.Program = editedProgram ?? pgm;
|
||||||
|
|
||||||
|
var bendSources = new HashSet<Entity>(
|
||||||
|
bendList.Where(b => b.SourceEntity != null).Select(b => b.SourceEntity));
|
||||||
|
|
||||||
|
drawing.SourceEntities = result.Entities
|
||||||
|
.Where(e => !bendSources.Contains(e))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
drawing.SuppressedEntityIds = new HashSet<System.Guid>(
|
||||||
|
drawing.SourceEntities
|
||||||
|
.Where(e => !(e.Layer != null && e.Layer.IsVisible && e.IsVisible))
|
||||||
|
.Select(e => e.Id));
|
||||||
|
|
||||||
|
return drawing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public static class EntitySerializer
|
||||||
|
{
|
||||||
|
public static EntitySetDto ToDto(List<Entity> entities, HashSet<Guid> suppressed)
|
||||||
|
{
|
||||||
|
return new EntitySetDto
|
||||||
|
{
|
||||||
|
Entities = entities.Select(ToEntityDto).ToList(),
|
||||||
|
Suppressed = suppressed.Select(id => id.ToString()).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (List<Entity> entities, HashSet<Guid> suppressed) FromDto(EntitySetDto dto)
|
||||||
|
{
|
||||||
|
var entities = dto.Entities.Select(FromEntityDto).ToList();
|
||||||
|
var suppressed = new HashSet<Guid>(dto.Suppressed.Select(Guid.Parse));
|
||||||
|
return (entities, suppressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EntityDto ToEntityDto(Entity entity)
|
||||||
|
{
|
||||||
|
switch (entity.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Line:
|
||||||
|
var line = (Line)entity;
|
||||||
|
return new EntityDto
|
||||||
|
{
|
||||||
|
Id = entity.Id.ToString(),
|
||||||
|
Type = "line",
|
||||||
|
Layer = entity.Layer?.Name ?? "",
|
||||||
|
LineType = entity.LineTypeName ?? "",
|
||||||
|
X1 = line.StartPoint.X,
|
||||||
|
Y1 = line.StartPoint.Y,
|
||||||
|
X2 = line.EndPoint.X,
|
||||||
|
Y2 = line.EndPoint.Y
|
||||||
|
};
|
||||||
|
|
||||||
|
case EntityType.Arc:
|
||||||
|
var arc = (Arc)entity;
|
||||||
|
return new EntityDto
|
||||||
|
{
|
||||||
|
Id = entity.Id.ToString(),
|
||||||
|
Type = "arc",
|
||||||
|
Layer = entity.Layer?.Name ?? "",
|
||||||
|
LineType = entity.LineTypeName ?? "",
|
||||||
|
CX = arc.Center.X,
|
||||||
|
CY = arc.Center.Y,
|
||||||
|
R = arc.Radius,
|
||||||
|
StartAngle = arc.StartAngle,
|
||||||
|
EndAngle = arc.EndAngle,
|
||||||
|
Reversed = arc.IsReversed
|
||||||
|
};
|
||||||
|
|
||||||
|
case EntityType.Circle:
|
||||||
|
var circle = (Circle)entity;
|
||||||
|
return new EntityDto
|
||||||
|
{
|
||||||
|
Id = entity.Id.ToString(),
|
||||||
|
Type = "circle",
|
||||||
|
Layer = entity.Layer?.Name ?? "",
|
||||||
|
LineType = entity.LineTypeName ?? "",
|
||||||
|
CX = circle.Center.X,
|
||||||
|
CY = circle.Center.Y,
|
||||||
|
R = circle.Radius,
|
||||||
|
Rotation = circle.Rotation == RotationType.CW ? "CW" : "CCW"
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"Entity type {entity.Type} is not supported for serialization.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Entity FromEntityDto(EntityDto dto)
|
||||||
|
{
|
||||||
|
Entity entity;
|
||||||
|
|
||||||
|
switch (dto.Type)
|
||||||
|
{
|
||||||
|
case "line":
|
||||||
|
entity = new Line(
|
||||||
|
new Vector(dto.X1, dto.Y1),
|
||||||
|
new Vector(dto.X2, dto.Y2));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "arc":
|
||||||
|
entity = new Arc(
|
||||||
|
new Vector(dto.CX, dto.CY),
|
||||||
|
dto.R,
|
||||||
|
dto.StartAngle,
|
||||||
|
dto.EndAngle,
|
||||||
|
dto.Reversed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "circle":
|
||||||
|
var circle = new Circle(new Vector(dto.CX, dto.CY), dto.R);
|
||||||
|
circle.Rotation = dto.Rotation == "CW" ? RotationType.CW : RotationType.CCW;
|
||||||
|
entity = circle;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"Entity type '{dto.Type}' is not supported for deserialization.");
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.Id = Guid.Parse(dto.Id);
|
||||||
|
entity.Layer = ResolveLayer(dto.Layer);
|
||||||
|
entity.LineTypeName = dto.LineType;
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Layer ResolveLayer(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name) || name == "0")
|
||||||
|
return Layer.Default;
|
||||||
|
|
||||||
|
if (string.Equals(name, SpecialLayers.Cut.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return SpecialLayers.Cut;
|
||||||
|
if (string.Equals(name, SpecialLayers.Rapid.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return SpecialLayers.Rapid;
|
||||||
|
if (string.Equals(name, SpecialLayers.Display.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return SpecialLayers.Display;
|
||||||
|
if (string.Equals(name, SpecialLayers.Leadin.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return SpecialLayers.Leadin;
|
||||||
|
if (string.Equals(name, SpecialLayers.Leadout.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return SpecialLayers.Leadout;
|
||||||
|
if (string.Equals(name, SpecialLayers.Scribe.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return SpecialLayers.Scribe;
|
||||||
|
|
||||||
|
return new Layer(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,6 +162,35 @@ namespace OpenNest.IO
|
|||||||
public double Cost { get; init; }
|
public double Cost { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record EntitySetDto
|
||||||
|
{
|
||||||
|
public List<EntityDto> Entities { get; init; } = new();
|
||||||
|
public List<string> Suppressed { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record EntityDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = "";
|
||||||
|
public string Type { get; init; } = "";
|
||||||
|
public string Layer { get; init; } = "";
|
||||||
|
public string LineType { get; init; } = "";
|
||||||
|
|
||||||
|
// Line
|
||||||
|
public double X1 { get; init; }
|
||||||
|
public double Y1 { get; init; }
|
||||||
|
public double X2 { get; init; }
|
||||||
|
public double Y2 { get; init; }
|
||||||
|
|
||||||
|
// Arc / Circle
|
||||||
|
public double CX { get; init; }
|
||||||
|
public double CY { get; init; }
|
||||||
|
public double R { get; init; }
|
||||||
|
public double StartAngle { get; init; }
|
||||||
|
public double EndAngle { get; init; }
|
||||||
|
public bool Reversed { get; init; }
|
||||||
|
public string Rotation { get; init; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
public record BestFitSetDto
|
public record BestFitSetDto
|
||||||
{
|
{
|
||||||
public double PlateWidth { get; init; }
|
public double PlateWidth { get; init; }
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ namespace OpenNest.IO
|
|||||||
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
|
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
|
||||||
|
|
||||||
var programs = ReadPrograms(dto.Drawings.Count);
|
var programs = ReadPrograms(dto.Drawings.Count);
|
||||||
var drawingMap = BuildDrawings(dto, programs);
|
var entitySets = ReadEntitySets(dto.Drawings.Count);
|
||||||
|
var drawingMap = BuildDrawings(dto, programs, entitySets);
|
||||||
ReadBestFits(drawingMap);
|
ReadBestFits(drawingMap);
|
||||||
var nest = BuildNest(dto, drawingMap);
|
var nest = BuildNest(dto, drawingMap);
|
||||||
|
|
||||||
@@ -70,11 +71,87 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
var reader = new ProgramReader(memStream);
|
var reader = new ProgramReader(memStream);
|
||||||
programs[i] = reader.Read();
|
programs[i] = reader.Read();
|
||||||
|
|
||||||
|
// Read sub-programs if present
|
||||||
|
var subsEntry = zipArchive.GetEntry($"programs/program-{i}-subs");
|
||||||
|
if (subsEntry != null)
|
||||||
|
{
|
||||||
|
using var subsStream = subsEntry.Open();
|
||||||
|
ReadSubPrograms(programs[i], subsStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return programs;
|
return programs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<int, Drawing> BuildDrawings(NestDto dto, Dictionary<int, Program> programs)
|
private static void ReadSubPrograms(Program parent, Stream stream)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var currentId = -1;
|
||||||
|
var lines = new List<string>();
|
||||||
|
|
||||||
|
string line;
|
||||||
|
while ((line = reader.ReadLine()) != null)
|
||||||
|
{
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
|
||||||
|
if (trimmed.StartsWith(":") && int.TryParse(trimmed.Substring(1), out var id))
|
||||||
|
{
|
||||||
|
// Flush previous sub-program
|
||||||
|
if (currentId >= 0 && lines.Count > 0)
|
||||||
|
parent.SubPrograms[currentId] = ParseSubProgram(lines);
|
||||||
|
|
||||||
|
currentId = id;
|
||||||
|
lines.Clear();
|
||||||
|
}
|
||||||
|
else if (trimmed == "M99")
|
||||||
|
{
|
||||||
|
if (currentId >= 0 && lines.Count > 0)
|
||||||
|
parent.SubPrograms[currentId] = ParseSubProgram(lines);
|
||||||
|
|
||||||
|
currentId = -1;
|
||||||
|
lines.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lines.Add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire up SubProgramCall.Program references
|
||||||
|
foreach (var code in parent.Codes)
|
||||||
|
{
|
||||||
|
if (code is SubProgramCall call && parent.SubPrograms.TryGetValue(call.Id, out var sub))
|
||||||
|
call.Program = sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Program ParseSubProgram(List<string> lines)
|
||||||
|
{
|
||||||
|
var text = string.Join("\n", lines);
|
||||||
|
var memStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(text));
|
||||||
|
var reader = new ProgramReader(memStream);
|
||||||
|
return reader.Read();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed)> ReadEntitySets(int count)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<int, (List<Entity>, HashSet<Guid>)>();
|
||||||
|
for (var i = 1; i <= count; i++)
|
||||||
|
{
|
||||||
|
var entry = zipArchive.GetEntry($"entities/entities-{i}");
|
||||||
|
if (entry == null) continue;
|
||||||
|
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
using var reader = new StreamReader(entryStream);
|
||||||
|
var json = reader.ReadToEnd();
|
||||||
|
var dto = JsonSerializer.Deserialize<EntitySetDto>(json, JsonOptions);
|
||||||
|
result[i] = EntitySerializer.FromDto(dto);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, Drawing> BuildDrawings(NestDto dto, Dictionary<int, Program> programs,
|
||||||
|
Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed)> entitySets)
|
||||||
{
|
{
|
||||||
var map = new Dictionary<int, Drawing>();
|
var map = new Dictionary<int, Drawing>();
|
||||||
foreach (var d in dto.Drawings)
|
foreach (var d in dto.Drawings)
|
||||||
@@ -112,6 +189,12 @@ namespace OpenNest.IO
|
|||||||
if (programs.TryGetValue(d.Id, out var pgm))
|
if (programs.TryGetValue(d.Id, out var pgm))
|
||||||
drawing.Program = pgm;
|
drawing.Program = pgm;
|
||||||
|
|
||||||
|
if (entitySets.TryGetValue(d.Id, out var entitySet))
|
||||||
|
{
|
||||||
|
drawing.SourceEntities = entitySet.entities;
|
||||||
|
drawing.SuppressedEntityIds = entitySet.suppressed;
|
||||||
|
}
|
||||||
|
|
||||||
map[d.Id] = drawing;
|
map[d.Id] = drawing;
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
WriteNestJson(zipArchive);
|
WriteNestJson(zipArchive);
|
||||||
WritePrograms(zipArchive);
|
WritePrograms(zipArchive);
|
||||||
|
WriteEntities(zipArchive);
|
||||||
WriteBestFits(zipArchive);
|
WriteBestFits(zipArchive);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -307,8 +308,50 @@ namespace OpenNest.IO
|
|||||||
WriteDrawing(stream, kvp.Value);
|
WriteDrawing(stream, kvp.Value);
|
||||||
|
|
||||||
var entry = zipArchive.CreateEntry(name);
|
var entry = zipArchive.CreateEntry(name);
|
||||||
using var entryStream = entry.Open();
|
using (var entryStream = entry.Open())
|
||||||
stream.CopyTo(entryStream);
|
{
|
||||||
|
stream.CopyTo(entryStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write sub-programs if present
|
||||||
|
if (kvp.Value.Program.SubPrograms.Count > 0)
|
||||||
|
WriteSubPrograms(zipArchive, kvp.Key, kvp.Value.Program.SubPrograms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteSubPrograms(ZipArchive zipArchive, int drawingId, Dictionary<int, Program> subPrograms)
|
||||||
|
{
|
||||||
|
var entry = zipArchive.CreateEntry($"programs/program-{drawingId}-subs");
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
using var writer = new StreamWriter(entryStream, Encoding.UTF8);
|
||||||
|
|
||||||
|
foreach (var kvp in subPrograms.OrderBy(k => k.Key))
|
||||||
|
{
|
||||||
|
writer.WriteLine($":{kvp.Key}");
|
||||||
|
writer.WriteLine(kvp.Value.Mode == Mode.Absolute ? "G90" : "G91");
|
||||||
|
|
||||||
|
foreach (var code in kvp.Value.Codes)
|
||||||
|
writer.WriteLine(GetCodeString(code));
|
||||||
|
|
||||||
|
writer.WriteLine("M99");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteEntities(ZipArchive zipArchive)
|
||||||
|
{
|
||||||
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
|
{
|
||||||
|
var drawing = kvp.Value;
|
||||||
|
if (drawing.SourceEntities == null || drawing.SourceEntities.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var dto = EntitySerializer.ToDto(drawing.SourceEntities, drawing.SuppressedEntityIds);
|
||||||
|
var json = JsonSerializer.Serialize(dto, JsonOptions);
|
||||||
|
|
||||||
|
var entry = zipArchive.CreateEntry($"entities/entities-{kvp.Key}");
|
||||||
|
using var stream = entry.Open();
|
||||||
|
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||||
|
writer.Write(json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +472,9 @@ namespace OpenNest.IO
|
|||||||
case CodeType.SubProgramCall:
|
case CodeType.SubProgramCall:
|
||||||
{
|
{
|
||||||
var subProgramCall = (SubProgramCall)code;
|
var subProgramCall = (SubProgramCall)code;
|
||||||
break;
|
var x = System.Math.Round(subProgramCall.Offset.X, OutputPrecision).ToString(CoordinateFormat);
|
||||||
|
var y = System.Math.Round(subProgramCall.Offset.Y, OutputPrecision).ToString(CoordinateFormat);
|
||||||
|
return $"G65P{subProgramCall.Id}X{x}Y{y}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -374,6 +374,8 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
var p = 0;
|
var p = 0;
|
||||||
var r = 0.0;
|
var r = 0.0;
|
||||||
|
var x = 0.0;
|
||||||
|
var y = 0.0;
|
||||||
|
|
||||||
while (section == CodeSection.SubProgram)
|
while (section == CodeSection.SubProgram)
|
||||||
{
|
{
|
||||||
@@ -395,13 +397,26 @@ namespace OpenNest.IO
|
|||||||
r = double.Parse(code.Value);
|
r = double.Parse(code.Value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'X':
|
||||||
|
x = double.Parse(code.Value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Y':
|
||||||
|
y = double.Parse(code.Value);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
section = CodeSection.Unknown;
|
section = CodeSection.Unknown;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
program.Codes.Add(new SubProgramCall() { Id = p, Rotation = r });
|
program.Codes.Add(new SubProgramCall
|
||||||
|
{
|
||||||
|
Id = p,
|
||||||
|
Rotation = r,
|
||||||
|
Offset = new Geometry.Vector(x, y)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Code GetNextCode()
|
private Code GetNextCode()
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using OpenNest.Converters;
|
|
||||||
using OpenNest.Geometry;
|
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
using OpenNest.Shapes;
|
using OpenNest.Shapes;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
@@ -96,24 +94,18 @@ namespace OpenNest.Mcp.Tools
|
|||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
return $"Error: file not found: {path}";
|
return $"Error: file not found: {path}";
|
||||||
|
|
||||||
var geometry = Dxf.GetGeometry(path);
|
try
|
||||||
|
{
|
||||||
|
var drawing = CadImporter.ImportDrawing(path, new CadImportOptions { Name = name });
|
||||||
|
_session.Drawings.Add(drawing);
|
||||||
|
|
||||||
if (geometry.Count == 0)
|
var bbox = drawing.Program.BoundingBox();
|
||||||
return "Error: failed to read DXF file or no geometry found";
|
return $"Imported drawing '{drawing.Name}': bbox={bbox.Width:F2} x {bbox.Length:F2}";
|
||||||
|
}
|
||||||
var normalized = ShapeProfile.NormalizeEntities(geometry);
|
catch (System.Exception ex)
|
||||||
var pgm = ConvertGeometry.ToProgram(normalized);
|
{
|
||||||
|
return $"Error: failed to import '{path}': {ex.Message}";
|
||||||
if (pgm == null)
|
}
|
||||||
return "Error: failed to convert geometry to program";
|
|
||||||
|
|
||||||
var drawingName = name ?? Path.GetFileNameWithoutExtension(path);
|
|
||||||
var drawing = new Drawing(drawingName, pgm);
|
|
||||||
drawing.Color = Drawing.GetNextColor();
|
|
||||||
_session.Drawings.Add(drawing);
|
|
||||||
|
|
||||||
var bbox = pgm.BoundingBox();
|
|
||||||
return $"Imported drawing '{drawingName}': bbox={bbox.Width:F2} x {bbox.Length:F2}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool(Name = "create_drawing")]
|
[McpServerTool(Name = "create_drawing")]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
@@ -15,11 +16,16 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
{
|
{
|
||||||
private readonly CincinnatiPostConfig _config;
|
private readonly CincinnatiPostConfig _config;
|
||||||
private readonly CincinnatiFeatureWriter _featureWriter;
|
private readonly CincinnatiFeatureWriter _featureWriter;
|
||||||
|
private readonly CoordinateFormatter _fmt;
|
||||||
|
private readonly Dictionary<int, int> _holeSubprograms;
|
||||||
|
|
||||||
public CincinnatiPartSubprogramWriter(CincinnatiPostConfig config)
|
public CincinnatiPartSubprogramWriter(CincinnatiPostConfig config,
|
||||||
|
Dictionary<int, int> holeSubprograms = null)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_featureWriter = new CincinnatiFeatureWriter(config);
|
_featureWriter = new CincinnatiFeatureWriter(config);
|
||||||
|
_fmt = new CoordinateFormatter(config.PostedAccuracy);
|
||||||
|
_holeSubprograms = holeSubprograms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -43,6 +49,15 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
for (var i = 0; i < ordered.Count; i++)
|
for (var i = 0; i < ordered.Count; i++)
|
||||||
{
|
{
|
||||||
var (codes, isEtch) = ordered[i];
|
var (codes, isEtch) = ordered[i];
|
||||||
|
var isLastFeature = i == ordered.Count - 1;
|
||||||
|
|
||||||
|
// SubProgramCall features are emitted as M98 hole calls
|
||||||
|
if (codes.Count == 1 && codes[0] is SubProgramCall holeCall)
|
||||||
|
{
|
||||||
|
WriteHoleSubprogramCall(w, holeCall, i, isLastFeature);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var featureNumber = i == 0
|
var featureNumber = i == 0
|
||||||
? _config.FeatureLineNumberStart
|
? _config.FeatureLineNumberStart
|
||||||
: 1000 + i + 1;
|
: 1000 + i + 1;
|
||||||
@@ -54,7 +69,7 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
FeatureNumber = featureNumber,
|
FeatureNumber = featureNumber,
|
||||||
PartName = drawingName,
|
PartName = drawingName,
|
||||||
IsFirstFeatureOfPart = false,
|
IsFirstFeatureOfPart = false,
|
||||||
IsLastFeatureOnSheet = i == ordered.Count - 1,
|
IsLastFeatureOnSheet = isLastFeature,
|
||||||
IsSafetyHeadraise = false,
|
IsSafetyHeadraise = false,
|
||||||
IsExteriorFeature = false,
|
IsExteriorFeature = false,
|
||||||
IsEtch = isEtch,
|
IsEtch = isEtch,
|
||||||
@@ -69,6 +84,30 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
w.WriteLine($"M99 (END OF {drawingName})");
|
w.WriteLine($"M99 (END OF {drawingName})");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void WriteHoleSubprogramCall(TextWriter w, SubProgramCall call,
|
||||||
|
int featureIndex, bool isLastFeature)
|
||||||
|
{
|
||||||
|
var postSubNum = _holeSubprograms != null && _holeSubprograms.TryGetValue(call.Id, out var num)
|
||||||
|
? num : call.Id;
|
||||||
|
|
||||||
|
var featureNumber = featureIndex == 0
|
||||||
|
? _config.FeatureLineNumberStart
|
||||||
|
: 1000 + featureIndex + 1;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
if (_config.UseLineNumbers)
|
||||||
|
sb.Append($"N{featureNumber} ");
|
||||||
|
sb.Append($"G52 X{_fmt.FormatCoord(call.Offset.X)} Y{_fmt.FormatCoord(call.Offset.Y)}");
|
||||||
|
w.WriteLine(sb.ToString());
|
||||||
|
|
||||||
|
w.WriteLine($"M98 P{postSubNum}");
|
||||||
|
|
||||||
|
w.WriteLine("G52 X0 Y0");
|
||||||
|
|
||||||
|
if (!isLastFeature)
|
||||||
|
w.WriteLine("M47");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If the program has no leading rapid, inserts a synthetic rapid at the
|
/// If the program has no leading rapid, inserts a synthetic rapid at the
|
||||||
/// last motion endpoint (the contour return point). This ensures the feature
|
/// last motion endpoint (the contour return point). This ensures the feature
|
||||||
@@ -136,4 +175,61 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
|
|
||||||
return (mapping, entries);
|
return (mapping, entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scans all parts across all plates and builds a nest-level registry of unique
|
||||||
|
/// hole sub-programs. Deduplicates by comparing sub-program code content.
|
||||||
|
/// </summary>
|
||||||
|
internal static (Dictionary<int, int> modelToPostMapping, List<(int subNum, Program program)> entries)
|
||||||
|
BuildHoleRegistry(IEnumerable<Plate> plates, int startNumber)
|
||||||
|
{
|
||||||
|
var mapping = new Dictionary<int, int>();
|
||||||
|
var entries = new List<(int, Program)>();
|
||||||
|
var contentIndex = new Dictionary<string, int>();
|
||||||
|
var nextSubNum = startNumber;
|
||||||
|
|
||||||
|
foreach (var plate in plates)
|
||||||
|
{
|
||||||
|
foreach (var part in plate.Parts)
|
||||||
|
{
|
||||||
|
if (part.BaseDrawing.IsCutOff) continue;
|
||||||
|
foreach (var code in part.Program.Codes)
|
||||||
|
{
|
||||||
|
if (code is not SubProgramCall call) continue;
|
||||||
|
if (mapping.ContainsKey(call.Id)) continue;
|
||||||
|
|
||||||
|
var canonical = ProgramToCanonical(call.Program);
|
||||||
|
if (contentIndex.TryGetValue(canonical, out var existingNum))
|
||||||
|
{
|
||||||
|
mapping[call.Id] = existingNum;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var subNum = nextSubNum++;
|
||||||
|
mapping[call.Id] = subNum;
|
||||||
|
contentIndex[canonical] = subNum;
|
||||||
|
entries.Add((subNum, call.Program));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (mapping, entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ProgramToCanonical(Program pgm)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append(pgm.Mode == Mode.Absolute ? "A" : "I");
|
||||||
|
foreach (var code in pgm.Codes)
|
||||||
|
{
|
||||||
|
if (code is LinearMove lm)
|
||||||
|
sb.Append($"L{lm.EndPoint.X:F6},{lm.EndPoint.Y:F6},{(int)lm.Layer}");
|
||||||
|
else if (code is ArcMove am)
|
||||||
|
sb.Append($"A{am.EndPoint.X:F6},{am.EndPoint.Y:F6},{am.CenterPoint.X:F6},{am.CenterPoint.Y:F6},{(int)am.Rotation},{(int)am.Layer}");
|
||||||
|
else if (code is RapidMove rm)
|
||||||
|
sb.Append($"R{rm.EndPoint.X:F6},{rm.EndPoint.Y:F6}");
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenNest.Posts.Cincinnati
|
namespace OpenNest.Posts.Cincinnati
|
||||||
{
|
{
|
||||||
@@ -277,6 +279,24 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
[DisplayName("Etch Libraries")]
|
[DisplayName("Etch Libraries")]
|
||||||
[Description("Gas-to-library mapping for etch operations.")]
|
[Description("Gas-to-library mapping for etch operations.")]
|
||||||
public List<EtchLibraryEntry> EtchLibraries { get; set; } = new();
|
public List<EtchLibraryEntry> EtchLibraries { get; set; } = new();
|
||||||
|
|
||||||
|
[Category("B. Libraries")]
|
||||||
|
[DisplayName("Selected Library")]
|
||||||
|
[Description("Overrides Material/Thickness/Gas auto-resolution. Pick an existing entry from Material Libraries, or leave blank to auto-resolve.")]
|
||||||
|
[TypeConverter(typeof(MaterialLibraryNameConverter))]
|
||||||
|
public string SelectedLibrary { get; set; } = "";
|
||||||
|
|
||||||
|
public string FindBestLibrary(string materialName, double thickness)
|
||||||
|
{
|
||||||
|
if (MaterialLibraries == null || string.IsNullOrEmpty(materialName))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
return MaterialLibraries
|
||||||
|
.Where(e => string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(e => System.Math.Abs(e.Thickness - thickness))
|
||||||
|
.Select(e => e.Library)
|
||||||
|
.FirstOrDefault() ?? "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MaterialLibraryEntry
|
public class MaterialLibraryEntry
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using OpenNest.CNC;
|
|||||||
|
|
||||||
namespace OpenNest.Posts.Cincinnati
|
namespace OpenNest.Posts.Cincinnati
|
||||||
{
|
{
|
||||||
public sealed class CincinnatiPostProcessor : IConfigurablePostProcessor
|
public sealed class CincinnatiPostProcessor : IConfigurablePostProcessor, IPostProcessorNestAware, IMaterialProvidingPostProcessor
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
@@ -25,6 +25,23 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
|
|
||||||
object IConfigurablePostProcessor.Config => Config;
|
object IConfigurablePostProcessor.Config => Config;
|
||||||
|
|
||||||
|
public IEnumerable<string> GetMaterialNames()
|
||||||
|
{
|
||||||
|
if (Config?.MaterialLibraries == null)
|
||||||
|
return System.Array.Empty<string>();
|
||||||
|
|
||||||
|
return Config.MaterialLibraries
|
||||||
|
.Select(e => e.Material)
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrepareForNest(Nest nest)
|
||||||
|
{
|
||||||
|
var materialName = nest?.Material?.Name ?? "";
|
||||||
|
var thickness = nest?.Thickness ?? 0.0;
|
||||||
|
Config.SelectedLibrary = Config.FindBestLibrary(materialName, thickness);
|
||||||
|
}
|
||||||
|
|
||||||
public CincinnatiPostProcessor()
|
public CincinnatiPostProcessor()
|
||||||
{
|
{
|
||||||
var configPath = GetConfigPath();
|
var configPath = GetConfigPath();
|
||||||
@@ -89,9 +106,15 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
if (Config.UsePartSubprograms)
|
if (Config.UsePartSubprograms)
|
||||||
(partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart);
|
(partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart);
|
||||||
|
|
||||||
|
// 5b. Build hole sub-program registry (SubProgramCalls across all parts)
|
||||||
|
var holeStartNumber = Config.PartSubprogramStart
|
||||||
|
+ (subprogramEntries?.Count ?? 0);
|
||||||
|
var (holeMapping, holeEntries) = CincinnatiPartSubprogramWriter.BuildHoleRegistry(plates, holeStartNumber);
|
||||||
|
|
||||||
// 6. Create writers
|
// 6. Create writers
|
||||||
var preamble = new CincinnatiPreambleWriter(Config);
|
var preamble = new CincinnatiPreambleWriter(Config);
|
||||||
var sheetWriter = new CincinnatiSheetWriter(Config, vars);
|
var sheetWriter = new CincinnatiSheetWriter(Config, vars,
|
||||||
|
holeMapping.Count > 0 ? holeMapping : null);
|
||||||
|
|
||||||
// 7. Build material description from nest
|
// 7. Build material description from nest
|
||||||
var material = nest.Material;
|
var material = nest.Material;
|
||||||
@@ -122,7 +145,8 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
// Part sub-programs (if enabled)
|
// Part sub-programs (if enabled)
|
||||||
if (subprogramEntries != null)
|
if (subprogramEntries != null)
|
||||||
{
|
{
|
||||||
var partSubWriter = new CincinnatiPartSubprogramWriter(Config);
|
var partSubWriter = new CincinnatiPartSubprogramWriter(Config,
|
||||||
|
holeMapping.Count > 0 ? holeMapping : null);
|
||||||
var sheetDiagonal = firstPlate != null
|
var sheetDiagonal = firstPlate != null
|
||||||
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
|
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
|
||||||
+ firstPlate.Size.Length * firstPlate.Size.Length)
|
+ firstPlate.Size.Length * firstPlate.Size.Length)
|
||||||
@@ -135,6 +159,23 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hole sub-programs (SubProgramCall definitions)
|
||||||
|
if (holeEntries.Count > 0)
|
||||||
|
{
|
||||||
|
var holeSubWriter = new CincinnatiPartSubprogramWriter(Config);
|
||||||
|
var sheetDiagonal = firstPlate != null
|
||||||
|
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
|
||||||
|
+ firstPlate.Size.Length * firstPlate.Size.Length)
|
||||||
|
: 100.0;
|
||||||
|
|
||||||
|
foreach (var (subNum, pgm) in holeEntries)
|
||||||
|
{
|
||||||
|
CincinnatiPartSubprogramWriter.EnsureLeadingRapid(pgm);
|
||||||
|
holeSubWriter.Write(writer, pgm, "HOLE", subNum,
|
||||||
|
initialCutLibrary, etchLibrary, sheetDiagonal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writer.Flush();
|
writer.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,16 @@ public sealed class CincinnatiSheetWriter
|
|||||||
private readonly ProgramVariableManager _vars;
|
private readonly ProgramVariableManager _vars;
|
||||||
private readonly CoordinateFormatter _fmt;
|
private readonly CoordinateFormatter _fmt;
|
||||||
private readonly CincinnatiFeatureWriter _featureWriter;
|
private readonly CincinnatiFeatureWriter _featureWriter;
|
||||||
|
private readonly Dictionary<int, int> _holeSubprograms;
|
||||||
|
|
||||||
public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars)
|
public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars,
|
||||||
|
Dictionary<int, int> holeSubprograms = null)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_vars = vars;
|
_vars = vars;
|
||||||
_fmt = new CoordinateFormatter(config.PostedAccuracy);
|
_fmt = new CoordinateFormatter(config.PostedAccuracy);
|
||||||
_featureWriter = new CincinnatiFeatureWriter(config);
|
_featureWriter = new CincinnatiFeatureWriter(config);
|
||||||
|
_holeSubprograms = holeSubprograms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -132,11 +135,21 @@ public sealed class CincinnatiSheetWriter
|
|||||||
for (var f = 0; f < features.Count; f++)
|
for (var f = 0; f < features.Count; f++)
|
||||||
{
|
{
|
||||||
var (codes, isEtch) = features[f];
|
var (codes, isEtch) = features[f];
|
||||||
|
var isLastFeature = isLastPart && f == features.Count - 1;
|
||||||
|
|
||||||
|
// SubProgramCall features are emitted as M98 hole calls
|
||||||
|
if (codes.Count == 1 && codes[0] is SubProgramCall holeCall)
|
||||||
|
{
|
||||||
|
WriteHoleSubprogramCall(w, holeCall, featureIndex, isLastFeature);
|
||||||
|
featureIndex++;
|
||||||
|
lastPartName = partName;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var featureNumber = featureIndex == 0
|
var featureNumber = featureIndex == 0
|
||||||
? _config.FeatureLineNumberStart
|
? _config.FeatureLineNumberStart
|
||||||
: 1000 + featureIndex + 1;
|
: 1000 + featureIndex + 1;
|
||||||
|
|
||||||
var isLastFeature = isLastPart && f == features.Count - 1;
|
|
||||||
var cutDistance = FeatureUtils.ComputeCutDistance(codes);
|
var cutDistance = FeatureUtils.ComputeCutDistance(codes);
|
||||||
|
|
||||||
var ctx = new FeatureContext
|
var ctx = new FeatureContext
|
||||||
@@ -204,6 +217,36 @@ public sealed class CincinnatiSheetWriter
|
|||||||
w.WriteLine("M47");
|
w.WriteLine("M47");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void WriteHoleSubprogramCall(TextWriter w, SubProgramCall call, int featureIndex, bool isLastFeature)
|
||||||
|
{
|
||||||
|
var postSubNum = _holeSubprograms != null && _holeSubprograms.TryGetValue(call.Id, out var num)
|
||||||
|
? num : call.Id;
|
||||||
|
|
||||||
|
var featureNumber = featureIndex == 0
|
||||||
|
? _config.FeatureLineNumberStart
|
||||||
|
: 1000 + featureIndex + 1;
|
||||||
|
|
||||||
|
// Shift the local origin to the hole center via G52 (manual §1.52).
|
||||||
|
// G52 does not move the nozzle, so the sub-program's first rapid
|
||||||
|
// (the lead-in to the pierce point) takes the tool straight from the
|
||||||
|
// previous feature's end to pierce. The hole sub-program is authored
|
||||||
|
// in hole-local coordinates and resolves to `hole + local` under the
|
||||||
|
// shift. See docs/cincinnati-post-output.md for the full bracket.
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
if (_config.UseLineNumbers)
|
||||||
|
sb.Append($"N{featureNumber} ");
|
||||||
|
sb.Append($"G52 X{_fmt.FormatCoord(call.Offset.X)} Y{_fmt.FormatCoord(call.Offset.Y)}");
|
||||||
|
w.WriteLine(sb.ToString());
|
||||||
|
|
||||||
|
w.WriteLine($"M98 P{postSubNum}");
|
||||||
|
|
||||||
|
// Cancel the local shift (manual §1.52).
|
||||||
|
w.WriteLine("G52 X0 Y0");
|
||||||
|
|
||||||
|
if (!isLastFeature)
|
||||||
|
w.WriteLine("M47");
|
||||||
|
}
|
||||||
|
|
||||||
private void WritePartsInline(TextWriter w, List<Part> allParts,
|
private void WritePartsInline(TextWriter w, List<Part> allParts,
|
||||||
string cutLibrary, string etchLibrary, double sheetDiagonal,
|
string cutLibrary, string etchLibrary, double sheetDiagonal,
|
||||||
double plateWidth, double plateLength,
|
double plateWidth, double plateLength,
|
||||||
@@ -228,6 +271,14 @@ public sealed class CincinnatiSheetWriter
|
|||||||
var isSafetyHeadraise = partName != lastPartName && lastPartName != "";
|
var isSafetyHeadraise = partName != lastPartName && lastPartName != "";
|
||||||
var isLastFeature = i == features.Count - 1;
|
var isLastFeature = i == features.Count - 1;
|
||||||
|
|
||||||
|
// SubProgramCall features are emitted as M98 hole calls
|
||||||
|
if (codes.Count == 1 && codes[0] is SubProgramCall holeCall)
|
||||||
|
{
|
||||||
|
WriteHoleSubprogramCall(w, holeCall, i, isLastFeature);
|
||||||
|
lastPartName = partName;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var featureNumber = i == 0
|
var featureNumber = i == 0
|
||||||
? _config.FeatureLineNumberStart
|
? _config.FeatureLineNumberStart
|
||||||
: 1000 + i + 1;
|
: 1000 + i + 1;
|
||||||
|
|||||||
@@ -21,7 +21,16 @@ public static class FeatureUtils
|
|||||||
|
|
||||||
foreach (var code in codes)
|
foreach (var code in codes)
|
||||||
{
|
{
|
||||||
if (code is RapidMove)
|
if (code is SubProgramCall)
|
||||||
|
{
|
||||||
|
// Flush any pending feature
|
||||||
|
if (current != null)
|
||||||
|
features.Add(current);
|
||||||
|
// SubProgramCall is its own feature
|
||||||
|
features.Add(new List<ICode> { code });
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
else if (code is RapidMove)
|
||||||
{
|
{
|
||||||
if (current != null)
|
if (current != null)
|
||||||
features.Add(current);
|
features.Add(current);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.Cincinnati
|
||||||
|
{
|
||||||
|
public sealed class MaterialLibraryNameConverter : StringConverter
|
||||||
|
{
|
||||||
|
public override bool GetStandardValuesSupported(ITypeDescriptorContext context) => true;
|
||||||
|
|
||||||
|
public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) => false;
|
||||||
|
|
||||||
|
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
|
||||||
|
{
|
||||||
|
var config = context?.Instance as CincinnatiPostConfig;
|
||||||
|
var names = new List<string> { "" };
|
||||||
|
|
||||||
|
if (config?.MaterialLibraries != null)
|
||||||
|
{
|
||||||
|
names.AddRange(config.MaterialLibraries
|
||||||
|
.Select(e => e.Library)
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StandardValuesCollection(names);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,15 +10,20 @@ public sealed class MaterialLibraryResolver
|
|||||||
|
|
||||||
private readonly List<MaterialLibraryEntry> _materialLibraries;
|
private readonly List<MaterialLibraryEntry> _materialLibraries;
|
||||||
private readonly List<EtchLibraryEntry> _etchLibraries;
|
private readonly List<EtchLibraryEntry> _etchLibraries;
|
||||||
|
private readonly string _selectedLibrary;
|
||||||
|
|
||||||
public MaterialLibraryResolver(CincinnatiPostConfig config)
|
public MaterialLibraryResolver(CincinnatiPostConfig config)
|
||||||
{
|
{
|
||||||
_materialLibraries = config.MaterialLibraries ?? new List<MaterialLibraryEntry>();
|
_materialLibraries = config.MaterialLibraries ?? new List<MaterialLibraryEntry>();
|
||||||
_etchLibraries = config.EtchLibraries ?? new List<EtchLibraryEntry>();
|
_etchLibraries = config.EtchLibraries ?? new List<EtchLibraryEntry>();
|
||||||
|
_selectedLibrary = config.SelectedLibrary ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ResolveCutLibrary(string materialName, double thickness, string gas)
|
public string ResolveCutLibrary(string materialName, double thickness, string gas)
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_selectedLibrary))
|
||||||
|
return EnsureLibExtension(_selectedLibrary);
|
||||||
|
|
||||||
var entry = _materialLibraries.FirstOrDefault(e =>
|
var entry = _materialLibraries.FirstOrDefault(e =>
|
||||||
string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase) &&
|
string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase) &&
|
||||||
System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance &&
|
System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance &&
|
||||||
|
|||||||
@@ -6,11 +6,19 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="OpenNest.Posts.Cincinnati.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
<Target Name="CopyToPostsDir" AfterTargets="Build">
|
<Target Name="CopyToPostsDir" AfterTargets="Build">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir>
|
<PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir>
|
||||||
|
<ConfigJson>$(MSBuildProjectDirectory)\OpenNest.Posts.Cincinnati.json</ConfigJson>
|
||||||
|
<DeployedConfigJson>$(PostsDir)OpenNest.Posts.Cincinnati.json</DeployedConfigJson>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<MakeDir Directories="$(PostsDir)" />
|
<MakeDir Directories="$(PostsDir)" />
|
||||||
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" />
|
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" />
|
||||||
|
<Copy SourceFiles="$(ConfigJson)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" Condition="!Exists('$(DeployedConfigJson)')" />
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"ConfigurationName": "CL940",
|
||||||
|
"PostedUnits": "Inches",
|
||||||
|
"PostedAccuracy": 4,
|
||||||
|
"UseLineNumbers": true,
|
||||||
|
"FeatureLineNumberStart": 1,
|
||||||
|
"UseSheetSubprograms": true,
|
||||||
|
"SheetSubprogramStart": 101,
|
||||||
|
"UsePartSubprograms": false,
|
||||||
|
"PartSubprogramStart": 200,
|
||||||
|
"VariableDeclarationSubprogram": 100,
|
||||||
|
"CoordModeBetweenParts": "G92",
|
||||||
|
"ProcessParameterMode": "LibraryFile",
|
||||||
|
"DefaultAssistGas": "O2",
|
||||||
|
"DefaultEtchGas": "N2",
|
||||||
|
"UseExactStopMode": false,
|
||||||
|
"UseSpeedGas": false,
|
||||||
|
"UseAntiDive": true,
|
||||||
|
"UseSmartRapids": false,
|
||||||
|
"KerfCompensation": "ControllerSide",
|
||||||
|
"DefaultKerfSide": "Left",
|
||||||
|
"InteriorM47": "Always",
|
||||||
|
"ExteriorM47": "Always",
|
||||||
|
"M47OverrideDistanceThreshold": null,
|
||||||
|
"SafetyHeadraiseDistance": 2000,
|
||||||
|
"PalletExchange": "EndOfSheet",
|
||||||
|
"LeadInFeedratePercent": 0.5,
|
||||||
|
"LeadInArcLine2FeedratePercent": 0.5,
|
||||||
|
"LeadOutFeedratePercent": 0.5,
|
||||||
|
"CircleFeedrateMultiplier": 0.8,
|
||||||
|
"ArcFeedrate": "None",
|
||||||
|
"ArcFeedrateRanges": [
|
||||||
|
{ "MaxRadius": 0.125, "FeedratePercent": 0.25, "VariableNumber": 123 },
|
||||||
|
{ "MaxRadius": 0.75, "FeedratePercent": 0.5, "VariableNumber": 124 },
|
||||||
|
{ "MaxRadius": 4.5, "FeedratePercent": 0.8, "VariableNumber": 125 }
|
||||||
|
],
|
||||||
|
"UserVariableStart": 200,
|
||||||
|
"SheetWidthVariable": 110,
|
||||||
|
"SheetLengthVariable": 111,
|
||||||
|
"MaterialLibraries": [
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.032, "Gas": "AIR", "Library": "AL032AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.032, "Gas": "N2", "Library": "AL032N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.032, "Gas": "O2", "Library": "AL032O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.050, "Gas": "AIR", "Library": "AL050AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.050, "Gas": "N2", "Library": "AL050N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.050, "Gas": "O2", "Library": "AL050O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.063, "Gas": "AIR", "Library": "AL063AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.063, "Gas": "N2", "Library": "AL063N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.063, "Gas": "O2", "Library": "AL063O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.080, "Gas": "AIR", "Library": "AL080AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.080, "Gas": "N2", "Library": "AL080N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.080, "Gas": "O2", "Library": "AL080O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.090, "Gas": "AIR", "Library": "AL090AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.090, "Gas": "N2", "Library": "AL090N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.090, "Gas": "O2", "Library": "AL090O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.100, "Gas": "AIR", "Library": "AL100AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.100, "Gas": "N2", "Library": "AL100N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.100, "Gas": "O2", "Library": "AL100O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.125, "Gas": "AIR", "Library": "AL125AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.125, "Gas": "N2", "Library": "AL125N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.125, "Gas": "O2", "Library": "AL125O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.190, "Gas": "AIR", "Library": "AL190AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.190, "Gas": "N2", "Library": "AL190N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.190, "Gas": "O2", "Library": "AL190O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.250, "Gas": "AIR", "Library": "AL250AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.250, "Gas": "N2", "Library": "AL250N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.250, "Gas": "O2", "Library": "AL250O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.375, "Gas": "AIR", "Library": "AL375AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.375, "Gas": "N2", "Library": "AL375N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.375, "Gas": "O2", "Library": "AL375O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.500, "Gas": "AIR", "Library": "AL500AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.500, "Gas": "N2", "Library": "AL500N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.500, "Gas": "O2", "Library": "AL500O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.625, "Gas": "N2", "Library": "AL625N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.750, "Gas": "AIR", "Library": "AL750AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.750, "Gas": "N2", "Library": "AL750N2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 0.750, "Gas": "O2", "Library": "AL750O2" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 1.000, "Gas": "AIR", "Library": "AL1000AIR" },
|
||||||
|
{ "Material": "Aluminum", "Thickness": 1.000, "Gas": "N2", "Library": "AL1000N2" },
|
||||||
|
|
||||||
|
{ "Material": "Galvanized Steel", "Thickness": 0.135, "Gas": "N2", "Library": "GALV135N2" },
|
||||||
|
{ "Material": "Galvanized Steel", "Thickness": 0.188, "Gas": "N2", "Library": "GALV188N2" },
|
||||||
|
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.036, "Gas": "AIR", "Library": "MS036AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.036, "Gas": "N2", "Library": "MS036N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.048, "Gas": "AIR", "Library": "MS048AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.048, "Gas": "N2", "Library": "MS048N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.060, "Gas": "AIR", "Library": "MS060AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.060, "Gas": "N2", "Library": "MS060N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.075, "Gas": "AIR", "Library": "MS075AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.075, "Gas": "N2", "Library": "MS075N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.075, "Gas": "N2", "Library": "MS075N2FE" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.090, "Gas": "N2", "Library": "MS090N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.105, "Gas": "AIR", "Library": "MS105AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.105, "Gas": "N2", "Library": "MS105N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.120, "Gas": "AIR", "Library": "MS120AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.120, "Gas": "N2", "Library": "MS120N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.120, "Gas": "N2", "Library": "MS120N2FE" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "AIR", "Library": "MS135AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "N2", "Library": "MS135N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "N2", "Library": "MS135N2FE" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "N2", "Library": "MS135N2Panel" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "AIR", "Library": "MS188AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "N2", "Library": "MS188N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "N2", "Library": "MS188N2FLOORPLATE" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "O2", "Library": "MS188O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "AIR", "Library": "MS250AIR" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "N2", "Library": "MS250N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "N2", "Library": "MS250N2FLOORPLATE" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "O2", "Library": "MS250O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.313, "Gas": "O2", "Library": "MS313O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.375, "Gas": "O2", "Library": "MS375O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.500, "Gas": "N2", "Library": "MS500N2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.500, "Gas": "O2", "Library": "MS500O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.625, "Gas": "O2", "Library": "MS625O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 0.750, "Gas": "O2", "Library": "MS750O2" },
|
||||||
|
{ "Material": "Carbon Steel", "Thickness": 1.000, "Gas": "O2", "Library": "MS1000O2" },
|
||||||
|
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.036, "Gas": "AIR", "Library": "SS036AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.036, "Gas": "N2", "Library": "SS036N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.048, "Gas": "AIR", "Library": "SS048AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.048, "Gas": "N2", "Library": "SS048N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.060, "Gas": "AIR", "Library": "SS060AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.060, "Gas": "N2", "Library": "SS060N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.075, "Gas": "AIR", "Library": "SS075AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.075, "Gas": "N2", "Library": "SS075N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.075, "Gas": "N2", "Library": "SS075N2FE" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.105, "Gas": "AIR", "Library": "SS105AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.105, "Gas": "N2", "Library": "SS105N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.105, "Gas": "N2", "Library": "SS105N2FE" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.120, "Gas": "AIR", "Library": "SS120AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.120, "Gas": "N2", "Library": "SS120N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.120, "Gas": "N2", "Library": "SS120N2FE" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.135, "Gas": "AIR", "Library": "SS135AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.135, "Gas": "N2", "Library": "SS135N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.135, "Gas": "N2", "Library": "SS135N2FE" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.188, "Gas": "AIR", "Library": "SS188AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.188, "Gas": "N2", "Library": "SS188N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.250, "Gas": "AIR", "Library": "SS250AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.250, "Gas": "N2", "Library": "SS250N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.313, "Gas": "N2", "Library": "SS313N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.375, "Gas": "AIR", "Library": "SS375AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.375, "Gas": "N2", "Library": "SS375N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.500, "Gas": "AIR", "Library": "SS500AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.500, "Gas": "N2", "Library": "SS500N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.625, "Gas": "N2", "Library": "SS625N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.750, "Gas": "AIR", "Library": "SS750AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 0.750, "Gas": "N2", "Library": "SS750N2" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 1.000, "Gas": "AIR", "Library": "SS1000AIR" },
|
||||||
|
{ "Material": "Stainless Steel", "Thickness": 1.000, "Gas": "N2", "Library": "SS1000N2" },
|
||||||
|
|
||||||
|
{ "Material": "Phenolic", "Thickness": 0.0, "Gas": "", "Library": "Phenolic" },
|
||||||
|
{ "Material": "Gasket", "Thickness": 0.250, "Gas": "N2", "Library": "GASKET250N2" }
|
||||||
|
],
|
||||||
|
"EtchLibraries": [
|
||||||
|
{ "Gas": "AIR", "Library": "EtchAIR" },
|
||||||
|
{ "Gas": "N2", "Library": "EtchN2" },
|
||||||
|
{ "Gas": "N2", "Library": "EtchN2_fast" },
|
||||||
|
{ "Gas": "N2", "Library": "Etchn2_no_mark_pvc" },
|
||||||
|
{ "Gas": "O2", "Library": "EtchO2" },
|
||||||
|
{ "Gas": "O2", "Library": "ETCHO2FINE" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Converters;
|
||||||
|
|
||||||
|
public class SubProgramExpansionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ToGeometry_ExpandsSubProgramCall_WithOffset()
|
||||||
|
{
|
||||||
|
// Sub-program: a small line relative to (0,0)
|
||||||
|
var sub = new Program(Mode.Incremental);
|
||||||
|
sub.Codes.Add(new LinearMove(0.5, 0));
|
||||||
|
|
||||||
|
// Main program: call sub at offset (10,20)
|
||||||
|
var main = new Program(Mode.Absolute);
|
||||||
|
main.SubPrograms[1] = sub;
|
||||||
|
main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(10, 20) });
|
||||||
|
|
||||||
|
var geometry = ConvertProgram.ToGeometry(main);
|
||||||
|
|
||||||
|
// The sub-program's line should be offset by (10,20)
|
||||||
|
// Sub emits incremental (0.5,0) from current position.
|
||||||
|
// Since offset is (10,20), the line goes from (10,20) to (10.5,20).
|
||||||
|
Assert.True(geometry.Count > 0);
|
||||||
|
var line = geometry.OfType<Line>().FirstOrDefault();
|
||||||
|
Assert.NotNull(line);
|
||||||
|
Assert.Equal(10.5, line.EndPoint.X, 4);
|
||||||
|
Assert.Equal(20, line.EndPoint.Y, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToGeometry_MultipleSubProgramCalls_DifferentOffsets()
|
||||||
|
{
|
||||||
|
var sub = new Program(Mode.Incremental);
|
||||||
|
sub.Codes.Add(new LinearMove(1, 0));
|
||||||
|
|
||||||
|
var main = new Program(Mode.Absolute);
|
||||||
|
main.SubPrograms[1] = sub;
|
||||||
|
main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(0, 0) });
|
||||||
|
main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(5, 5) });
|
||||||
|
|
||||||
|
var geometry = ConvertProgram.ToGeometry(main);
|
||||||
|
var lines = geometry.OfType<Line>().ToList();
|
||||||
|
|
||||||
|
Assert.Equal(2, lines.Count);
|
||||||
|
// First call at (0,0): line from (0,0) to (1,0)
|
||||||
|
Assert.Equal(1, lines[0].EndPoint.X, 4);
|
||||||
|
Assert.Equal(0, lines[0].EndPoint.Y, 4);
|
||||||
|
// Second call at (5,5): line from (5,5) to (6,5)
|
||||||
|
Assert.Equal(6, lines[1].EndPoint.X, 4);
|
||||||
|
Assert.Equal(5, lines[1].EndPoint.Y, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.CuttingStrategy;
|
||||||
|
|
||||||
|
public class HoleSubProgramTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void SubProgramCall_Offset_DefaultsToZero()
|
||||||
|
{
|
||||||
|
var call = new SubProgramCall();
|
||||||
|
Assert.Equal(0, call.Offset.X);
|
||||||
|
Assert.Equal(0, call.Offset.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SubProgramCall_Offset_StoresValue()
|
||||||
|
{
|
||||||
|
var call = new SubProgramCall { Offset = new Vector(1.5, 2.5) };
|
||||||
|
Assert.Equal(1.5, call.Offset.X);
|
||||||
|
Assert.Equal(2.5, call.Offset.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SubProgramCall_Clone_CopiesOffset()
|
||||||
|
{
|
||||||
|
var call = new SubProgramCall { Id = 1, Offset = new Vector(3, 4) };
|
||||||
|
var clone = (SubProgramCall)call.Clone();
|
||||||
|
Assert.Equal(3, clone.Offset.X);
|
||||||
|
Assert.Equal(4, clone.Offset.Y);
|
||||||
|
Assert.Equal(1, clone.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SubProgramCall_ToString_IncludesOffset()
|
||||||
|
{
|
||||||
|
var call = new SubProgramCall { Id = 1000, Offset = new Vector(1.5, 2.5) };
|
||||||
|
var str = call.ToString();
|
||||||
|
Assert.Contains("P1000", str);
|
||||||
|
Assert.Contains("X1.5", str);
|
||||||
|
Assert.Contains("Y2.5", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SubProgramCall_ToString_IncludesOffsetAndRotation()
|
||||||
|
{
|
||||||
|
var call = new SubProgramCall { Id = 1000, Offset = new Vector(1.5, 2.5), Rotation = 30 };
|
||||||
|
var str = call.ToString();
|
||||||
|
Assert.Contains("P1000", str);
|
||||||
|
Assert.Contains("X1.5", str);
|
||||||
|
Assert.Contains("Y2.5", str);
|
||||||
|
Assert.Contains("R30", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SubProgramCall_ToString_OmitsZeroFields()
|
||||||
|
{
|
||||||
|
var call = new SubProgramCall { Id = 1000 };
|
||||||
|
var str = call.ToString();
|
||||||
|
Assert.Equal("G65 P1000", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Program_SubPrograms_EmptyByDefault()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
Assert.NotNull(pgm.SubPrograms);
|
||||||
|
Assert.Empty(pgm.SubPrograms);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Program_SubPrograms_StoresAndRetrieves()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
var sub = new Program(Mode.Incremental);
|
||||||
|
sub.Codes.Add(new LinearMove(0.1, 0.2));
|
||||||
|
|
||||||
|
pgm.SubPrograms[1] = sub;
|
||||||
|
|
||||||
|
Assert.Single(pgm.SubPrograms);
|
||||||
|
Assert.Same(sub, pgm.SubPrograms[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Program_Clone_DeepCopiesSubPrograms()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
var sub = new Program(Mode.Incremental);
|
||||||
|
sub.Codes.Add(new LinearMove(0.1, 0.2));
|
||||||
|
pgm.SubPrograms[1] = sub;
|
||||||
|
|
||||||
|
var clone = (Program)pgm.Clone();
|
||||||
|
|
||||||
|
Assert.Single(clone.SubPrograms);
|
||||||
|
Assert.NotSame(sub, clone.SubPrograms[1]);
|
||||||
|
Assert.Equal(Mode.Incremental, clone.SubPrograms[1].Mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Apply_CircleHole_EmitsSubProgramCall()
|
||||||
|
{
|
||||||
|
// Create a program with a square perimeter and a circle hole at (5, 5) radius 0.5
|
||||||
|
var pgm = new Program(Mode.Absolute);
|
||||||
|
// Square perimeter
|
||||||
|
pgm.Codes.Add(new RapidMove(0, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 10));
|
||||||
|
pgm.Codes.Add(new LinearMove(10, 10));
|
||||||
|
pgm.Codes.Add(new LinearMove(10, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 0));
|
||||||
|
// Circle hole at (5, 5) radius 0.5
|
||||||
|
pgm.Codes.Add(new RapidMove(5.5, 5));
|
||||||
|
pgm.Codes.Add(new ArcMove(new Vector(5.5, 5), new Vector(5, 5), RotationType.CW));
|
||||||
|
|
||||||
|
var strategy = new ContourCuttingStrategy
|
||||||
|
{
|
||||||
|
Parameters = new CuttingParameters
|
||||||
|
{
|
||||||
|
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
||||||
|
ArcCircleLeadOut = new NoLeadOut()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = strategy.Apply(pgm, new Vector(10, 10));
|
||||||
|
|
||||||
|
// Should contain at least one SubProgramCall
|
||||||
|
var calls = result.Program.Codes.OfType<SubProgramCall>().ToList();
|
||||||
|
Assert.Single(calls);
|
||||||
|
|
||||||
|
// The call's offset should be approximately at the hole center (5, 5)
|
||||||
|
var call = calls[0];
|
||||||
|
Assert.Equal(5, call.Offset.X, 1);
|
||||||
|
Assert.Equal(5, call.Offset.Y, 1);
|
||||||
|
|
||||||
|
// The parent program should have a sub-program registered
|
||||||
|
Assert.True(result.Program.SubPrograms.ContainsKey(call.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Apply_TwoIdenticalCircles_ShareSubProgram()
|
||||||
|
{
|
||||||
|
// Square perimeter with two identical circle holes at different positions
|
||||||
|
var pgm = new Program(Mode.Absolute);
|
||||||
|
// Square perimeter
|
||||||
|
pgm.Codes.Add(new RapidMove(0, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 10));
|
||||||
|
pgm.Codes.Add(new LinearMove(10, 10));
|
||||||
|
pgm.Codes.Add(new LinearMove(10, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 0));
|
||||||
|
// Circle 1 at (2, 2) radius 0.5
|
||||||
|
pgm.Codes.Add(new RapidMove(2.5, 2));
|
||||||
|
pgm.Codes.Add(new ArcMove(new Vector(2.5, 2), new Vector(2, 2), RotationType.CW));
|
||||||
|
// Circle 2 at (6, 6) radius 0.5
|
||||||
|
pgm.Codes.Add(new RapidMove(6.5, 6));
|
||||||
|
pgm.Codes.Add(new ArcMove(new Vector(6.5, 6), new Vector(6, 6), RotationType.CW));
|
||||||
|
|
||||||
|
var strategy = new ContourCuttingStrategy
|
||||||
|
{
|
||||||
|
Parameters = new CuttingParameters
|
||||||
|
{
|
||||||
|
RoundLeadInAngles = true,
|
||||||
|
LeadInAngleIncrement = 5.0,
|
||||||
|
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
||||||
|
ArcCircleLeadOut = new NoLeadOut()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = strategy.Apply(pgm, new Vector(10, 10));
|
||||||
|
|
||||||
|
var calls = result.Program.Codes.OfType<SubProgramCall>().ToList();
|
||||||
|
Assert.Equal(2, calls.Count);
|
||||||
|
|
||||||
|
// Both calls should reference the same sub-program ID (same radius, same quantized angle)
|
||||||
|
Assert.Equal(calls[0].Id, calls[1].Id);
|
||||||
|
|
||||||
|
// But different offsets
|
||||||
|
Assert.NotEqual(calls[0].Offset.X, calls[1].Offset.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Apply_HoleCenters_PreservedInGeometry()
|
||||||
|
{
|
||||||
|
// Square perimeter 10x10 with two circle holes at known positions
|
||||||
|
var holeCenter1 = new Vector(3, 3);
|
||||||
|
var holeCenter2 = new Vector(7, 5);
|
||||||
|
var holeRadius = 0.5;
|
||||||
|
|
||||||
|
var pgm = new Program(Mode.Absolute);
|
||||||
|
// Perimeter
|
||||||
|
pgm.Codes.Add(new RapidMove(0, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(10, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(10, 10));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 10));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 0));
|
||||||
|
// Hole 1 at (3, 3)
|
||||||
|
pgm.Codes.Add(new RapidMove(holeCenter1.X + holeRadius, holeCenter1.Y));
|
||||||
|
pgm.Codes.Add(new ArcMove(
|
||||||
|
new Vector(holeCenter1.X + holeRadius, holeCenter1.Y),
|
||||||
|
holeCenter1, RotationType.CW));
|
||||||
|
// Hole 2 at (7, 5)
|
||||||
|
pgm.Codes.Add(new RapidMove(holeCenter2.X + holeRadius, holeCenter2.Y));
|
||||||
|
pgm.Codes.Add(new ArcMove(
|
||||||
|
new Vector(holeCenter2.X + holeRadius, holeCenter2.Y),
|
||||||
|
holeCenter2, RotationType.CW));
|
||||||
|
|
||||||
|
var strategy = new ContourCuttingStrategy
|
||||||
|
{
|
||||||
|
Parameters = new CuttingParameters
|
||||||
|
{
|
||||||
|
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
||||||
|
ArcCircleLeadOut = new NoLeadOut()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = strategy.Apply(pgm, new Vector(10, 10));
|
||||||
|
|
||||||
|
// Convert to geometry — this is what PlateView renders
|
||||||
|
var geometry = ConvertProgram.ToGeometry(result.Program);
|
||||||
|
var circles = geometry.OfType<Circle>().ToList();
|
||||||
|
|
||||||
|
Assert.Equal(2, circles.Count);
|
||||||
|
|
||||||
|
// Circle centers must match the original hole positions
|
||||||
|
var center1 = circles[0].Center;
|
||||||
|
var center2 = circles[1].Center;
|
||||||
|
|
||||||
|
Assert.Equal(holeCenter1.X, center1.X, 2);
|
||||||
|
Assert.Equal(holeCenter1.Y, center1.Y, 2);
|
||||||
|
Assert.Equal(holeCenter2.X, center2.X, 2);
|
||||||
|
Assert.Equal(holeCenter2.Y, center2.Y, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Part_ApplyLeadIns_HolesAndPerimeter_CorrectPositions()
|
||||||
|
{
|
||||||
|
// Build a drawing with a square and two holes
|
||||||
|
var holeCenter1 = new Vector(3, 3);
|
||||||
|
var holeCenter2 = new Vector(7, 5);
|
||||||
|
var holeRadius = 0.5;
|
||||||
|
|
||||||
|
var pgm = new Program(Mode.Absolute);
|
||||||
|
pgm.Codes.Add(new RapidMove(0, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(10, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(10, 10));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 10));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 0));
|
||||||
|
pgm.Codes.Add(new RapidMove(holeCenter1.X + holeRadius, holeCenter1.Y));
|
||||||
|
pgm.Codes.Add(new ArcMove(
|
||||||
|
new Vector(holeCenter1.X + holeRadius, holeCenter1.Y),
|
||||||
|
holeCenter1, RotationType.CW));
|
||||||
|
pgm.Codes.Add(new RapidMove(holeCenter2.X + holeRadius, holeCenter2.Y));
|
||||||
|
pgm.Codes.Add(new ArcMove(
|
||||||
|
new Vector(holeCenter2.X + holeRadius, holeCenter2.Y),
|
||||||
|
holeCenter2, RotationType.CW));
|
||||||
|
|
||||||
|
var drawing = new Drawing("TestPart") { Program = pgm };
|
||||||
|
var part = new Part(drawing);
|
||||||
|
|
||||||
|
var parameters = new CuttingParameters
|
||||||
|
{
|
||||||
|
RoundLeadInAngles = true,
|
||||||
|
LeadInAngleIncrement = 5.0,
|
||||||
|
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
||||||
|
ArcCircleLeadOut = new NoLeadOut(),
|
||||||
|
ExternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 },
|
||||||
|
ExternalLeadOut = new NoLeadOut()
|
||||||
|
};
|
||||||
|
|
||||||
|
part.ApplyLeadIns(parameters, new Vector(10, 10));
|
||||||
|
|
||||||
|
// Convert to geometry — this is what PlateView renders
|
||||||
|
var geometry = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var circles = geometry.OfType<Circle>().ToList();
|
||||||
|
var lines = geometry.OfType<Line>().Where(l => l.Layer != SpecialLayers.Rapid).ToList();
|
||||||
|
|
||||||
|
// Hole circles must be at correct positions
|
||||||
|
Assert.Equal(2, circles.Count);
|
||||||
|
Assert.Equal(holeCenter1.X, circles[0].Center.X, 2);
|
||||||
|
Assert.Equal(holeCenter1.Y, circles[0].Center.Y, 2);
|
||||||
|
Assert.Equal(holeCenter2.X, circles[1].Center.X, 2);
|
||||||
|
Assert.Equal(holeCenter2.Y, circles[1].Center.Y, 2);
|
||||||
|
Assert.Equal(holeRadius, circles[0].Radius, 2);
|
||||||
|
Assert.Equal(holeRadius, circles[1].Radius, 2);
|
||||||
|
|
||||||
|
// Perimeter lines must stay within the original 10x10 bounding box.
|
||||||
|
// This catches the mode conversion bug where perimeter gets shifted
|
||||||
|
// by the last hole's position.
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
Assert.True(line.StartPoint.X >= -1 && line.StartPoint.X <= 11,
|
||||||
|
$"Perimeter line start X={line.StartPoint.X} is outside the 10x10 part bounds");
|
||||||
|
Assert.True(line.StartPoint.Y >= -1 && line.StartPoint.Y <= 11,
|
||||||
|
$"Perimeter line start Y={line.StartPoint.Y} is outside the 10x10 part bounds");
|
||||||
|
Assert.True(line.EndPoint.X >= -1 && line.EndPoint.X <= 11,
|
||||||
|
$"Perimeter line end X={line.EndPoint.X} is outside the 10x10 part bounds");
|
||||||
|
Assert.True(line.EndPoint.Y >= -1 && line.EndPoint.Y <= 11,
|
||||||
|
$"Perimeter line end Y={line.EndPoint.Y} is outside the 10x10 part bounds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Program_BoundingBox_IncludesSubProgramOffset()
|
||||||
|
{
|
||||||
|
var sub = new Program(Mode.Incremental);
|
||||||
|
sub.Codes.Add(new LinearMove(1, 0));
|
||||||
|
|
||||||
|
var main = new Program(Mode.Absolute);
|
||||||
|
main.SubPrograms[1] = sub;
|
||||||
|
main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(10, 20) });
|
||||||
|
|
||||||
|
var box = main.BoundingBox();
|
||||||
|
|
||||||
|
// Sub-program line goes from (10,20) to (11,20)
|
||||||
|
Assert.True(box.Right >= 11);
|
||||||
|
Assert.True(box.Top >= 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Program_Rotate_RotatesSubProgramCallOffsets()
|
||||||
|
{
|
||||||
|
var sub = new Program(Mode.Incremental);
|
||||||
|
sub.Codes.Add(new LinearMove(1, 0));
|
||||||
|
|
||||||
|
var main = new Program(Mode.Absolute);
|
||||||
|
main.SubPrograms[1] = sub;
|
||||||
|
main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(10, 0) });
|
||||||
|
|
||||||
|
// Rotate 90 degrees CCW around origin
|
||||||
|
main.Rotate(System.Math.PI / 2);
|
||||||
|
|
||||||
|
var call = main.Codes.OfType<SubProgramCall>().First();
|
||||||
|
// (10, 0) rotated 90 CCW = (0, 10)
|
||||||
|
Assert.Equal(0, call.Offset.X, 1);
|
||||||
|
Assert.Equal(10, call.Offset.Y, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -229,16 +229,9 @@ public class MultiPlateNesterTests
|
|||||||
MakeItem("big2", 70, 35, 1),
|
MakeItem("big2", 70, 35, 1),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var options = new MultiPlateNestOptions { Template = template };
|
||||||
items, template,
|
|
||||||
plateOptions: null,
|
var result = MultiPlateNester.Nest(items, options);
|
||||||
salvageRate: 0.5,
|
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
|
||||||
minRemnantSize: 12.0,
|
|
||||||
allowPlateCreation: true,
|
|
||||||
existingPlates: null,
|
|
||||||
progress: null,
|
|
||||||
token: CancellationToken.None);
|
|
||||||
|
|
||||||
// Each large part should be on its own plate.
|
// Each large part should be on its own plate.
|
||||||
Assert.True(result.Plates.Count >= 2,
|
Assert.True(result.Plates.Count >= 2,
|
||||||
@@ -261,16 +254,9 @@ public class MultiPlateNesterTests
|
|||||||
MakeItem("tinyB", 4, 4, 3),
|
MakeItem("tinyB", 4, 4, 3),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var options = new MultiPlateNestOptions { Template = template };
|
||||||
items, template,
|
|
||||||
plateOptions: null,
|
var result = MultiPlateNester.Nest(items, options);
|
||||||
salvageRate: 0.5,
|
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
|
||||||
minRemnantSize: 12.0,
|
|
||||||
allowPlateCreation: true,
|
|
||||||
existingPlates: null,
|
|
||||||
progress: null,
|
|
||||||
token: CancellationToken.None);
|
|
||||||
|
|
||||||
// Both small drawing types should share space — not each on their own plate.
|
// Both small drawing types should share space — not each on their own plate.
|
||||||
// With consolidation, they pack into remaining space alongside the big part.
|
// With consolidation, they pack into remaining space alongside the big part.
|
||||||
@@ -291,16 +277,13 @@ public class MultiPlateNesterTests
|
|||||||
MakeItem("big2", 70, 35, 1),
|
MakeItem("big2", 70, 35, 1),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var options = new MultiPlateNestOptions
|
||||||
items, template,
|
{
|
||||||
plateOptions: null,
|
Template = template,
|
||||||
salvageRate: 0.5,
|
AllowPlateCreation = false,
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
};
|
||||||
minRemnantSize: 12.0,
|
|
||||||
allowPlateCreation: false,
|
var result = MultiPlateNester.Nest(items, options);
|
||||||
existingPlates: null,
|
|
||||||
progress: null,
|
|
||||||
token: CancellationToken.None);
|
|
||||||
|
|
||||||
// No existing plates and no plate creation — nothing can be placed.
|
// No existing plates and no plate creation — nothing can be placed.
|
||||||
Assert.Empty(result.Plates);
|
Assert.Empty(result.Plates);
|
||||||
@@ -325,16 +308,10 @@ public class MultiPlateNesterTests
|
|||||||
MakeItem("medium", 24, 22, 1),
|
MakeItem("medium", 24, 22, 1),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var options = new MultiPlateNestOptions { Template = template };
|
||||||
items, template,
|
|
||||||
plateOptions: null,
|
var result = MultiPlateNester.Nest(items, options,
|
||||||
salvageRate: 0.5,
|
existingPlates: new List<Plate> { existingPlate });
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
|
||||||
minRemnantSize: 12.0,
|
|
||||||
allowPlateCreation: true,
|
|
||||||
existingPlates: new List<Plate> { existingPlate },
|
|
||||||
progress: null,
|
|
||||||
token: CancellationToken.None);
|
|
||||||
|
|
||||||
// Part should be placed on the existing plate, not a new one.
|
// Part should be placed on the existing plate, not a new one.
|
||||||
Assert.Single(result.Plates);
|
Assert.Single(result.Plates);
|
||||||
@@ -403,16 +380,13 @@ public class MultiPlateNesterTests
|
|||||||
_output.WriteLine($"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}");
|
_output.WriteLine($"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}");
|
||||||
_output.WriteLine("");
|
_output.WriteLine("");
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var options = new MultiPlateNestOptions
|
||||||
items, template,
|
{
|
||||||
plateOptions: plateOptions,
|
Template = template,
|
||||||
salvageRate: 0.5,
|
PlateOptions = plateOptions,
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
};
|
||||||
minRemnantSize: 12.0,
|
|
||||||
allowPlateCreation: true,
|
var result = MultiPlateNester.Nest(items, options);
|
||||||
existingPlates: null,
|
|
||||||
progress: null,
|
|
||||||
token: CancellationToken.None);
|
|
||||||
|
|
||||||
_output.WriteLine($"=== RESULTS: {result.Plates.Count} plates ===");
|
_output.WriteLine($"=== RESULTS: {result.Plates.Count} plates ===");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using OpenNest;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Fill
|
||||||
|
{
|
||||||
|
public class FillLinearCircleTests
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
|
public FillLinearCircleTests(ITestOutputHelper output) => _output = output;
|
||||||
|
|
||||||
|
private static Drawing MakeCircleDrawing(double radius)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
var startPt = new Vector(radius * 2, radius); // rightmost point
|
||||||
|
pgm.Codes.Add(new RapidMove(startPt));
|
||||||
|
pgm.Codes.Add(new ArcMove(startPt, new Vector(radius, radius), RotationType.CCW));
|
||||||
|
return new Drawing("circle", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Drawing MakeRingDrawing(double outerRadius, double innerRadius)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
// Outer circle (CCW)
|
||||||
|
var outerStart = new Vector(outerRadius * 2, outerRadius);
|
||||||
|
pgm.Codes.Add(new RapidMove(outerStart));
|
||||||
|
pgm.Codes.Add(new ArcMove(outerStart, new Vector(outerRadius, outerRadius), RotationType.CCW));
|
||||||
|
// Inner circle (CW = hole)
|
||||||
|
var innerStart = new Vector(outerRadius + innerRadius, outerRadius);
|
||||||
|
pgm.Codes.Add(new RapidMove(innerStart));
|
||||||
|
pgm.Codes.Add(new ArcMove(innerStart, new Vector(outerRadius, outerRadius), RotationType.CW));
|
||||||
|
return new Drawing("ring", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2.0, 0.125)] // 4" diameter circle, 1/8" spacing
|
||||||
|
[InlineData(1.0, 0.125)] // 2" diameter circle
|
||||||
|
[InlineData(3.0, 0.0625)] // 6" diameter circle, 1/16" spacing
|
||||||
|
[InlineData(0.5, 0.25)] // 1" diameter circle, 1/4" spacing
|
||||||
|
public void CircleFill_OffsetBoundaries_DoNotOverlap(double radius, double spacing)
|
||||||
|
{
|
||||||
|
var drawing = MakeCircleDrawing(radius);
|
||||||
|
var workArea = new Box(0, 0, 48, 48);
|
||||||
|
var engine = new FillLinear(workArea, spacing);
|
||||||
|
var parts = engine.Fill(drawing, 0, NestDirection.Horizontal);
|
||||||
|
|
||||||
|
_output.WriteLine($"Circle R={radius}, spacing={spacing}: {parts.Count} parts");
|
||||||
|
|
||||||
|
AssertNoOffsetOverlap(parts, spacing, radius * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2.0, 1.5, 0.125)] // Ring: outer R=2, inner R=1.5
|
||||||
|
[InlineData(1.5, 1.0, 0.125)] // Ring: outer R=1.5, inner R=1.0
|
||||||
|
public void RingFill_OffsetBoundaries_DoNotOverlap(double outerR, double innerR, double spacing)
|
||||||
|
{
|
||||||
|
var drawing = MakeRingDrawing(outerR, innerR);
|
||||||
|
var workArea = new Box(0, 0, 48, 48);
|
||||||
|
var engine = new FillLinear(workArea, spacing);
|
||||||
|
var parts = engine.Fill(drawing, 0, NestDirection.Horizontal);
|
||||||
|
|
||||||
|
_output.WriteLine($"Ring outerR={outerR}, innerR={innerR}, spacing={spacing}: {parts.Count} parts");
|
||||||
|
|
||||||
|
AssertNoOffsetOverlap(parts, spacing, outerR * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AssertNoOffsetOverlap(List<Part> parts, double spacing, double expectedDiameter)
|
||||||
|
{
|
||||||
|
if (parts.Count < 2)
|
||||||
|
{
|
||||||
|
_output.WriteLine(" Only 1 part placed, skipping overlap check");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var halfSpacing = spacing / 2;
|
||||||
|
var radius = expectedDiameter / 2;
|
||||||
|
var minGap = double.MaxValue;
|
||||||
|
var violationCount = 0;
|
||||||
|
|
||||||
|
// For circular parts, the center is at Location + (radius, radius).
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var ci = parts[i].Location + new Vector(radius, radius);
|
||||||
|
|
||||||
|
for (var j = i + 1; j < parts.Count; j++)
|
||||||
|
{
|
||||||
|
var cj = parts[j].Location + new Vector(radius, radius);
|
||||||
|
var centerDist = ci.DistanceTo(cj);
|
||||||
|
|
||||||
|
// Gap between raw circle perimeters
|
||||||
|
var rawGap = centerDist - expectedDiameter;
|
||||||
|
|
||||||
|
// Gap between offset circle perimeters (halfSpacing each side)
|
||||||
|
var offsetGap = centerDist - expectedDiameter - spacing;
|
||||||
|
|
||||||
|
if (rawGap < minGap)
|
||||||
|
minGap = rawGap;
|
||||||
|
|
||||||
|
if (rawGap < spacing - Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
violationCount++;
|
||||||
|
if (violationCount <= 5)
|
||||||
|
{
|
||||||
|
_output.WriteLine($" SPACING VIOLATION parts[{i}] vs parts[{j}]: " +
|
||||||
|
$"centerDist={centerDist:F6}, rawGap={rawGap:F6}, offsetGap={offsetGap:F6}, " +
|
||||||
|
$"expected>={spacing:F4}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_output.WriteLine($" Min gap={minGap:F6}, expected>={spacing:F4}, violations={violationCount}");
|
||||||
|
|
||||||
|
if (violationCount > 0)
|
||||||
|
{
|
||||||
|
var maxDeficit = spacing - minGap;
|
||||||
|
_output.WriteLine($" Max deficit={maxDeficit:F6}");
|
||||||
|
Assert.Fail($"{violationCount} pairs violate spacing: min gap={minGap:F6}, expected>={spacing}, deficit={maxDeficit:F6}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Geometry;
|
||||||
|
|
||||||
|
public class SpatialQueryTests
|
||||||
|
{
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
private static List<Entity> MakeSquare(double size)
|
||||||
|
{
|
||||||
|
return new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(0, 0, size, 0),
|
||||||
|
new Line(size, 0, size, size),
|
||||||
|
new Line(size, size, 0, size),
|
||||||
|
new Line(0, size, 0, 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Entity> MakeRoundedRect(double length, double width, double r)
|
||||||
|
{
|
||||||
|
return new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(r, 0, length - r, 0),
|
||||||
|
new Arc(length - r, r, r, Angle.ToRadians(270), Angle.ToRadians(360)),
|
||||||
|
new Line(length, r, length, width - r),
|
||||||
|
new Arc(length - r, width - r, r, Angle.ToRadians(0), Angle.ToRadians(90)),
|
||||||
|
new Line(length - r, width, r, width),
|
||||||
|
new Arc(r, width - r, r, Angle.ToRadians(90), Angle.ToRadians(180)),
|
||||||
|
new Line(0, width - r, 0, r),
|
||||||
|
new Arc(r, r, r, Angle.ToRadians(180), Angle.ToRadians(270)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Entity> MakeCircle(double cx, double cy, double radius)
|
||||||
|
{
|
||||||
|
return new List<Entity> { new Circle(cx, cy, radius) };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Entity> Translate(List<Entity> entities, double dx, double dy)
|
||||||
|
{
|
||||||
|
var result = new List<Entity>();
|
||||||
|
foreach (var e in entities)
|
||||||
|
{
|
||||||
|
if (e is Line line)
|
||||||
|
result.Add(new Line(line.pt1.X + dx, line.pt1.Y + dy, line.pt2.X + dx, line.pt2.Y + dy));
|
||||||
|
else if (e is Arc arc)
|
||||||
|
result.Add(new Arc(arc.Center.X + dx, arc.Center.Y + dy, arc.Radius, arc.StartAngle, arc.EndAngle));
|
||||||
|
else if (e is Circle circle)
|
||||||
|
result.Add(new Circle(circle.Center.X + dx, circle.Center.Y + dy, circle.Radius));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Circle vs Circle
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CircleToCircle_Right_ReturnsGap()
|
||||||
|
{
|
||||||
|
var a = MakeCircle(0, 0, 5);
|
||||||
|
var b = MakeCircle(20, 0, 5);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 9.9, 10.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CircleToCircle_Left_ReturnsGap()
|
||||||
|
{
|
||||||
|
var a = MakeCircle(20, 0, 5);
|
||||||
|
var b = MakeCircle(0, 0, 5);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(-1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 9.9, 10.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CircleToCircle_Up_ReturnsGap()
|
||||||
|
{
|
||||||
|
var a = MakeCircle(0, 0, 5);
|
||||||
|
var b = MakeCircle(0, 20, 5);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(0, 1));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 9.9, 10.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CircleToCircle_Touching_ReturnsZero()
|
||||||
|
{
|
||||||
|
var a = MakeCircle(0, 0, 5);
|
||||||
|
var b = MakeCircle(10, 0, 5);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, -0.01, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CircleToCircle_NoPath_ReturnsMaxValue()
|
||||||
|
{
|
||||||
|
var a = MakeCircle(0, 0, 3);
|
||||||
|
var b = MakeCircle(0, 20, 3);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.Equal(double.MaxValue, dist);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CircleToCircle_PushDirection_Right()
|
||||||
|
{
|
||||||
|
var a = MakeCircle(0, 0, 5);
|
||||||
|
var b = MakeCircle(20, 0, 5);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, PushDirection.Right);
|
||||||
|
|
||||||
|
Assert.InRange(dist, 9.9, 10.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Square vs Square
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SquareToSquare_Right_ReturnsGap()
|
||||||
|
{
|
||||||
|
var a = MakeSquare(10);
|
||||||
|
var b = Translate(MakeSquare(10), 25, 0);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 14.9, 15.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SquareToSquare_Left_ReturnsGap()
|
||||||
|
{
|
||||||
|
var a = Translate(MakeSquare(10), 25, 0);
|
||||||
|
var b = MakeSquare(10);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(-1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 14.9, 15.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SquareToSquare_Down_ReturnsGap()
|
||||||
|
{
|
||||||
|
var a = Translate(MakeSquare(10), 0, 25);
|
||||||
|
var b = MakeSquare(10);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(0, -1));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 14.9, 15.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SquareToSquare_Touching_ReturnsZero()
|
||||||
|
{
|
||||||
|
var a = MakeSquare(10);
|
||||||
|
var b = Translate(MakeSquare(10), 10, 0);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, -0.01, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SquareToSquare_NoOverlap_ReturnsMaxValue()
|
||||||
|
{
|
||||||
|
var a = MakeSquare(10);
|
||||||
|
var b = Translate(MakeSquare(10), 0, 20);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.Equal(double.MaxValue, dist);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SquareToSquare_PartialOverlap_Right()
|
||||||
|
{
|
||||||
|
var a = MakeSquare(10);
|
||||||
|
var b = Translate(MakeSquare(10), 20, 5);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 9.9, 10.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Rounded Rectangle
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundedRect_Right_ReturnsGap()
|
||||||
|
{
|
||||||
|
var a = MakeRoundedRect(20, 10, 2);
|
||||||
|
var b = Translate(MakeRoundedRect(20, 10, 2), 30, 0);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 9.9, 10.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundedRect_Up_ReturnsGap()
|
||||||
|
{
|
||||||
|
var a = MakeRoundedRect(20, 10, 2);
|
||||||
|
var b = Translate(MakeRoundedRect(20, 10, 2), 0, 25);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(0, 1));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 14.9, 15.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundedRect_Touching_ReturnsZero()
|
||||||
|
{
|
||||||
|
var a = MakeRoundedRect(20, 10, 2);
|
||||||
|
var b = Translate(MakeRoundedRect(20, 10, 2), 20, 0);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, -0.01, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundedRect_Diagonal_ReturnsDistance()
|
||||||
|
{
|
||||||
|
var dir = new Vector(1 / System.Math.Sqrt(2), 1 / System.Math.Sqrt(2));
|
||||||
|
var a = MakeRoundedRect(10, 10, 2);
|
||||||
|
var b = Translate(MakeRoundedRect(10, 10, 2), 20, 20);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, dir);
|
||||||
|
|
||||||
|
Assert.True(dist > 0 && dist < double.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Circle vs Square
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CircleToSquare_Right_ReturnsGap()
|
||||||
|
{
|
||||||
|
var circle = MakeCircle(0, 5, 5);
|
||||||
|
var square = Translate(MakeSquare(10), 15, 0);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(circle, square, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 9.9, 10.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SquareToCircle_Right_ReturnsGap()
|
||||||
|
{
|
||||||
|
var square = MakeSquare(10);
|
||||||
|
var circle = MakeCircle(25, 5, 5);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(square, circle, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 9.9, 10.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CircleToSquare_Touching_ReturnsZero()
|
||||||
|
{
|
||||||
|
var circle = MakeCircle(0, 5, 5);
|
||||||
|
var square = Translate(MakeSquare(10), 5, 0);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(circle, square, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, -0.01, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Circle vs Rounded Rectangle
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CircleToRoundedRect_Right_ReturnsGap()
|
||||||
|
{
|
||||||
|
var circle = MakeCircle(0, 5, 5);
|
||||||
|
var rect = Translate(MakeRoundedRect(20, 10, 2), 15, 0);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(circle, rect, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 9.9, 10.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundedRectToCircle_Left_ReturnsGap()
|
||||||
|
{
|
||||||
|
var rect = Translate(MakeRoundedRect(20, 10, 2), 15, 0);
|
||||||
|
var circle = MakeCircle(0, 5, 5);
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(rect, circle, new Vector(-1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(dist, 9.9, 10.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Edge cases
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyLists_ReturnsMaxValue()
|
||||||
|
{
|
||||||
|
var a = new List<Entity>();
|
||||||
|
var b = new List<Entity>();
|
||||||
|
|
||||||
|
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
|
||||||
|
Assert.Equal(double.MaxValue, dist);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Symmetry_LeftRightReturnSameDistance()
|
||||||
|
{
|
||||||
|
var a = MakeSquare(10);
|
||||||
|
var b = Translate(MakeSquare(10), 25, 0);
|
||||||
|
|
||||||
|
var right = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
var left = SpatialQuery.DirectionalDistance(b, a, new Vector(-1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(System.Math.Abs(right - left), 0, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Symmetry_CirclesLeftRightSame()
|
||||||
|
{
|
||||||
|
var a = MakeCircle(0, 0, 5);
|
||||||
|
var b = MakeCircle(20, 0, 5);
|
||||||
|
|
||||||
|
var right = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||||
|
var left = SpatialQuery.DirectionalDistance(b, a, new Vector(-1, 0));
|
||||||
|
|
||||||
|
Assert.InRange(System.Math.Abs(right - left), 0, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.IO;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.IO
|
||||||
|
{
|
||||||
|
public class CadImporterTests
|
||||||
|
{
|
||||||
|
private static string TestDxf =>
|
||||||
|
Path.Combine("Bending", "TestData", "4526 A14 PT11.dxf");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Import_LoadsEntitiesAndDetectsBends()
|
||||||
|
{
|
||||||
|
var result = CadImporter.Import(TestDxf);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.NotEmpty(result.Entities);
|
||||||
|
Assert.NotNull(result.Bends);
|
||||||
|
Assert.NotNull(result.Bounds);
|
||||||
|
Assert.Equal(TestDxf, result.SourcePath);
|
||||||
|
Assert.Equal("4526 A14 PT11", result.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Import_WhenDetectBendsFalse_ReturnsEmptyBends()
|
||||||
|
{
|
||||||
|
var result = CadImporter.Import(TestDxf, new CadImportOptions { DetectBends = false });
|
||||||
|
|
||||||
|
Assert.Empty(result.Bends);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Import_WhenNameOverrideProvided_UsesOverride()
|
||||||
|
{
|
||||||
|
var result = CadImporter.Import(TestDxf, new CadImportOptions { Name = "custom" });
|
||||||
|
|
||||||
|
Assert.Equal("custom", result.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Import_WhenNamedDetectorDoesNotExist_ReturnsEmptyBends()
|
||||||
|
{
|
||||||
|
// Exercises the named-detector branch: when BendDetectorName doesn't
|
||||||
|
// match any registered detector, bends should be an empty list
|
||||||
|
// (not a crash, and no fall-through to auto-detect).
|
||||||
|
var result = CadImporter.Import(TestDxf,
|
||||||
|
new CadImportOptions { BendDetectorName = "__nonexistent__" });
|
||||||
|
|
||||||
|
Assert.Empty(result.Bends);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildDrawing_ProducesDrawingWithProgramAndMetadata()
|
||||||
|
{
|
||||||
|
var result = CadImporter.Import(TestDxf);
|
||||||
|
|
||||||
|
var drawing = CadImporter.BuildDrawing(
|
||||||
|
result,
|
||||||
|
result.Entities,
|
||||||
|
result.Bends,
|
||||||
|
quantity: 5,
|
||||||
|
customer: "ACME",
|
||||||
|
editedProgram: null);
|
||||||
|
|
||||||
|
Assert.NotNull(drawing);
|
||||||
|
Assert.Equal("4526 A14 PT11", drawing.Name);
|
||||||
|
Assert.Equal("ACME", drawing.Customer);
|
||||||
|
Assert.Equal(5, drawing.Quantity.Required);
|
||||||
|
Assert.Equal(TestDxf, drawing.Source.Path);
|
||||||
|
Assert.NotNull(drawing.Program);
|
||||||
|
Assert.NotEmpty(drawing.Program.Codes);
|
||||||
|
Assert.NotNull(drawing.SourceEntities);
|
||||||
|
Assert.NotEmpty(drawing.SourceEntities);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildDrawing_ExtractsFirstRapidAsSourceOffset()
|
||||||
|
{
|
||||||
|
var result = CadImporter.Import(TestDxf);
|
||||||
|
|
||||||
|
var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends,
|
||||||
|
quantity: 1, customer: null, editedProgram: null);
|
||||||
|
|
||||||
|
Assert.NotNull(drawing.Source.Offset);
|
||||||
|
// After offset extraction, the program's first rapid must start at origin.
|
||||||
|
var firstRapid = (OpenNest.CNC.RapidMove)drawing.Program.Codes[0];
|
||||||
|
Assert.Equal(0, firstRapid.EndPoint.X, 6);
|
||||||
|
Assert.Equal(0, firstRapid.EndPoint.Y, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildDrawing_WhenEntityHidden_TracksSuppressedId()
|
||||||
|
{
|
||||||
|
var result = CadImporter.Import(TestDxf);
|
||||||
|
// Suppress the first non-bend-source entity
|
||||||
|
var bendSources = result.Bends
|
||||||
|
.Where(b => b.SourceEntity != null)
|
||||||
|
.Select(b => b.SourceEntity)
|
||||||
|
.ToHashSet();
|
||||||
|
var hidden = result.Entities.First(e => !bendSources.Contains(e));
|
||||||
|
hidden.IsVisible = false;
|
||||||
|
|
||||||
|
var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends,
|
||||||
|
quantity: 1, customer: null, editedProgram: null);
|
||||||
|
|
||||||
|
Assert.Contains(hidden.Id, drawing.SuppressedEntityIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildDrawing_WhenEditedProgramProvided_UsesEditedProgram()
|
||||||
|
{
|
||||||
|
var result = CadImporter.Import(TestDxf);
|
||||||
|
var edited = new OpenNest.CNC.Program();
|
||||||
|
edited.MoveTo(new OpenNest.Geometry.Vector(0, 0));
|
||||||
|
|
||||||
|
var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends,
|
||||||
|
quantity: 1, customer: null, editedProgram: edited);
|
||||||
|
|
||||||
|
Assert.Same(edited, drawing.Program);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImportDrawing_ComposesImportAndBuild()
|
||||||
|
{
|
||||||
|
var drawing = CadImporter.ImportDrawing(TestDxf,
|
||||||
|
new CadImportOptions { Quantity = 3, Customer = "ACME" });
|
||||||
|
|
||||||
|
Assert.NotNull(drawing);
|
||||||
|
Assert.Equal("4526 A14 PT11", drawing.Name);
|
||||||
|
Assert.Equal(3, drawing.Quantity.Required);
|
||||||
|
Assert.Equal("ACME", drawing.Customer);
|
||||||
|
Assert.NotNull(drawing.Program);
|
||||||
|
Assert.NotNull(drawing.SourceEntities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.IO;
|
||||||
|
|
||||||
|
public class SubProgramSerializationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NestWriter_WritesSubProgramCall_WithOffset()
|
||||||
|
{
|
||||||
|
var nest = CreateNestWithHoleSubProgram();
|
||||||
|
|
||||||
|
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 drawing = loaded.Drawings.First();
|
||||||
|
var calls = drawing.Program.Codes.OfType<SubProgramCall>().ToList();
|
||||||
|
Assert.Single(calls);
|
||||||
|
Assert.Equal(5, calls[0].Offset.X, 1);
|
||||||
|
Assert.Equal(5, calls[0].Offset.Y, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NestWriter_WritesSubPrograms_AndRestoresOnLoad()
|
||||||
|
{
|
||||||
|
var nest = CreateNestWithHoleSubProgram();
|
||||||
|
|
||||||
|
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 drawing = loaded.Drawings.First();
|
||||||
|
Assert.True(drawing.Program.SubPrograms.Count > 0);
|
||||||
|
|
||||||
|
var call = drawing.Program.Codes.OfType<SubProgramCall>().First();
|
||||||
|
Assert.True(drawing.Program.SubPrograms.ContainsKey(call.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Nest CreateNestWithHoleSubProgram()
|
||||||
|
{
|
||||||
|
var sub = new Program(Mode.Incremental);
|
||||||
|
sub.Codes.Add(new LinearMove(0.1, 0) { Layer = LayerType.Leadin });
|
||||||
|
sub.Codes.Add(new ArcMove(new Vector(0, 0), new Vector(-0.5, 0), RotationType.CW));
|
||||||
|
|
||||||
|
var pgm = new Program(Mode.Absolute);
|
||||||
|
pgm.SubPrograms[42] = sub;
|
||||||
|
pgm.Codes.Add(new SubProgramCall { Id = 42, Program = sub, Offset = new Vector(5, 5) });
|
||||||
|
// Add perimeter so the drawing has non-zero geometry
|
||||||
|
pgm.Codes.Add(new RapidMove(0, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(10, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(10, 10));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 10));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 0));
|
||||||
|
|
||||||
|
var drawing = new Drawing("TestPart") { Program = pgm };
|
||||||
|
var nest = new Nest();
|
||||||
|
nest.Drawings.Add(drawing);
|
||||||
|
|
||||||
|
var plate = new Plate { Size = new Size(48, 96) };
|
||||||
|
plate.Parts.Add(new Part(drawing));
|
||||||
|
nest.Plates.Add(plate);
|
||||||
|
|
||||||
|
return nest;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using OpenNest;
|
using OpenNest;
|
||||||
using OpenNest.Engine.BestFit;
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Engine.ML;
|
using OpenNest.Engine.ML;
|
||||||
using OpenNest.Geometry;
|
|
||||||
using OpenNest.Gpu;
|
using OpenNest.Gpu;
|
||||||
|
using OpenNest.Geometry;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
using OpenNest.Training;
|
using OpenNest.Training;
|
||||||
using System;
|
using System;
|
||||||
@@ -128,17 +128,26 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var entities = Dxf.GetGeometry(file);
|
Drawing drawing;
|
||||||
if (entities.Count == 0)
|
try
|
||||||
|
{
|
||||||
|
drawing = CadImporter.ImportDrawing(file,
|
||||||
|
new CadImportOptions { DetectBends = false, Name = Path.GetFileName(file) });
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - SKIP ({ex.Message})");
|
||||||
|
skippedGeometry++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawing.Program == null || drawing.Program.Codes.Count == 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine(" - SKIP (no geometry)");
|
Console.WriteLine(" - SKIP (no geometry)");
|
||||||
skippedGeometry++;
|
skippedGeometry++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var drawing = new Drawing(Path.GetFileName(file));
|
|
||||||
var normalized = ShapeProfile.NormalizeEntities(entities);
|
|
||||||
drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(normalized);
|
|
||||||
drawing.UpdateArea();
|
drawing.UpdateArea();
|
||||||
drawing.Color = PartColors[colorIndex % PartColors.Length];
|
drawing.Color = PartColors[colorIndex % PartColors.Length];
|
||||||
colorIndex++;
|
colorIndex++;
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using OpenNest.IO.Bom;
|
||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public static class ArchUnits
|
||||||
|
{
|
||||||
|
private static readonly Regex UnitRegex =
|
||||||
|
new Regex("^(?<Feet>\\d+\\.?\\d*\\s*')?\\s*(?<Inches>\\d+\\.?\\d*\\s*\")?$");
|
||||||
|
|
||||||
|
public static double ParseToInches(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var sb = new StringBuilder(input.Trim().ToLower());
|
||||||
|
|
||||||
|
sb.Replace("ft", "'");
|
||||||
|
sb.Replace("feet", "'");
|
||||||
|
sb.Replace("foot", "'");
|
||||||
|
sb.Replace("inches", "\"");
|
||||||
|
sb.Replace("inch", "\"");
|
||||||
|
sb.Replace("in", "\"");
|
||||||
|
|
||||||
|
input = Fraction.ReplaceFractionsWithDecimals(sb.ToString());
|
||||||
|
|
||||||
|
var match = UnitRegex.Match(input);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
if (!input.Contains("'") && !input.Contains("\""))
|
||||||
|
{
|
||||||
|
if (double.TryParse(input.Trim(), out var plainInches))
|
||||||
|
return System.Math.Round(plainInches, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FormatException("Input is not in a valid format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var feet = match.Groups["Feet"];
|
||||||
|
var inches = match.Groups["Inches"];
|
||||||
|
var totalInches = 0.0;
|
||||||
|
|
||||||
|
if (feet.Success)
|
||||||
|
{
|
||||||
|
var x = double.Parse(feet.Value.Remove(feet.Length - 1));
|
||||||
|
totalInches += x * 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inches.Success)
|
||||||
|
{
|
||||||
|
var x = double.Parse(inches.Value.Remove(inches.Length - 1));
|
||||||
|
totalInches += x;
|
||||||
|
}
|
||||||
|
|
||||||
|
return System.Math.Round(totalInches, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double GetLengthInches(TextBox tb)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (double.TryParse(tb.Text, out var d))
|
||||||
|
{
|
||||||
|
tb.ForeColor = SystemColors.WindowText;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = ParseToInches(tb.Text);
|
||||||
|
tb.ForeColor = SystemColors.WindowText;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
tb.ForeColor = Color.Red;
|
||||||
|
return double.NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Action = OpenNest.Actions.Action;
|
||||||
|
|
||||||
|
namespace OpenNest.Controls
|
||||||
|
{
|
||||||
|
internal class ActionManager
|
||||||
|
{
|
||||||
|
private readonly PlateView view;
|
||||||
|
private Action currentAction;
|
||||||
|
private Action previousAction;
|
||||||
|
|
||||||
|
public ActionManager(PlateView view)
|
||||||
|
{
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action CurrentAction => currentAction;
|
||||||
|
|
||||||
|
public void SetAction(Type type)
|
||||||
|
{
|
||||||
|
var action = Activator.CreateInstance(type, view) as Action;
|
||||||
|
if (action == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (currentAction != null)
|
||||||
|
{
|
||||||
|
if (type == typeof(Actions.ActionSelect) && !(currentAction is Actions.ActionSelect))
|
||||||
|
previousAction = currentAction;
|
||||||
|
else
|
||||||
|
previousAction = null;
|
||||||
|
|
||||||
|
currentAction.CancelAction();
|
||||||
|
currentAction.DisconnectEvents();
|
||||||
|
currentAction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAction = action;
|
||||||
|
view.Status = GetDisplayName(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetAction(Type type, params object[] args)
|
||||||
|
{
|
||||||
|
if (currentAction != null)
|
||||||
|
{
|
||||||
|
previousAction = null;
|
||||||
|
currentAction.CancelAction();
|
||||||
|
currentAction.DisconnectEvents();
|
||||||
|
currentAction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Resize(ref args, args.Length + 1);
|
||||||
|
for (var i = args.Length - 2; i >= 0; i--)
|
||||||
|
args[i + 1] = args[i];
|
||||||
|
args[0] = view;
|
||||||
|
|
||||||
|
var action = Activator.CreateInstance(type, args) as Action;
|
||||||
|
if (action == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
currentAction = action;
|
||||||
|
view.Status = GetDisplayName(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessEscapeKey()
|
||||||
|
{
|
||||||
|
if (currentAction.IsBusy())
|
||||||
|
currentAction.CancelAction();
|
||||||
|
else if (currentAction is Actions.ActionSelect && previousAction != null)
|
||||||
|
RestorePreviousAction();
|
||||||
|
else
|
||||||
|
view.SetAction(typeof(Actions.ActionSelect));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RestorePreviousAction()
|
||||||
|
{
|
||||||
|
var action = previousAction;
|
||||||
|
previousAction = null;
|
||||||
|
|
||||||
|
currentAction.CancelAction();
|
||||||
|
currentAction.DisconnectEvents();
|
||||||
|
|
||||||
|
action.ConnectEvents();
|
||||||
|
currentAction = action;
|
||||||
|
|
||||||
|
view.Status = GetDisplayName(action.GetType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPlateChanged()
|
||||||
|
{
|
||||||
|
if (currentAction == null || !currentAction.SurvivesPlateChange)
|
||||||
|
view.SetAction(typeof(Actions.ActionSelect));
|
||||||
|
else
|
||||||
|
currentAction.OnPlateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
if (currentAction != null)
|
||||||
|
{
|
||||||
|
currentAction.CancelAction();
|
||||||
|
currentAction.DisconnectEvents();
|
||||||
|
currentAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDisplayName(Type type)
|
||||||
|
{
|
||||||
|
var attributes = type.GetCustomAttributes(true);
|
||||||
|
foreach (var attr in attributes)
|
||||||
|
{
|
||||||
|
if (attr is DisplayNameAttribute displayNameAttr)
|
||||||
|
return displayNameAttr.DisplayName;
|
||||||
|
}
|
||||||
|
return type.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,12 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
public static void DrawProgram(Graphics g, DrawControl view, Program pgm, ref Vector pos,
|
public static void DrawProgram(Graphics g, DrawControl view, Program pgm, ref Vector pos,
|
||||||
Pen pen, double spacing, float arrowSize)
|
Pen pen, double spacing, float arrowSize)
|
||||||
|
{
|
||||||
|
DrawProgram(g, view, pgm, pos, ref pos, pen, spacing, arrowSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawProgram(Graphics g, DrawControl view, Program pgm, Vector basePos, ref Vector pos,
|
||||||
|
Pen pen, double spacing, float arrowSize)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < pgm.Length; ++i)
|
for (var i = 0; i < pgm.Length; ++i)
|
||||||
{
|
{
|
||||||
@@ -18,7 +24,11 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
var subpgm = (SubProgramCall)code;
|
var subpgm = (SubProgramCall)code;
|
||||||
if (subpgm.Program != null)
|
if (subpgm.Program != null)
|
||||||
DrawProgram(g, view, subpgm.Program, ref pos, pen, spacing, arrowSize);
|
{
|
||||||
|
var holeBase = basePos + subpgm.Offset;
|
||||||
|
pos = holeBase;
|
||||||
|
DrawProgram(g, view, subpgm.Program, holeBase, ref pos, pen, spacing, arrowSize);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +36,7 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
var endpt = pgm.Mode == Mode.Incremental
|
var endpt = pgm.Mode == Mode.Incremental
|
||||||
? motion.EndPoint + pos
|
? motion.EndPoint + pos
|
||||||
: motion.EndPoint;
|
: motion.EndPoint + basePos;
|
||||||
|
|
||||||
if (code.Type == CodeType.LinearMove)
|
if (code.Type == CodeType.LinearMove)
|
||||||
{
|
{
|
||||||
@@ -41,7 +51,7 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
var center = pgm.Mode == Mode.Incremental
|
var center = pgm.Mode == Mode.Incremental
|
||||||
? arc.CenterPoint + pos
|
? arc.CenterPoint + pos
|
||||||
: arc.CenterPoint;
|
: arc.CenterPoint + basePos;
|
||||||
DrawArcArrows(g, view, pos, endpt, center, arc.Rotation, pen, spacing, arrowSize);
|
DrawArcArrows(g, view, pos, endpt, center, arc.Rotation, pen, spacing, arrowSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Controls
|
||||||
|
{
|
||||||
|
internal class CutOffHandler
|
||||||
|
{
|
||||||
|
private readonly PlateView view;
|
||||||
|
private Dictionary<Part, Geometry.Entity> dragPerimeterCache;
|
||||||
|
|
||||||
|
public CutOffHandler(PlateView view)
|
||||||
|
{
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDragging { get; private set; }
|
||||||
|
|
||||||
|
public CutOff TryStartDrag(Vector point, double tolerance)
|
||||||
|
{
|
||||||
|
var hitCutOff = GetCutOffAtPoint(point, tolerance);
|
||||||
|
if (hitCutOff == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
IsDragging = true;
|
||||||
|
dragPerimeterCache = Plate.BuildPerimeterCache(view.Plate);
|
||||||
|
return hitCutOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateDrag(Vector currentPoint, CutOff cutOff)
|
||||||
|
{
|
||||||
|
if (!IsDragging || cutOff == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (cutOff.Axis == CutOffAxis.Vertical)
|
||||||
|
cutOff.Position = new Vector(currentPoint.X, cutOff.Position.Y);
|
||||||
|
else
|
||||||
|
cutOff.Position = new Vector(cutOff.Position.X, currentPoint.Y);
|
||||||
|
|
||||||
|
cutOff.Regenerate(view.Plate, view.CutOffSettings, dragPerimeterCache);
|
||||||
|
view.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndDrag()
|
||||||
|
{
|
||||||
|
if (!IsDragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IsDragging = false;
|
||||||
|
dragPerimeterCache = null;
|
||||||
|
view.Plate.RegenerateCutOffs(view.CutOffSettings);
|
||||||
|
view.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
|
||||||
|
{
|
||||||
|
if (view.Plate?.CutOffs == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
foreach (var cutoff in view.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 Line(rapid.EndPoint, linear.EndPoint);
|
||||||
|
if (line.ClosestPointTo(point).DistanceTo(point) <= tolerance)
|
||||||
|
return cutoff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ namespace OpenNest.Controls
|
|||||||
{ "None", "Line", "Arc", "Line + Arc", "Clean Hole", "Line + Line" };
|
{ "None", "Line", "Arc", "Line + Arc", "Clean Hole", "Line + Line" };
|
||||||
|
|
||||||
private static readonly string[] LeadOutTypes =
|
private static readonly string[] LeadOutTypes =
|
||||||
{ "None", "Line", "Arc", "Microtab" };
|
{ "None", "Line", "Arc" };
|
||||||
|
|
||||||
private readonly TabControl tabControl;
|
private readonly TabControl tabControl;
|
||||||
private readonly ComboBox cboExternalLeadIn, cboExternalLeadOut;
|
private readonly ComboBox cboExternalLeadIn, cboExternalLeadOut;
|
||||||
@@ -28,6 +28,9 @@ namespace OpenNest.Controls
|
|||||||
private readonly NumericUpDown nudAutoTabMax;
|
private readonly NumericUpDown nudAutoTabMax;
|
||||||
private readonly NumericUpDown nudPierceClearance;
|
private readonly NumericUpDown nudPierceClearance;
|
||||||
|
|
||||||
|
private readonly CheckBox chkRoundLeadInAngles;
|
||||||
|
private readonly NumericUpDown nudLeadInAngleIncrement;
|
||||||
|
|
||||||
private readonly Button btnAutoAssign;
|
private readonly Button btnAutoAssign;
|
||||||
|
|
||||||
private bool suppressEvents;
|
private bool suppressEvents;
|
||||||
@@ -162,7 +165,7 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
HeaderText = "Pierce",
|
HeaderText = "Pierce",
|
||||||
Dock = DockStyle.Top,
|
Dock = DockStyle.Top,
|
||||||
ExpandedHeight = 60,
|
ExpandedHeight = 90,
|
||||||
IsExpanded = true
|
IsExpanded = true
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,6 +179,34 @@ namespace OpenNest.Controls
|
|||||||
nudPierceClearance = CreateNumeric(130, 3, 0.0625, 0.0625);
|
nudPierceClearance = CreateNumeric(130, 3, 0.0625, 0.0625);
|
||||||
piercePanel.ContentPanel.Controls.Add(nudPierceClearance);
|
piercePanel.ContentPanel.Controls.Add(nudPierceClearance);
|
||||||
|
|
||||||
|
chkRoundLeadInAngles = new CheckBox
|
||||||
|
{
|
||||||
|
Text = "Round Lead-In Angles",
|
||||||
|
Location = new Point(12, 32),
|
||||||
|
AutoSize = true
|
||||||
|
};
|
||||||
|
chkRoundLeadInAngles.CheckedChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
nudLeadInAngleIncrement.Enabled = chkRoundLeadInAngles.Checked;
|
||||||
|
OnParametersChanged();
|
||||||
|
};
|
||||||
|
piercePanel.ContentPanel.Controls.Add(chkRoundLeadInAngles);
|
||||||
|
|
||||||
|
piercePanel.ContentPanel.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
Text = "Increment:",
|
||||||
|
Location = new Point(175, 34),
|
||||||
|
AutoSize = true
|
||||||
|
});
|
||||||
|
|
||||||
|
nudLeadInAngleIncrement = CreateNumeric(245, 31, 5, 1);
|
||||||
|
nudLeadInAngleIncrement.DecimalPlaces = 0;
|
||||||
|
nudLeadInAngleIncrement.Minimum = 1;
|
||||||
|
nudLeadInAngleIncrement.Maximum = 90;
|
||||||
|
nudLeadInAngleIncrement.Enabled = false;
|
||||||
|
nudLeadInAngleIncrement.ValueChanged += (s, e) => OnParametersChanged();
|
||||||
|
piercePanel.ContentPanel.Controls.Add(nudLeadInAngleIncrement);
|
||||||
|
|
||||||
// Auto-Assign button — wrapped in a panel for Dock.Top with padding
|
// Auto-Assign button — wrapped in a panel for Dock.Top with padding
|
||||||
btnAutoAssign = new Button
|
btnAutoAssign = new Button
|
||||||
{
|
{
|
||||||
@@ -218,6 +249,8 @@ namespace OpenNest.Controls
|
|||||||
TabsEnabled = chkTabsEnabled.Checked,
|
TabsEnabled = chkTabsEnabled.Checked,
|
||||||
TabConfig = new NormalTab { Size = (double)nudTabWidth.Value },
|
TabConfig = new NormalTab { Size = (double)nudTabWidth.Value },
|
||||||
PierceClearance = (double)nudPierceClearance.Value,
|
PierceClearance = (double)nudPierceClearance.Value,
|
||||||
|
RoundLeadInAngles = chkRoundLeadInAngles.Checked,
|
||||||
|
LeadInAngleIncrement = (double)nudLeadInAngleIncrement.Value,
|
||||||
AutoTabMinSize = (double)nudAutoTabMin.Value,
|
AutoTabMinSize = (double)nudAutoTabMin.Value,
|
||||||
AutoTabMaxSize = (double)nudAutoTabMax.Value
|
AutoTabMaxSize = (double)nudAutoTabMax.Value
|
||||||
};
|
};
|
||||||
@@ -238,6 +271,9 @@ namespace OpenNest.Controls
|
|||||||
if (p.TabConfig != null)
|
if (p.TabConfig != null)
|
||||||
nudTabWidth.Value = (decimal)p.TabConfig.Size;
|
nudTabWidth.Value = (decimal)p.TabConfig.Size;
|
||||||
nudPierceClearance.Value = (decimal)p.PierceClearance;
|
nudPierceClearance.Value = (decimal)p.PierceClearance;
|
||||||
|
chkRoundLeadInAngles.Checked = p.RoundLeadInAngles;
|
||||||
|
nudLeadInAngleIncrement.Value = (decimal)p.LeadInAngleIncrement;
|
||||||
|
nudLeadInAngleIncrement.Enabled = p.RoundLeadInAngles;
|
||||||
nudAutoTabMin.Value = (decimal)p.AutoTabMinSize;
|
nudAutoTabMin.Value = (decimal)p.AutoTabMinSize;
|
||||||
nudAutoTabMax.Value = (decimal)p.AutoTabMaxSize;
|
nudAutoTabMax.Value = (decimal)p.AutoTabMaxSize;
|
||||||
|
|
||||||
@@ -424,9 +460,6 @@ namespace OpenNest.Controls
|
|||||||
case 2:
|
case 2:
|
||||||
AddNumericField(panel, "Radius:", 0.25, ref y, "Radius");
|
AddNumericField(panel, "Radius:", 0.25, ref y, "Radius");
|
||||||
break;
|
break;
|
||||||
case 3:
|
|
||||||
AddNumericField(panel, "Gap Size:", 0.06, ref y, "GapSize");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,10 +546,6 @@ namespace OpenNest.Controls
|
|||||||
combo.SelectedIndex = 2;
|
combo.SelectedIndex = 2;
|
||||||
SetParam(panel, "Radius", arc.Radius);
|
SetParam(panel, "Radius", arc.Radius);
|
||||||
break;
|
break;
|
||||||
case MicrotabLeadOut microtab:
|
|
||||||
combo.SelectedIndex = 3;
|
|
||||||
SetParam(panel, "GapSize", microtab.GapSize);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
combo.SelectedIndex = 0;
|
combo.SelectedIndex = 0;
|
||||||
break;
|
break;
|
||||||
@@ -572,10 +601,6 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
Radius = GetParam(panel, "Radius", 0.25)
|
Radius = GetParam(panel, "Radius", 0.25)
|
||||||
},
|
},
|
||||||
3 => new MicrotabLeadOut
|
|
||||||
{
|
|
||||||
GapSize = GetParam(panel, "GapSize", 0.06)
|
|
||||||
},
|
|
||||||
_ => new NoLeadOut()
|
_ => new NoLeadOut()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
ViewScale = 1.0f;
|
ViewScale = 1.0f;
|
||||||
ViewScaleMin = 0.3f;
|
ViewScaleMin = 0.3f;
|
||||||
ViewScaleMax = 3000;
|
ViewScaleMax = 10000;
|
||||||
origin = new PointF(100, 100);
|
origin = new PointF(100, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
public class DrawingListBox : ListBox
|
public class DrawingListBox : ListBox
|
||||||
{
|
{
|
||||||
|
private const int WM_ERASEBKGND = 0x0014;
|
||||||
|
|
||||||
private readonly Size imageSize;
|
private readonly Size imageSize;
|
||||||
private readonly Font nameFont;
|
private readonly Font nameFont;
|
||||||
private Point lastClickLocation;
|
private Point lastClickLocation;
|
||||||
|
|
||||||
public DrawingListBox()
|
public DrawingListBox()
|
||||||
{
|
{
|
||||||
SetStyle(
|
|
||||||
ControlStyles.AllPaintingInWmPaint |
|
|
||||||
ControlStyles.OptimizedDoubleBuffer, true);
|
|
||||||
|
|
||||||
DrawMode = DrawMode.OwnerDrawFixed;
|
DrawMode = DrawMode.OwnerDrawFixed;
|
||||||
ItemHeight = 85;
|
ItemHeight = 85;
|
||||||
|
|
||||||
@@ -149,6 +147,30 @@ namespace OpenNest.Controls
|
|||||||
base.OnMouseDown(e);
|
base.OnMouseDown(e);
|
||||||
lastClickLocation = e.Location;
|
lastClickLocation = e.Location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void WndProc(ref Message m)
|
||||||
|
{
|
||||||
|
if (m.Msg == WM_ERASEBKGND)
|
||||||
|
{
|
||||||
|
var itemBottom = 0;
|
||||||
|
|
||||||
|
if (Items.Count > 0)
|
||||||
|
{
|
||||||
|
var lastVisible = System.Math.Min(TopIndex + (ClientSize.Height / ItemHeight), Items.Count - 1);
|
||||||
|
itemBottom = GetItemRectangle(lastVisible).Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemBottom < ClientSize.Height)
|
||||||
|
{
|
||||||
|
using var g = Graphics.FromHdc(m.WParam);
|
||||||
|
g.FillRectangle(Brushes.White, 0, itemBottom, ClientSize.Width, ClientSize.Height - itemBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Result = (IntPtr)1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
base.WndProc(ref m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class PointExtensions
|
public static class PointExtensions
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace OpenNest.Controls
|
|||||||
public List<Entity> Entities { get; set; } = new();
|
public List<Entity> Entities { get; set; } = new();
|
||||||
public List<Entity> OriginalEntities { get; set; }
|
public List<Entity> OriginalEntities { get; set; }
|
||||||
public List<Bend> Bends { get; set; } = new();
|
public List<Bend> Bends { get; set; } = new();
|
||||||
|
public HashSet<Guid> SuppressedEntityIds { get; set; }
|
||||||
public Box Bounds { get; set; }
|
public Box Bounds { get; set; }
|
||||||
public int EntityCount { get; set; }
|
public int EntityCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,10 @@ namespace OpenNest.Controls
|
|||||||
Font = new Font("Segoe UI", 9f)
|
Font = new Font("Segoe UI", 9f)
|
||||||
};
|
};
|
||||||
list.ItemCheck += (s, e) =>
|
list.ItemCheck += (s, e) =>
|
||||||
BeginInvoke((Action)(() => FilterChanged?.Invoke(this, EventArgs.Empty)));
|
{
|
||||||
|
if (IsHandleCreated)
|
||||||
|
BeginInvoke((Action)(() => FilterChanged?.Invoke(this, EventArgs.Empty)));
|
||||||
|
};
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,10 +170,11 @@ namespace OpenNest.Controls
|
|||||||
layersList.Items.Clear();
|
layersList.Items.Clear();
|
||||||
var layers = entities
|
var layers = entities
|
||||||
.Where(e => e.Layer != null)
|
.Where(e => e.Layer != null)
|
||||||
.Select(e => e.Layer.Name)
|
.Select(e => e.Layer)
|
||||||
.Distinct();
|
.GroupBy(l => l.Name)
|
||||||
|
.Select(g => g.First());
|
||||||
foreach (var layer in layers)
|
foreach (var layer in layers)
|
||||||
layersList.Items.Add(layer, true); // checked = visible
|
layersList.Items.Add(layer.Name, layer.IsVisible);
|
||||||
|
|
||||||
layersPanel.HeaderText = $"Layers ({layersList.Items.Count})";
|
layersPanel.HeaderText = $"Layers ({layersList.Items.Count})";
|
||||||
|
|
||||||
@@ -188,10 +192,10 @@ namespace OpenNest.Controls
|
|||||||
// Line Types
|
// Line Types
|
||||||
lineTypesList.Items.Clear();
|
lineTypesList.Items.Clear();
|
||||||
var lineTypes = entities
|
var lineTypes = entities
|
||||||
.Select(e => e.LineTypeName ?? "Continuous")
|
.GroupBy(e => e.LineTypeName ?? "Continuous")
|
||||||
.Distinct();
|
.Select(g => new { Name = g.Key, Visible = g.Any(e => e.IsVisible) });
|
||||||
foreach (var lt in lineTypes)
|
foreach (var lt in lineTypes)
|
||||||
lineTypesList.Items.Add(lt, true); // checked = visible
|
lineTypesList.Items.Add(lt.Name, lt.Visible);
|
||||||
|
|
||||||
lineTypesPanel.HeaderText = $"Line Types ({lineTypesList.Items.Count})";
|
lineTypesPanel.HeaderText = $"Line Types ({lineTypesList.Items.Count})";
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ namespace OpenNest.Controls
|
|||||||
if (program == null || program.Codes.Count == 0)
|
if (program == null || program.Codes.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var activePen = cutoff == view.SelectedCutOff ? selectedPen : pen;
|
var activePen = view.Selection.SelectedCutOffs.Contains(cutoff) ? selectedPen : pen;
|
||||||
|
|
||||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||||
{
|
{
|
||||||
@@ -395,8 +395,8 @@ namespace OpenNest.Controls
|
|||||||
var piercePoint = GetFirstPiercePoint(pgm, part.Location);
|
var piercePoint = GetFirstPiercePoint(pgm, part.Location);
|
||||||
DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen);
|
DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen);
|
||||||
|
|
||||||
pos = part.Location;
|
pos = piercePoint;
|
||||||
DrawRapids(g, pgm, ref pos, skipFirstRapid: true);
|
DrawRapids(g, pgm, part.Location, ref pos, skipFirstRapid: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,17 +404,18 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
for (var i = 0; i < pgm.Length; i++)
|
for (var i = 0; i < pgm.Length; i++)
|
||||||
{
|
{
|
||||||
|
if (pgm[i] is SubProgramCall call && call.Program != null)
|
||||||
|
return GetFirstPiercePoint(call.Program, partLocation + call.Offset);
|
||||||
|
|
||||||
if (pgm[i] is Motion motion)
|
if (pgm[i] is Motion motion)
|
||||||
{
|
{
|
||||||
if (pgm.Mode == Mode.Incremental)
|
return motion.EndPoint + partLocation;
|
||||||
return motion.EndPoint + partLocation;
|
|
||||||
return motion.EndPoint;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return partLocation;
|
return partLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawRapids(Graphics g, Program pgm, ref Vector pos, bool skipFirstRapid = false)
|
private void DrawRapids(Graphics g, Program pgm, Vector basePos, ref Vector pos, bool skipFirstRapid = false)
|
||||||
{
|
{
|
||||||
var firstRapidSkipped = false;
|
var firstRapidSkipped = false;
|
||||||
|
|
||||||
@@ -422,49 +423,49 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
var code = pgm[i];
|
var code = pgm[i];
|
||||||
|
|
||||||
if (code.Type == CodeType.SubProgramCall)
|
if (code is SubProgramCall { Program: { } program } call)
|
||||||
{
|
{
|
||||||
var subpgm = (SubProgramCall)code;
|
// A SubProgramCall is a coordinate-frame shift, not a physical
|
||||||
var program = subpgm.Program;
|
// rapid to the hole center. The Cincinnati post emits it as a
|
||||||
|
// G52 bracket, so the physical rapid is the sub-program's first
|
||||||
|
// motion, which goes straight from here to the lead-in pierce.
|
||||||
|
// Look ahead for that pierce point and draw the direct rapid,
|
||||||
|
// then recurse with skipFirstRapid so the sub doesn't also draw
|
||||||
|
// its first rapid on top. See docs/cincinnati-post-output.md.
|
||||||
|
var holeBase = basePos + call.Offset;
|
||||||
|
var firstPierce = GetFirstPiercePoint(program, holeBase);
|
||||||
|
|
||||||
if (program != null)
|
if (ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
|
||||||
DrawRapids(g, program, ref pos);
|
DrawLine(g, pos, firstPierce, view.ColorScheme.RapidPen);
|
||||||
|
|
||||||
|
var subPos = holeBase;
|
||||||
|
DrawRapids(g, program, holeBase, ref subPos, skipFirstRapid: true);
|
||||||
|
pos = subPos;
|
||||||
}
|
}
|
||||||
else
|
else if (code is Motion motion)
|
||||||
{
|
{
|
||||||
var motion = code as Motion;
|
var endpt = pgm.Mode == Mode.Incremental
|
||||||
|
? motion.EndPoint + pos
|
||||||
|
: motion.EndPoint;
|
||||||
|
|
||||||
if (motion != null)
|
if (code.Type == CodeType.RapidMove && ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
|
||||||
{
|
DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
|
||||||
if (pgm.Mode == Mode.Incremental)
|
|
||||||
{
|
|
||||||
var endpt = motion.EndPoint + pos;
|
|
||||||
|
|
||||||
if (code.Type == CodeType.RapidMove)
|
pos = endpt;
|
||||||
{
|
|
||||||
if (skipFirstRapid && !firstRapidSkipped)
|
|
||||||
firstRapidSkipped = true;
|
|
||||||
else
|
|
||||||
DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
|
|
||||||
}
|
|
||||||
pos = endpt;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (code.Type == CodeType.RapidMove)
|
|
||||||
{
|
|
||||||
if (skipFirstRapid && !firstRapidSkipped)
|
|
||||||
firstRapidSkipped = true;
|
|
||||||
else
|
|
||||||
DrawLine(g, pos, motion.EndPoint, view.ColorScheme.RapidPen);
|
|
||||||
}
|
|
||||||
pos = motion.EndPoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldDrawRapid(bool skipFirstRapid, ref bool firstRapidSkipped)
|
||||||
|
{
|
||||||
|
if (skipFirstRapid && !firstRapidSkipped)
|
||||||
|
{
|
||||||
|
firstRapidSkipped = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawAllPiercePoints(Graphics g)
|
private void DrawAllPiercePoints(Graphics g)
|
||||||
{
|
{
|
||||||
using var brush = new SolidBrush(Color.Red);
|
using var brush = new SolidBrush(Color.Red);
|
||||||
@@ -475,11 +476,11 @@ namespace OpenNest.Controls
|
|||||||
var part = view.Plate.Parts[i];
|
var part = view.Plate.Parts[i];
|
||||||
var pgm = part.Program;
|
var pgm = part.Program;
|
||||||
var pos = part.Location;
|
var pos = part.Location;
|
||||||
DrawProgramPiercePoints(g, pgm, ref pos, brush, pen);
|
DrawProgramPiercePoints(g, pgm, part.Location, ref pos, brush, pen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawProgramPiercePoints(Graphics g, Program pgm, ref Vector pos, Brush brush, Pen pen)
|
private void DrawProgramPiercePoints(Graphics g, Program pgm, Vector basePos, ref Vector pos, Brush brush, Pen pen)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < pgm.Length; ++i)
|
for (var i = 0; i < pgm.Length; ++i)
|
||||||
{
|
{
|
||||||
@@ -489,7 +490,11 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
var subpgm = (SubProgramCall)code;
|
var subpgm = (SubProgramCall)code;
|
||||||
if (subpgm.Program != null)
|
if (subpgm.Program != null)
|
||||||
DrawProgramPiercePoints(g, subpgm.Program, ref pos, brush, pen);
|
{
|
||||||
|
var holeBase = basePos + subpgm.Offset;
|
||||||
|
pos = holeBase;
|
||||||
|
DrawProgramPiercePoints(g, subpgm.Program, holeBase, ref pos, brush, pen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -498,7 +503,7 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
var endpt = pgm.Mode == Mode.Incremental
|
var endpt = pgm.Mode == Mode.Incremental
|
||||||
? motion.EndPoint + pos
|
? motion.EndPoint + pos
|
||||||
: motion.EndPoint;
|
: motion.EndPoint + basePos;
|
||||||
|
|
||||||
if (code.Type == CodeType.RapidMove)
|
if (code.Type == CodeType.RapidMove)
|
||||||
{
|
{
|
||||||
|
|||||||
+146
-413
@@ -1,5 +1,4 @@
|
|||||||
using OpenNest.Actions;
|
using OpenNest.Actions;
|
||||||
using OpenNest.CNC;
|
|
||||||
using OpenNest.Collections;
|
using OpenNest.Collections;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Forms;
|
using OpenNest.Forms;
|
||||||
@@ -8,7 +7,6 @@ using OpenNest.Math;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
@@ -16,31 +14,30 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using Action = OpenNest.Actions.Action;
|
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace OpenNest.Controls
|
namespace OpenNest.Controls
|
||||||
{
|
{
|
||||||
public class PlateView : DrawControl
|
public class PlateView : DrawControl
|
||||||
{
|
{
|
||||||
private readonly Font programIdFont;
|
|
||||||
private readonly Timer redrawTimer;
|
private readonly Timer redrawTimer;
|
||||||
|
|
||||||
private string status;
|
private string status;
|
||||||
private Plate plate;
|
private Plate plate;
|
||||||
private Action currentAction;
|
private ActionManager actionManager;
|
||||||
private Action previousAction;
|
|
||||||
private CutOffSettings cutOffSettings = new CutOffSettings();
|
private CutOffSettings cutOffSettings = new CutOffSettings();
|
||||||
private CutOff selectedCutOff;
|
private SelectionManager selection;
|
||||||
private bool draggingCutOff;
|
private CutOffHandler cutOffHandler;
|
||||||
private Dictionary<Part, Geometry.Entity> dragPerimeterCache;
|
private PreviewManager previewManager;
|
||||||
protected List<LayoutPart> parts;
|
protected List<LayoutPart> parts;
|
||||||
private List<LayoutPart> stationaryParts = new List<LayoutPart>();
|
|
||||||
private List<LayoutPart> activeParts = new List<LayoutPart>();
|
|
||||||
private Point middleMouseDownPoint;
|
private Point middleMouseDownPoint;
|
||||||
private Box activeWorkArea;
|
private Box activeWorkArea;
|
||||||
private List<Box> debugRemnants;
|
private List<Box> debugRemnants;
|
||||||
private PlateRenderer renderer;
|
private PlateRenderer renderer;
|
||||||
|
private LayoutPart hoveredPart;
|
||||||
|
private Point hoverPoint;
|
||||||
|
private bool showTooltip;
|
||||||
|
private Timer hoverTimer;
|
||||||
|
|
||||||
public Box ActiveWorkArea
|
public Box ActiveWorkArea
|
||||||
{
|
{
|
||||||
@@ -64,13 +61,23 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
public List<int> DebugRemnantPriorities { get; set; }
|
public List<int> DebugRemnantPriorities { get; set; }
|
||||||
|
|
||||||
public List<LayoutPart> SelectedParts;
|
public List<LayoutPart> SelectedParts => selection.SelectedParts;
|
||||||
public ReadOnlyCollection<LayoutPart> Parts;
|
public ReadOnlyCollection<LayoutPart> Parts => new ReadOnlyCollection<LayoutPart>(parts);
|
||||||
|
|
||||||
|
internal SelectionManager Selection => selection;
|
||||||
|
internal CutOffHandler CutOffs => cutOffHandler;
|
||||||
|
internal ActionManager Actions => actionManager;
|
||||||
|
internal PreviewManager Previews => previewManager;
|
||||||
|
|
||||||
public event EventHandler<ItemAddedEventArgs<Part>> PartAdded;
|
public event EventHandler<ItemAddedEventArgs<Part>> PartAdded;
|
||||||
public event EventHandler<ItemRemovedEventArgs<Part>> PartRemoved;
|
public event EventHandler<ItemRemovedEventArgs<Part>> PartRemoved;
|
||||||
public event EventHandler StatusChanged;
|
public event EventHandler StatusChanged;
|
||||||
public event EventHandler SelectionChanged;
|
|
||||||
|
public event EventHandler SelectionChanged
|
||||||
|
{
|
||||||
|
add => selection.SelectionChanged += value;
|
||||||
|
remove => selection.SelectionChanged -= value;
|
||||||
|
}
|
||||||
|
|
||||||
public PlateView()
|
public PlateView()
|
||||||
: this(ColorScheme.Default)
|
: this(ColorScheme.Default)
|
||||||
@@ -80,11 +87,11 @@ namespace OpenNest.Controls
|
|||||||
public PlateView(ColorScheme colorScheme)
|
public PlateView(ColorScheme colorScheme)
|
||||||
{
|
{
|
||||||
Plate = new Plate(60, 120);
|
Plate = new Plate(60, 120);
|
||||||
programIdFont = new Font(DefaultFont, FontStyle.Bold | FontStyle.Underline);
|
|
||||||
origin = new PointF();
|
origin = new PointF();
|
||||||
parts = new List<LayoutPart>();
|
parts = new List<LayoutPart>();
|
||||||
Parts = new ReadOnlyCollection<LayoutPart>(parts);
|
selection = new SelectionManager(this);
|
||||||
SelectedParts = new List<LayoutPart>();
|
cutOffHandler = new CutOffHandler(this);
|
||||||
|
previewManager = new PreviewManager(this);
|
||||||
|
|
||||||
redrawTimer = new Timer()
|
redrawTimer = new Timer()
|
||||||
{
|
{
|
||||||
@@ -94,6 +101,9 @@ namespace OpenNest.Controls
|
|||||||
};
|
};
|
||||||
redrawTimer.Elapsed += redrawTimer_Elapsed;
|
redrawTimer.Elapsed += redrawTimer_Elapsed;
|
||||||
|
|
||||||
|
hoverTimer = new Timer() { AutoReset = false, Interval = 1000 };
|
||||||
|
hoverTimer.Elapsed += hoverTimer_Elapsed;
|
||||||
|
|
||||||
SetStyle(
|
SetStyle(
|
||||||
ControlStyles.AllPaintingInWmPaint |
|
ControlStyles.AllPaintingInWmPaint |
|
||||||
ControlStyles.OptimizedDoubleBuffer |
|
ControlStyles.OptimizedDoubleBuffer |
|
||||||
@@ -115,7 +125,8 @@ namespace OpenNest.Controls
|
|||||||
DrawOffset = false;
|
DrawOffset = false;
|
||||||
FillParts = true;
|
FillParts = true;
|
||||||
renderer = new PlateRenderer(this);
|
renderer = new PlateRenderer(this);
|
||||||
SetAction(typeof(ActionSelect));
|
actionManager = new ActionManager(this);
|
||||||
|
actionManager.SetAction(typeof(ActionSelect));
|
||||||
|
|
||||||
UpdateMatrix();
|
UpdateMatrix();
|
||||||
}
|
}
|
||||||
@@ -148,14 +159,9 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
internal List<LayoutPart> LayoutParts => parts;
|
internal List<LayoutPart> LayoutParts => parts;
|
||||||
|
|
||||||
internal IReadOnlyList<LayoutPart> PreviewParts =>
|
internal IReadOnlyList<LayoutPart> PreviewParts => previewManager.PreviewParts;
|
||||||
activeParts.Count > 0 ? activeParts : stationaryParts;
|
internal Brush PreviewBrush => previewManager.PreviewBrush;
|
||||||
|
internal Pen PreviewPen => previewManager.PreviewPen;
|
||||||
internal Brush PreviewBrush =>
|
|
||||||
activeParts.Count > 0 ? ColorScheme.ActivePreviewPartBrush : ColorScheme.PreviewPartBrush;
|
|
||||||
|
|
||||||
internal Pen PreviewPen =>
|
|
||||||
activeParts.Count > 0 ? ColorScheme.ActivePreviewPartPen : ColorScheme.PreviewPartPen;
|
|
||||||
|
|
||||||
internal RectangleF GetViewBounds() =>
|
internal RectangleF GetViewBounds() =>
|
||||||
new RectangleF(-origin.X, -origin.Y, Width, Height);
|
new RectangleF(-origin.X, -origin.Y, Width, Height);
|
||||||
@@ -173,16 +179,6 @@ namespace OpenNest.Controls
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CutOff SelectedCutOff
|
|
||||||
{
|
|
||||||
get => selectedCutOff;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
selectedCutOff = value;
|
|
||||||
Invalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public double RotateIncrementAngle { get; set; }
|
public double RotateIncrementAngle { get; set; }
|
||||||
|
|
||||||
public double OffsetIncrementDistance { get; set; }
|
public double OffsetIncrementDistance { get; set; }
|
||||||
@@ -200,9 +196,8 @@ namespace OpenNest.Controls
|
|||||||
plate.PartAdded -= plate_PartAdded;
|
plate.PartAdded -= plate_PartAdded;
|
||||||
plate.PartRemoved -= plate_PartRemoved;
|
plate.PartRemoved -= plate_PartRemoved;
|
||||||
parts.Clear();
|
parts.Clear();
|
||||||
stationaryParts.Clear();
|
previewManager.Clear();
|
||||||
activeParts.Clear();
|
selection.Clear();
|
||||||
SelectedParts.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
plate = p;
|
plate = p;
|
||||||
@@ -212,10 +207,7 @@ namespace OpenNest.Controls
|
|||||||
foreach (var part in plate.Parts)
|
foreach (var part in plate.Parts)
|
||||||
parts.Add(LayoutPart.Create(part, this));
|
parts.Add(LayoutPart.Create(part, this));
|
||||||
|
|
||||||
if (currentAction == null || !currentAction.SurvivesPlateChange)
|
actionManager?.OnPlateChanged();
|
||||||
SetAction(typeof(ActionSelect));
|
|
||||||
else
|
|
||||||
currentAction.OnPlateChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Status
|
public string Status
|
||||||
@@ -233,7 +225,6 @@ namespace OpenNest.Controls
|
|||||||
protected override void OnMouseEnter(EventArgs e)
|
protected override void OnMouseEnter(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnMouseEnter(e);
|
base.OnMouseEnter(e);
|
||||||
if (!Focused) Focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDragEnter(DragEventArgs drgevent)
|
protected override void OnDragEnter(DragEventArgs drgevent)
|
||||||
@@ -257,22 +248,25 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
protected override void OnMouseDown(MouseEventArgs e)
|
protected override void OnMouseDown(MouseEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (!Focused) Focus();
|
||||||
|
|
||||||
if (e.Button == MouseButtons.Middle)
|
if (e.Button == MouseButtons.Middle)
|
||||||
middleMouseDownPoint = e.Location;
|
middleMouseDownPoint = e.Location;
|
||||||
|
|
||||||
if (e.Button == MouseButtons.Left && currentAction is ActionSelect)
|
if (e.Button == MouseButtons.Left && actionManager.CurrentAction is ActionSelect)
|
||||||
{
|
{
|
||||||
var hitCutOff = GetCutOffAtPoint(CurrentPoint, 5.0 / ViewScale);
|
var hitCutOff = cutOffHandler.TryStartDrag(CurrentPoint, 5.0 / ViewScale);
|
||||||
if (hitCutOff != null)
|
if (hitCutOff != null)
|
||||||
{
|
{
|
||||||
SelectedCutOff = hitCutOff;
|
selection.DeselectParts();
|
||||||
draggingCutOff = true;
|
selection.SelectedCutOffs.Clear();
|
||||||
dragPerimeterCache = Plate.BuildPerimeterCache(Plate);
|
selection.SelectedCutOffs.Add(hitCutOff);
|
||||||
|
Invalidate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SelectedCutOff = null;
|
selection.DeselectCutOffs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,17 +282,14 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
if (dx * dx + dy * dy < 25)
|
if (dx * dx + dy * dy < 25)
|
||||||
{
|
{
|
||||||
RotateSelectedParts(Angle.ToRadians(90));
|
selection.RotateSelectedParts(Angle.ToRadians(90));
|
||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (draggingCutOff && selectedCutOff != null)
|
if (cutOffHandler.IsDragging && selection.SelectedCutOffs.Count > 0)
|
||||||
{
|
{
|
||||||
draggingCutOff = false;
|
cutOffHandler.EndDrag();
|
||||||
dragPerimeterCache = null;
|
|
||||||
Plate.RegenerateCutOffs(cutOffSettings);
|
|
||||||
Invalidate();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +310,7 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
var angle = Angle.ToRadians((e.Delta > 0 ? -increment : increment) * multiplier);
|
var angle = Angle.ToRadians((e.Delta > 0 ? -increment : increment) * multiplier);
|
||||||
|
|
||||||
RotateSelectedParts(angle);
|
selection.RotateSelectedParts(angle);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -358,18 +349,30 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
lastPoint = e.Location;
|
lastPoint = e.Location;
|
||||||
|
|
||||||
if (draggingCutOff && selectedCutOff != null)
|
if (cutOffHandler.IsDragging && selection.SelectedCutOffs.Count > 0)
|
||||||
{
|
{
|
||||||
if (selectedCutOff.Axis == CutOffAxis.Vertical)
|
cutOffHandler.UpdateDrag(CurrentPoint, selection.SelectedCutOffs[0]);
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.Button == MouseButtons.None && actionManager.CurrentAction is ActionSelect)
|
||||||
|
{
|
||||||
|
hoverPoint = e.Location;
|
||||||
|
showTooltip = false;
|
||||||
|
hoverTimer.Stop();
|
||||||
|
hoverTimer.Start();
|
||||||
|
|
||||||
|
if (hoveredPart != null)
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
else if (hoveredPart != null || showTooltip)
|
||||||
|
{
|
||||||
|
hoveredPart = null;
|
||||||
|
hoverTimer.Stop();
|
||||||
|
showTooltip = false;
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
base.OnMouseMove(e);
|
base.OnMouseMove(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,17 +389,7 @@ namespace OpenNest.Controls
|
|||||||
switch (e.KeyCode)
|
switch (e.KeyCode)
|
||||||
{
|
{
|
||||||
case Keys.Delete:
|
case Keys.Delete:
|
||||||
if (selectedCutOff != null)
|
selection.DeleteSelected();
|
||||||
{
|
|
||||||
Plate.CutOffs.Remove(selectedCutOff);
|
|
||||||
selectedCutOff = null;
|
|
||||||
Plate.RegenerateCutOffs(cutOffSettings);
|
|
||||||
Invalidate();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RemoveSelectedParts();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Keys.F:
|
case Keys.F:
|
||||||
@@ -412,15 +405,7 @@ namespace OpenNest.Controls
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ProcessEscapeKey()
|
public void ProcessEscapeKey() => actionManager.ProcessEscapeKey();
|
||||||
{
|
|
||||||
if (currentAction.IsBusy())
|
|
||||||
currentAction.CancelAction();
|
|
||||||
else if (currentAction is ActionSelect && previousAction != null)
|
|
||||||
RestorePreviousAction();
|
|
||||||
else
|
|
||||||
SetAction(typeof(ActionSelect));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool ProcessDialogKey(Keys keyData)
|
protected override bool ProcessDialogKey(Keys keyData)
|
||||||
{
|
{
|
||||||
@@ -440,22 +425,22 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
case Keys.X:
|
case Keys.X:
|
||||||
case Keys.Shift | Keys.Left:
|
case Keys.Shift | Keys.Left:
|
||||||
PushSelected(PushDirection.Left);
|
selection.PushSelected(PushDirection.Left);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Keys.Shift | Keys.X:
|
case Keys.Shift | Keys.X:
|
||||||
case Keys.Shift | Keys.Right:
|
case Keys.Shift | Keys.Right:
|
||||||
PushSelected(PushDirection.Right);
|
selection.PushSelected(PushDirection.Right);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Keys.Shift | Keys.Y:
|
case Keys.Shift | Keys.Y:
|
||||||
case Keys.Shift | Keys.Up:
|
case Keys.Shift | Keys.Up:
|
||||||
PushSelected(PushDirection.Up);
|
selection.PushSelected(PushDirection.Up);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Keys.Y:
|
case Keys.Y:
|
||||||
case Keys.Shift | Keys.Down:
|
case Keys.Shift | Keys.Down:
|
||||||
PushSelected(PushDirection.Down);
|
selection.PushSelected(PushDirection.Down);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Keys.Right:
|
case Keys.Right:
|
||||||
@@ -496,229 +481,53 @@ namespace OpenNest.Controls
|
|||||||
renderer.DrawDebugRemnants(e.Graphics);
|
renderer.DrawDebugRemnants(e.Graphics);
|
||||||
|
|
||||||
base.OnPaint(e);
|
base.OnPaint(e);
|
||||||
|
|
||||||
|
if (hoveredPart != null && showTooltip)
|
||||||
|
{
|
||||||
|
e.Graphics.ResetTransform();
|
||||||
|
var text = hoveredPart.BasePart.BaseDrawing.Name;
|
||||||
|
var size = e.Graphics.MeasureString(text, Font);
|
||||||
|
var x = hoverPoint.X + 16f;
|
||||||
|
var y = hoverPoint.Y - size.Height - 6f;
|
||||||
|
|
||||||
|
if (x + size.Width + 8 > ClientSize.Width)
|
||||||
|
x = hoverPoint.X - size.Width - 8;
|
||||||
|
if (y < 0)
|
||||||
|
y = hoverPoint.Y + 20;
|
||||||
|
|
||||||
|
var rect = new RectangleF(x, y, size.Width + 6, size.Height + 4);
|
||||||
|
using (var bgBrush = new SolidBrush(Color.FromArgb(230, Color.White)))
|
||||||
|
e.Graphics.FillRectangle(bgBrush, rect);
|
||||||
|
e.Graphics.DrawRectangle(Pens.DimGray, rect.X, rect.Y, rect.Width, rect.Height);
|
||||||
|
e.Graphics.DrawString(text, Font, Brushes.Black, x + 3, y + 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnHandleDestroyed(EventArgs e)
|
protected override void OnHandleDestroyed(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnHandleDestroyed(e);
|
base.OnHandleDestroyed(e);
|
||||||
|
actionManager.Cleanup();
|
||||||
if (currentAction != null)
|
|
||||||
{
|
|
||||||
currentAction.CancelAction();
|
|
||||||
currentAction.DisconnectEvents();
|
|
||||||
currentAction = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Refresh()
|
public override void Refresh()
|
||||||
{
|
{
|
||||||
parts.ForEach(p => p.Update(this));
|
parts.ForEach(p => p.Update(this));
|
||||||
stationaryParts.ForEach(p => p.Update(this));
|
previewManager.Update();
|
||||||
activeParts.ForEach(p => p.Update(this));
|
|
||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
|
public CutOff GetCutOffAtPoint(Vector point, double tolerance) => cutOffHandler.GetCutOffAtPoint(point, tolerance);
|
||||||
{
|
|
||||||
if (Plate?.CutOffs == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
foreach (var cutoff in Plate.CutOffs)
|
public LayoutPart GetPartAtControlPoint(Point pt) => selection.GetPartAtControlPoint(pt);
|
||||||
{
|
public LayoutPart GetPartAtGraphPoint(PointF pt) => selection.GetPartAtGraphPoint(pt);
|
||||||
var program = cutoff.Drawing?.Program;
|
public LayoutPart GetPartAtPoint(Vector pt) => selection.GetPartAtPoint(pt);
|
||||||
if (program == null)
|
public IList<LayoutPart> GetPartsFromWindow(RectangleF rect, SelectionType selectionType) => selection.GetPartsFromWindow(rect, selectionType);
|
||||||
continue;
|
|
||||||
|
|
||||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
public void SetAction(Type type) => actionManager.SetAction(type);
|
||||||
{
|
public void SetAction(Type type, params object[] args) => actionManager.SetAction(type, args);
|
||||||
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;
|
public void AlignSelected(AlignType alignType) => selection.AlignSelected(alignType);
|
||||||
}
|
public void AlignSelected(AlignType alignType, LayoutPart fixedPart) => selection.AlignSelected(alignType, fixedPart);
|
||||||
|
|
||||||
public LayoutPart GetPartAtControlPoint(Point pt)
|
|
||||||
{
|
|
||||||
var pt2 = PointControlToGraph(pt);
|
|
||||||
return GetPartAtGraphPoint(pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LayoutPart GetPartAtGraphPoint(PointF pt)
|
|
||||||
{
|
|
||||||
for (int i = parts.Count - 1; i >= 0; --i)
|
|
||||||
{
|
|
||||||
if (parts[i].Path.IsVisible(pt))
|
|
||||||
return parts[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LayoutPart GetPartAtPoint(Vector pt)
|
|
||||||
{
|
|
||||||
var pt2 = PointWorldToGraph(pt);
|
|
||||||
return GetPartAtGraphPoint(pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<LayoutPart> GetPartsFromWindow(RectangleF rect, SelectionType selectionType)
|
|
||||||
{
|
|
||||||
var list = new List<LayoutPart>();
|
|
||||||
|
|
||||||
if (selectionType == SelectionType.Intersect)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < parts.Count; ++i)
|
|
||||||
{
|
|
||||||
var part = parts[i];
|
|
||||||
var path = part.Path;
|
|
||||||
var region = new Region(path);
|
|
||||||
|
|
||||||
if (region.IsVisible(rect))
|
|
||||||
list.Add(part);
|
|
||||||
|
|
||||||
region.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (int i = 0; i < parts.Count; ++i)
|
|
||||||
{
|
|
||||||
var part = parts[i];
|
|
||||||
var path = part.Path;
|
|
||||||
var bounds = path.GetBounds();
|
|
||||||
|
|
||||||
if (rect.Contains(bounds))
|
|
||||||
list.Add(part);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetAction(Type type)
|
|
||||||
{
|
|
||||||
var action = Activator.CreateInstance(type, this) as Action;
|
|
||||||
|
|
||||||
if (action == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (currentAction != null)
|
|
||||||
{
|
|
||||||
if (type == typeof(ActionSelect) && !(currentAction is ActionSelect))
|
|
||||||
previousAction = currentAction;
|
|
||||||
else
|
|
||||||
previousAction = null;
|
|
||||||
|
|
||||||
currentAction.CancelAction();
|
|
||||||
currentAction.DisconnectEvents();
|
|
||||||
currentAction = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentAction = action;
|
|
||||||
|
|
||||||
Status = GetDisplayName(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetAction(Type type, params object[] args)
|
|
||||||
{
|
|
||||||
if (currentAction != null)
|
|
||||||
{
|
|
||||||
previousAction = null;
|
|
||||||
currentAction.CancelAction();
|
|
||||||
currentAction.DisconnectEvents();
|
|
||||||
currentAction = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.Resize(ref args, args.Length + 1);
|
|
||||||
|
|
||||||
// shift all elements to the right
|
|
||||||
for (int i = args.Length - 2; i >= 0; i--)
|
|
||||||
args[i + 1] = args[i];
|
|
||||||
|
|
||||||
// set the first argument to this.
|
|
||||||
args[0] = this;
|
|
||||||
|
|
||||||
var action = Activator.CreateInstance(type, args) as Action;
|
|
||||||
|
|
||||||
if (action == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
currentAction = action;
|
|
||||||
|
|
||||||
Status = GetDisplayName(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RestorePreviousAction()
|
|
||||||
{
|
|
||||||
var action = previousAction;
|
|
||||||
previousAction = null;
|
|
||||||
|
|
||||||
currentAction.CancelAction();
|
|
||||||
currentAction.DisconnectEvents();
|
|
||||||
|
|
||||||
action.ConnectEvents();
|
|
||||||
currentAction = action;
|
|
||||||
|
|
||||||
Status = GetDisplayName(action.GetType());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AlignSelected(AlignType alignType)
|
|
||||||
{
|
|
||||||
if (SelectedParts.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
AlignSelected(alignType, SelectedParts[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AlignSelected(AlignType alignType, LayoutPart fixedPart)
|
|
||||||
{
|
|
||||||
switch (alignType)
|
|
||||||
{
|
|
||||||
case AlignType.Bottom:
|
|
||||||
Align.Bottom(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AlignType.Horizontally:
|
|
||||||
Align.Horizontally(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AlignType.Left:
|
|
||||||
Align.Left(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AlignType.Right:
|
|
||||||
Align.Right(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AlignType.Top:
|
|
||||||
Align.Top(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AlignType.Vertically:
|
|
||||||
Align.Vertically(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AlignType.EvenlySpaceHorizontally:
|
|
||||||
Align.EvenlyDistributeHorizontally(SelectedParts.Select(p => p.BasePart).ToList());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AlignType.EvenlySpaceVertically:
|
|
||||||
Align.EvenlyDistributeVertically(SelectedParts.Select(p => p.BasePart).ToList());
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectedParts.ForEach(p => p.IsDirty = true);
|
|
||||||
Invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddPartFromDrawing(Drawing dwg, Vector location)
|
public void AddPartFromDrawing(Drawing dwg, Vector location)
|
||||||
{
|
{
|
||||||
@@ -731,51 +540,10 @@ namespace OpenNest.Controls
|
|||||||
Plate.Parts.Add(part);
|
Plate.Parts.Add(part);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetStationaryParts(List<Part> parts)
|
public void SetStationaryParts(List<Part> parts) => previewManager.SetStationaryParts(parts);
|
||||||
{
|
public void SetActiveParts(List<Part> parts) => previewManager.SetActiveParts(parts);
|
||||||
stationaryParts.Clear();
|
public void ClearPreviewParts() => previewManager.ClearPreviewParts();
|
||||||
activeParts.Clear();
|
public void AcceptPreviewParts(List<Part> parts) => previewManager.AcceptPreviewParts(parts);
|
||||||
|
|
||||||
if (parts != null)
|
|
||||||
{
|
|
||||||
foreach (var part in parts)
|
|
||||||
stationaryParts.Add(LayoutPart.Create(part, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
Invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetActiveParts(List<Part> parts)
|
|
||||||
{
|
|
||||||
activeParts.Clear();
|
|
||||||
|
|
||||||
if (parts != null)
|
|
||||||
{
|
|
||||||
foreach (var part in parts)
|
|
||||||
activeParts.Add(LayoutPart.Create(part, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
Invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearPreviewParts()
|
|
||||||
{
|
|
||||||
stationaryParts.Clear();
|
|
||||||
activeParts.Clear();
|
|
||||||
Invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AcceptPreviewParts(List<Part> parts)
|
|
||||||
{
|
|
||||||
if (parts != null)
|
|
||||||
{
|
|
||||||
foreach (var part in parts)
|
|
||||||
Plate.Parts.Add(part);
|
|
||||||
}
|
|
||||||
|
|
||||||
stationaryParts.Clear();
|
|
||||||
activeParts.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void FillWithProgress(List<Part> groupParts, Box workArea)
|
public async void FillWithProgress(List<Part> groupParts, Box workArea)
|
||||||
{
|
{
|
||||||
@@ -848,19 +616,41 @@ namespace OpenNest.Controls
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveSelectedParts()
|
public void RemoveSelectedParts() => selection.RemoveSelectedParts();
|
||||||
{
|
|
||||||
foreach (var part in SelectedParts)
|
|
||||||
Plate.Parts.Remove(part.BasePart);
|
|
||||||
|
|
||||||
DeselectAll();
|
|
||||||
Invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void redrawTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
private void redrawTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||||
{
|
{
|
||||||
Invalidate();
|
if (IsDisposed || !IsHandleCreated) return;
|
||||||
|
BeginInvoke(new System.Action(Invalidate));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hoverTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||||
|
{
|
||||||
|
if (IsDisposed || !IsHandleCreated) return;
|
||||||
|
BeginInvoke(new System.Action(HoverCheck));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HoverCheck()
|
||||||
|
{
|
||||||
|
var graphPt = PointControlToGraph(hoverPoint);
|
||||||
|
LayoutPart hitPart = null;
|
||||||
|
|
||||||
|
for (var i = parts.Count - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
if (parts[i].Path.GetBounds().Contains(graphPt) &&
|
||||||
|
parts[i].Path.IsVisible(graphPt))
|
||||||
|
{
|
||||||
|
hitPart = parts[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hoveredPart = hitPart;
|
||||||
|
showTooltip = hitPart != null;
|
||||||
|
|
||||||
|
if (showTooltip)
|
||||||
|
Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void plate_PartAdded(object sender, ItemAddedEventArgs<Part> e)
|
private void plate_PartAdded(object sender, ItemAddedEventArgs<Part> e)
|
||||||
@@ -880,24 +670,9 @@ namespace OpenNest.Controls
|
|||||||
parts.RemoveAll(p => p.BasePart == e.Item);
|
parts.RemoveAll(p => p.BasePart == e.Item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeselectAll()
|
public void DeselectAll() => selection.DeselectAll();
|
||||||
{
|
public void SelectAll() => selection.SelectAll();
|
||||||
SelectedParts.ForEach(p => p.IsSelected = false);
|
public void NotifySelectionChanged() => selection.NotifySelectionChanged();
|
||||||
SelectedParts.Clear();
|
|
||||||
SelectionChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SelectAll()
|
|
||||||
{
|
|
||||||
parts.ForEach(p => p.IsSelected = true);
|
|
||||||
SelectedParts.AddRange(parts);
|
|
||||||
SelectionChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void NotifySelectionChanged()
|
|
||||||
{
|
|
||||||
SelectionChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ZoomToPoint(Vector pt, float zoomFactor, bool redraw = true)
|
public override void ZoomToPoint(Vector pt, float zoomFactor, bool redraw = true)
|
||||||
{
|
{
|
||||||
@@ -930,57 +705,15 @@ namespace OpenNest.Controls
|
|||||||
ZoomToArea(plate.BoundingBox(false), redraw);
|
ZoomToArea(plate.BoundingBox(false), redraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PushSelected(PushDirection direction)
|
public void PushSelected(PushDirection direction) => selection.PushSelected(direction);
|
||||||
{
|
|
||||||
var movingParts = SelectedParts.Select(p => p.BasePart).ToList();
|
|
||||||
Compactor.Push(movingParts, Plate, direction);
|
|
||||||
SelectedParts.ForEach(p => p.IsDirty = true);
|
|
||||||
Invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetDisplayName(Type type)
|
public void RotateSelectedParts(double angle) => selection.RotateSelectedParts(angle);
|
||||||
{
|
|
||||||
var attributes = type.GetCustomAttributes(true);
|
|
||||||
|
|
||||||
foreach (var attr in attributes)
|
|
||||||
{
|
|
||||||
var displayNameAttr = attr as DisplayNameAttribute;
|
|
||||||
|
|
||||||
if (displayNameAttr != null)
|
|
||||||
return displayNameAttr.DisplayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return type.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RotateSelectedParts(double angle)
|
|
||||||
{
|
|
||||||
var parts = SelectedParts.Select(p => p.BasePart).ToList();
|
|
||||||
var bounds = parts.GetBoundingBox();
|
|
||||||
var center = bounds.Center;
|
|
||||||
var anchor = bounds.Location;
|
|
||||||
|
|
||||||
for (var i = 0; i < SelectedParts.Count; ++i)
|
|
||||||
{
|
|
||||||
var part = SelectedParts[i];
|
|
||||||
part.BasePart.Rotate(angle, center);
|
|
||||||
}
|
|
||||||
|
|
||||||
var diff = anchor - parts.GetBoundingBox().Location;
|
|
||||||
|
|
||||||
for (var i = 0; i < SelectedParts.Count; ++i)
|
|
||||||
SelectedParts[i].Offset(diff);
|
|
||||||
|
|
||||||
if (Plate.CutOffs.Count > 0)
|
|
||||||
Plate.RegenerateCutOffs(cutOffSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateMatrix()
|
protected override void UpdateMatrix()
|
||||||
{
|
{
|
||||||
base.UpdateMatrix();
|
base.UpdateMatrix();
|
||||||
parts.ForEach(p => p.Update(this));
|
parts.ForEach(p => p.Update(this));
|
||||||
stationaryParts.ForEach(p => p.Update(this));
|
previewManager.Update();
|
||||||
activeParts.ForEach(p => p.Update(this));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace OpenNest.Controls
|
||||||
|
{
|
||||||
|
internal class PreviewManager
|
||||||
|
{
|
||||||
|
private readonly PlateView view;
|
||||||
|
private readonly List<LayoutPart> stationaryParts = new List<LayoutPart>();
|
||||||
|
private readonly List<LayoutPart> activeParts = new List<LayoutPart>();
|
||||||
|
|
||||||
|
public PreviewManager(PlateView view)
|
||||||
|
{
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<LayoutPart> PreviewParts =>
|
||||||
|
activeParts.Count > 0 ? activeParts : stationaryParts;
|
||||||
|
|
||||||
|
public Brush PreviewBrush =>
|
||||||
|
activeParts.Count > 0 ? view.ColorScheme.ActivePreviewPartBrush : view.ColorScheme.PreviewPartBrush;
|
||||||
|
|
||||||
|
public Pen PreviewPen =>
|
||||||
|
activeParts.Count > 0 ? view.ColorScheme.ActivePreviewPartPen : view.ColorScheme.PreviewPartPen;
|
||||||
|
|
||||||
|
public void SetStationaryParts(List<Part> parts)
|
||||||
|
{
|
||||||
|
stationaryParts.Clear();
|
||||||
|
activeParts.Clear();
|
||||||
|
|
||||||
|
if (parts != null)
|
||||||
|
{
|
||||||
|
foreach (var part in parts)
|
||||||
|
stationaryParts.Add(LayoutPart.Create(part, view));
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetActiveParts(List<Part> parts)
|
||||||
|
{
|
||||||
|
activeParts.Clear();
|
||||||
|
|
||||||
|
if (parts != null)
|
||||||
|
{
|
||||||
|
foreach (var part in parts)
|
||||||
|
activeParts.Add(LayoutPart.Create(part, view));
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearPreviewParts()
|
||||||
|
{
|
||||||
|
stationaryParts.Clear();
|
||||||
|
activeParts.Clear();
|
||||||
|
view.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AcceptPreviewParts(List<Part> parts)
|
||||||
|
{
|
||||||
|
if (parts != null)
|
||||||
|
{
|
||||||
|
foreach (var part in parts)
|
||||||
|
view.Plate.Parts.Add(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
stationaryParts.Clear();
|
||||||
|
activeParts.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
stationaryParts.ForEach(p => p.Update(view));
|
||||||
|
activeParts.ForEach(p => p.Update(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
stationaryParts.Clear();
|
||||||
|
activeParts.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Controls
|
||||||
|
{
|
||||||
|
internal class SelectionManager
|
||||||
|
{
|
||||||
|
private readonly PlateView view;
|
||||||
|
private readonly List<LayoutPart> selectedParts = new List<LayoutPart>();
|
||||||
|
private readonly List<CutOff> selectedCutOffs = new List<CutOff>();
|
||||||
|
|
||||||
|
public SelectionManager(PlateView view)
|
||||||
|
{
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<LayoutPart> SelectedParts => selectedParts;
|
||||||
|
public List<CutOff> SelectedCutOffs => selectedCutOffs;
|
||||||
|
|
||||||
|
public event EventHandler SelectionChanged;
|
||||||
|
|
||||||
|
public void DeselectAll()
|
||||||
|
{
|
||||||
|
selectedParts.ForEach(p => p.IsSelected = false);
|
||||||
|
selectedParts.Clear();
|
||||||
|
selectedCutOffs.Clear();
|
||||||
|
SelectionChanged?.Invoke(view, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeselectParts()
|
||||||
|
{
|
||||||
|
selectedParts.ForEach(p => p.IsSelected = false);
|
||||||
|
selectedParts.Clear();
|
||||||
|
SelectionChanged?.Invoke(view, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeselectCutOffs()
|
||||||
|
{
|
||||||
|
selectedCutOffs.Clear();
|
||||||
|
view.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SelectAll()
|
||||||
|
{
|
||||||
|
var parts = view.LayoutParts;
|
||||||
|
parts.ForEach(p => p.IsSelected = true);
|
||||||
|
selectedParts.AddRange(parts);
|
||||||
|
SelectionChanged?.Invoke(view, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifySelectionChanged()
|
||||||
|
{
|
||||||
|
SelectionChanged?.Invoke(view, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteSelected()
|
||||||
|
{
|
||||||
|
if (selectedCutOffs.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var cutOff in selectedCutOffs)
|
||||||
|
view.Plate.CutOffs.Remove(cutOff);
|
||||||
|
|
||||||
|
selectedCutOffs.Clear();
|
||||||
|
view.Plate.RegenerateCutOffs(view.CutOffSettings);
|
||||||
|
view.Invalidate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RemoveSelectedParts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSelectedParts()
|
||||||
|
{
|
||||||
|
foreach (var part in selectedParts)
|
||||||
|
view.Plate.Parts.Remove(part.BasePart);
|
||||||
|
|
||||||
|
DeselectAll();
|
||||||
|
view.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AlignSelected(AlignType alignType)
|
||||||
|
{
|
||||||
|
if (selectedParts.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AlignSelected(alignType, selectedParts[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AlignSelected(AlignType alignType, LayoutPart fixedPart)
|
||||||
|
{
|
||||||
|
switch (alignType)
|
||||||
|
{
|
||||||
|
case AlignType.Bottom:
|
||||||
|
Align.Bottom(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
|
||||||
|
break;
|
||||||
|
case AlignType.Horizontally:
|
||||||
|
Align.Horizontally(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
|
||||||
|
break;
|
||||||
|
case AlignType.Left:
|
||||||
|
Align.Left(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
|
||||||
|
break;
|
||||||
|
case AlignType.Right:
|
||||||
|
Align.Right(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
|
||||||
|
break;
|
||||||
|
case AlignType.Top:
|
||||||
|
Align.Top(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
|
||||||
|
break;
|
||||||
|
case AlignType.Vertically:
|
||||||
|
Align.Vertically(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
|
||||||
|
break;
|
||||||
|
case AlignType.EvenlySpaceHorizontally:
|
||||||
|
Align.EvenlyDistributeHorizontally(selectedParts.Select(p => p.BasePart).ToList());
|
||||||
|
break;
|
||||||
|
case AlignType.EvenlySpaceVertically:
|
||||||
|
Align.EvenlyDistributeVertically(selectedParts.Select(p => p.BasePart).ToList());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedParts.ForEach(p => p.IsDirty = true);
|
||||||
|
view.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RotateSelectedParts(double angle)
|
||||||
|
{
|
||||||
|
var parts = selectedParts.Select(p => p.BasePart).ToList();
|
||||||
|
var bounds = parts.GetBoundingBox();
|
||||||
|
var center = bounds.Center;
|
||||||
|
var anchor = bounds.Location;
|
||||||
|
|
||||||
|
for (var i = 0; i < selectedParts.Count; ++i)
|
||||||
|
selectedParts[i].BasePart.Rotate(angle, center);
|
||||||
|
|
||||||
|
var diff = anchor - parts.GetBoundingBox().Location;
|
||||||
|
|
||||||
|
for (var i = 0; i < selectedParts.Count; ++i)
|
||||||
|
selectedParts[i].Offset(diff);
|
||||||
|
|
||||||
|
if (view.Plate.CutOffs.Count > 0)
|
||||||
|
view.Plate.RegenerateCutOffs(view.CutOffSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PushSelected(PushDirection direction)
|
||||||
|
{
|
||||||
|
var movingParts = selectedParts.Select(p => p.BasePart).ToList();
|
||||||
|
Compactor.Push(movingParts, view.Plate, direction);
|
||||||
|
selectedParts.ForEach(p => p.IsDirty = true);
|
||||||
|
view.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LayoutPart GetPartAtControlPoint(Point pt)
|
||||||
|
{
|
||||||
|
var pt2 = view.PointControlToGraph(pt);
|
||||||
|
return GetPartAtGraphPoint(pt2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LayoutPart GetPartAtGraphPoint(PointF pt)
|
||||||
|
{
|
||||||
|
var parts = view.LayoutParts;
|
||||||
|
for (var i = parts.Count - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
if (parts[i].Path.IsVisible(pt))
|
||||||
|
return parts[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LayoutPart GetPartAtPoint(Vector pt)
|
||||||
|
{
|
||||||
|
var pt2 = view.PointWorldToGraph(pt);
|
||||||
|
return GetPartAtGraphPoint(pt2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<LayoutPart> GetPartsFromWindow(RectangleF rect, SelectionType selectionType)
|
||||||
|
{
|
||||||
|
var list = new List<LayoutPart>();
|
||||||
|
var parts = view.LayoutParts;
|
||||||
|
|
||||||
|
if (selectionType == SelectionType.Intersect)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < parts.Count; ++i)
|
||||||
|
{
|
||||||
|
var part = parts[i];
|
||||||
|
var region = new Region(part.Path);
|
||||||
|
if (region.IsVisible(rect))
|
||||||
|
list.Add(part);
|
||||||
|
region.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = 0; i < parts.Count; ++i)
|
||||||
|
{
|
||||||
|
var part = parts[i];
|
||||||
|
var bounds = part.Path.GetBounds();
|
||||||
|
if (rect.Contains(bounds))
|
||||||
|
list.Add(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
selectedParts.Clear();
|
||||||
|
selectedCutOffs.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest.Controls
|
||||||
|
{
|
||||||
|
public class ShapePreviewControl : PlateView
|
||||||
|
{
|
||||||
|
private string[] infoLines;
|
||||||
|
|
||||||
|
public ShapePreviewControl()
|
||||||
|
{
|
||||||
|
DrawOrigin = false;
|
||||||
|
DrawBounds = false;
|
||||||
|
AllowPan = false;
|
||||||
|
AllowSelect = false;
|
||||||
|
AllowZoom = false;
|
||||||
|
AllowDrop = false;
|
||||||
|
BackColor = Color.White;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetInfo(params string[] lines)
|
||||||
|
{
|
||||||
|
infoLines = lines;
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowDrawing(Drawing drawing)
|
||||||
|
{
|
||||||
|
Plate.Parts.Clear();
|
||||||
|
Plate.Size = new Geometry.Size(0, 0);
|
||||||
|
|
||||||
|
if (drawing?.Program != null)
|
||||||
|
{
|
||||||
|
AddPartFromDrawing(drawing, Geometry.Vector.Zero);
|
||||||
|
ZoomToFit();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnResize(System.EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnResize(e);
|
||||||
|
|
||||||
|
if (Plate.Parts.Count > 0)
|
||||||
|
ZoomToFit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
|
{
|
||||||
|
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||||
|
|
||||||
|
e.Graphics.TranslateTransform(origin.X, origin.Y);
|
||||||
|
Renderer.DrawPlate(e.Graphics);
|
||||||
|
Renderer.DrawParts(e.Graphics);
|
||||||
|
e.Graphics.ResetTransform();
|
||||||
|
|
||||||
|
PaintInfo(e.Graphics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PaintInfo(Graphics g)
|
||||||
|
{
|
||||||
|
if (infoLines == null) return;
|
||||||
|
|
||||||
|
var lineHeight = Font.GetHeight(g) + 1;
|
||||||
|
var y = 4f;
|
||||||
|
|
||||||
|
foreach (var line in infoLines)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(line)) continue;
|
||||||
|
g.DrawString(line, Font, Brushes.Black, 4, y);
|
||||||
|
y += lineHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
using OpenNest.Bending;
|
|
||||||
using OpenNest.CNC;
|
|
||||||
using OpenNest.Converters;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
using OpenNest.IO.Bending;
|
|
||||||
using OpenNest.IO.Bom;
|
using OpenNest.IO.Bom;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -470,33 +466,9 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = Dxf.Import(part.DxfPath);
|
var drawing = CadImporter.ImportDrawing(part.DxfPath,
|
||||||
|
new CadImportOptions { Quantity = part.Qty ?? 1 });
|
||||||
var bends = new List<Bend>();
|
|
||||||
if (result.Document != null)
|
|
||||||
bends = BendDetectorRegistry.AutoDetect(result.Document);
|
|
||||||
Bend.UpdateEtchEntities(result.Entities, bends);
|
|
||||||
|
|
||||||
var drawingName = Path.GetFileNameWithoutExtension(part.DxfPath);
|
|
||||||
var drawing = new Drawing(drawingName);
|
|
||||||
drawing.Color = Drawing.GetNextColor();
|
|
||||||
drawing.Source.Path = part.DxfPath;
|
|
||||||
drawing.Quantity.Required = part.Qty ?? 1;
|
|
||||||
drawing.Material = new Material(material);
|
drawing.Material = new Material(material);
|
||||||
if (bends.Count > 0)
|
|
||||||
drawing.Bends.AddRange(bends);
|
|
||||||
|
|
||||||
var normalized = ShapeProfile.NormalizeEntities(result.Entities);
|
|
||||||
var pgm = ConvertGeometry.ToProgram(normalized);
|
|
||||||
|
|
||||||
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
|
|
||||||
{
|
|
||||||
var rapid = (RapidMove)pgm[0];
|
|
||||||
drawing.Source.Offset = rapid.EndPoint;
|
|
||||||
pgm.Offset(-rapid.EndPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawing.Program = pgm;
|
|
||||||
nest.Drawings.Add(drawing);
|
nest.Drawings.Add(drawing);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using OpenNest.Converters;
|
|||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
using OpenNest.IO.Bending;
|
using OpenNest.IO.Bending;
|
||||||
using OpenNest.Properties;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
@@ -74,36 +73,24 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = Dxf.Import(file);
|
var options = new CadImportOptions
|
||||||
|
{
|
||||||
|
BendDetectorName = detectorIndex == 0 ? null : detectorName,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = CadImporter.Import(file, options);
|
||||||
if (result.Entities.Count == 0)
|
if (result.Entities.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Compute bounds
|
|
||||||
var bounds = result.Entities.GetBoundingBox();
|
|
||||||
|
|
||||||
// Detect bends (detectorIndex/Name captured on UI thread)
|
|
||||||
var bends = new List<Bend>();
|
|
||||||
if (result.Document != null)
|
|
||||||
{
|
|
||||||
bends = detectorIndex == 0
|
|
||||||
? BendDetectorRegistry.AutoDetect(result.Document)
|
|
||||||
: BendDetectorRegistry.GetByName(detectorName)
|
|
||||||
?.DetectBends(result.Document)
|
|
||||||
?? new List<Bend>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Bend.UpdateEtchEntities(result.Entities, bends);
|
|
||||||
|
|
||||||
var item = new FileListItem
|
var item = new FileListItem
|
||||||
{
|
{
|
||||||
Name = Path.GetFileNameWithoutExtension(file),
|
Name = result.Name,
|
||||||
Entities = result.Entities,
|
Entities = result.Entities,
|
||||||
Path = file,
|
Path = result.SourcePath,
|
||||||
Quantity = 1,
|
Quantity = 1,
|
||||||
Customer = string.Empty,
|
Customer = string.Empty,
|
||||||
Bends = bends,
|
Bends = result.Bends,
|
||||||
Bounds = bounds,
|
Bounds = result.Bounds,
|
||||||
EntityCount = result.Entities.Count
|
EntityCount = result.Entities.Count
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -169,6 +156,7 @@ namespace OpenNest.Forms
|
|||||||
if (item.Entities.Any(e => e.Layer != null))
|
if (item.Entities.Any(e => e.Layer != null))
|
||||||
item.Entities.ForEach(e => e.Layer.IsVisible = true);
|
item.Entities.ForEach(e => e.Layer.IsVisible = true);
|
||||||
ReHidePromotedEntities(item.Bends);
|
ReHidePromotedEntities(item.Bends);
|
||||||
|
ReHideSuppressedEntities(item);
|
||||||
|
|
||||||
filterPanel.LoadItem(item.Entities, item.Bends);
|
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||||
|
|
||||||
@@ -245,6 +233,7 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
filterPanel.ApplyFilters(item.Entities);
|
filterPanel.ApplyFilters(item.Entities);
|
||||||
ReHidePromotedEntities(item.Bends);
|
ReHidePromotedEntities(item.Bends);
|
||||||
|
SyncSuppressedState(item);
|
||||||
entityView1.Invalidate();
|
entityView1.Invalidate();
|
||||||
staleProgram = true;
|
staleProgram = true;
|
||||||
}
|
}
|
||||||
@@ -366,7 +355,6 @@ namespace OpenNest.Forms
|
|||||||
: Path.GetTempPath();
|
: Path.GetTempPath();
|
||||||
|
|
||||||
var index = fileList.SelectedIndex;
|
var index = fileList.SelectedIndex;
|
||||||
var newItems = new List<string>();
|
|
||||||
|
|
||||||
var splitWriter = new SplitDxfWriter();
|
var splitWriter = new SplitDxfWriter();
|
||||||
var splitItems = new List<FileListItem>();
|
var splitItems = new List<FileListItem>();
|
||||||
@@ -379,7 +367,6 @@ namespace OpenNest.Forms
|
|||||||
var splitPath = GetUniquePath(Path.Combine(writableDir, splitName));
|
var splitPath = GetUniquePath(Path.Combine(writableDir, splitName));
|
||||||
|
|
||||||
splitWriter.Write(splitPath, splitDrawing);
|
splitWriter.Write(splitPath, splitDrawing);
|
||||||
newItems.Add(splitPath);
|
|
||||||
|
|
||||||
// Re-import geometry but keep bends from the split drawing
|
// Re-import geometry but keep bends from the split drawing
|
||||||
var result = Dxf.Import(splitPath);
|
var result = Dxf.Import(splitPath);
|
||||||
@@ -604,6 +591,61 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Load Existing Drawings
|
||||||
|
|
||||||
|
public void LoadDrawings(IEnumerable<Drawing> drawings)
|
||||||
|
{
|
||||||
|
foreach (var drawing in drawings)
|
||||||
|
{
|
||||||
|
List<Entity> entities;
|
||||||
|
|
||||||
|
if (drawing.SourceEntities != null)
|
||||||
|
{
|
||||||
|
// Use stored entities with stable GUIDs; apply suppression state
|
||||||
|
entities = new List<Entity>(drawing.SourceEntities);
|
||||||
|
|
||||||
|
foreach (var entity in entities)
|
||||||
|
entity.IsVisible = !drawing.SuppressedEntityIds.Contains(entity.Id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: derive entities from Program (older drawings without source entities)
|
||||||
|
entities = ConvertProgram.ToGeometry(drawing.Program);
|
||||||
|
|
||||||
|
// Re-apply source offset so entities appear in their natural position
|
||||||
|
if (drawing.Source?.Offset != null && drawing.Source.Offset != Vector.Zero)
|
||||||
|
{
|
||||||
|
foreach (var entity in entities)
|
||||||
|
entity.Offset(drawing.Source.Offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove rapid traversals — they aren't part of the cut geometry
|
||||||
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = entities.GetBoundingBox();
|
||||||
|
|
||||||
|
var item = new FileListItem
|
||||||
|
{
|
||||||
|
Name = drawing.Name,
|
||||||
|
Entities = entities,
|
||||||
|
Path = drawing.Source?.Path,
|
||||||
|
Quantity = drawing.Quantity.Required,
|
||||||
|
Customer = drawing.Customer ?? string.Empty,
|
||||||
|
Bends = drawing.Bends?.ToList() ?? new List<Bend>(),
|
||||||
|
SuppressedEntityIds = drawing.SuppressedEntityIds.Count > 0
|
||||||
|
? new HashSet<Guid>(drawing.SuppressedEntityIds)
|
||||||
|
: null,
|
||||||
|
Bounds = bounds,
|
||||||
|
EntityCount = entities.Count
|
||||||
|
};
|
||||||
|
|
||||||
|
fileList.AddItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Output
|
#region Output
|
||||||
|
|
||||||
public List<Drawing> GetDrawings()
|
public List<Drawing> GetDrawings()
|
||||||
@@ -612,38 +654,36 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
foreach (var item in fileList.Items)
|
foreach (var item in fileList.Items)
|
||||||
{
|
{
|
||||||
var entities = item.Entities.Where(e => e.Layer.IsVisible && e.IsVisible).ToList();
|
var visible = item.Entities
|
||||||
|
.Where(e => e.Layer.IsVisible && e.IsVisible)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
if (entities.Count == 0)
|
if (visible.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var drawing = new Drawing(item.Name);
|
// Rebuild a CadImportResult from the FileListItem's current state so
|
||||||
drawing.Color = Drawing.GetNextColor();
|
// BuildDrawing sees the user's edits (filters, suppressions, new bends).
|
||||||
drawing.Customer = item.Customer;
|
var result = new CadImportResult
|
||||||
drawing.Source.Path = item.Path;
|
|
||||||
drawing.Quantity.Required = item.Quantity;
|
|
||||||
|
|
||||||
// Copy bends
|
|
||||||
if (item.Bends != null)
|
|
||||||
drawing.Bends.AddRange(item.Bends);
|
|
||||||
|
|
||||||
var normalized = ShapeProfile.NormalizeEntities(entities);
|
|
||||||
var pgm = ConvertGeometry.ToProgram(normalized);
|
|
||||||
var firstCode = pgm[0];
|
|
||||||
|
|
||||||
if (firstCode.Type == CodeType.RapidMove)
|
|
||||||
{
|
{
|
||||||
var rapid = (RapidMove)firstCode;
|
Entities = item.Entities,
|
||||||
drawing.Source.Offset = rapid.EndPoint;
|
Bends = item.Bends ?? new List<Bend>(),
|
||||||
pgm.Offset(-rapid.EndPoint);
|
Bounds = item.Bounds,
|
||||||
// Keep the rapid (now at origin) — it marks the contour
|
SourcePath = item.Path,
|
||||||
// start and is needed by the post for correct pierce placement.
|
Name = item.Name,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
var editedProgram = (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null)
|
||||||
|
? programEditor.Program
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var drawing = CadImporter.BuildDrawing(
|
||||||
|
result,
|
||||||
|
visible,
|
||||||
|
result.Bends,
|
||||||
|
item.Quantity,
|
||||||
|
item.Customer,
|
||||||
|
editedProgram);
|
||||||
|
|
||||||
if (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null)
|
|
||||||
drawing.Program = programEditor.Program;
|
|
||||||
else
|
|
||||||
drawing.Program = pgm;
|
|
||||||
drawings.Add(drawing);
|
drawings.Add(drawing);
|
||||||
|
|
||||||
Thread.Sleep(20);
|
Thread.Sleep(20);
|
||||||
@@ -666,8 +706,46 @@ namespace OpenNest.Forms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ReHideSuppressedEntities(FileListItem item)
|
||||||
|
{
|
||||||
|
if (item.SuppressedEntityIds == null || item.SuppressedEntityIds.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
private static Color GetNextColor() => Drawing.GetNextColor();
|
foreach (var entity in item.Entities)
|
||||||
|
{
|
||||||
|
if (item.SuppressedEntityIds.Contains(entity.Id))
|
||||||
|
entity.IsVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all entities on a layer are suppressed, uncheck the layer too
|
||||||
|
var layerGroups = item.Entities
|
||||||
|
.Where(e => e.Layer != null)
|
||||||
|
.GroupBy(e => e.Layer);
|
||||||
|
|
||||||
|
foreach (var group in layerGroups)
|
||||||
|
{
|
||||||
|
if (group.All(e => !e.IsVisible))
|
||||||
|
group.Key.IsVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SyncSuppressedState(FileListItem item)
|
||||||
|
{
|
||||||
|
var bendSources = new HashSet<Entity>(
|
||||||
|
(item.Bends ?? new List<Bend>())
|
||||||
|
.Where(b => b.SourceEntity != null)
|
||||||
|
.Select(b => b.SourceEntity));
|
||||||
|
|
||||||
|
var suppressed = item.Entities
|
||||||
|
.Where(e => !(e.Layer.IsVisible && e.IsVisible))
|
||||||
|
.Where(e => !bendSources.Contains(e))
|
||||||
|
.Select(e => e.Id);
|
||||||
|
|
||||||
|
item.SuppressedEntityIds = new HashSet<Guid>(suppressed);
|
||||||
|
|
||||||
|
if (item.SuppressedEntityIds.Count == 0)
|
||||||
|
item.SuppressedEntityIds = null;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsDirectoryWritable(string path)
|
private static bool IsDirectoryWritable(string path)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Controls;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest.Forms
|
||||||
|
{
|
||||||
|
public class CuttingParametersDialog : Form
|
||||||
|
{
|
||||||
|
private readonly CuttingPanel cuttingPanel;
|
||||||
|
|
||||||
|
public CuttingParametersDialog()
|
||||||
|
{
|
||||||
|
Text = "Cutting Parameters";
|
||||||
|
Size = new Size(400, 560);
|
||||||
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||||
|
MaximizeBox = false;
|
||||||
|
MinimizeBox = false;
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
|
||||||
|
cuttingPanel = new CuttingPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill
|
||||||
|
};
|
||||||
|
|
||||||
|
var buttonPanel = new Panel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Bottom,
|
||||||
|
Height = 40
|
||||||
|
};
|
||||||
|
|
||||||
|
var btnOk = new Button
|
||||||
|
{
|
||||||
|
Text = "OK",
|
||||||
|
DialogResult = DialogResult.OK,
|
||||||
|
Size = new Size(80, 28),
|
||||||
|
Location = new Point(220, 6)
|
||||||
|
};
|
||||||
|
|
||||||
|
var btnCancel = new Button
|
||||||
|
{
|
||||||
|
Text = "Cancel",
|
||||||
|
DialogResult = DialogResult.Cancel,
|
||||||
|
Size = new Size(80, 28),
|
||||||
|
Location = new Point(305, 6)
|
||||||
|
};
|
||||||
|
|
||||||
|
buttonPanel.Controls.Add(btnOk);
|
||||||
|
buttonPanel.Controls.Add(btnCancel);
|
||||||
|
|
||||||
|
Controls.Add(cuttingPanel);
|
||||||
|
Controls.Add(buttonPanel);
|
||||||
|
|
||||||
|
AcceptButton = btnOk;
|
||||||
|
CancelButton = btnCancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadParameters(CuttingParameters parameters)
|
||||||
|
{
|
||||||
|
cuttingPanel.LoadFromParameters(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CuttingParameters GetParameters()
|
||||||
|
{
|
||||||
|
return cuttingPanel.BuildParameters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ namespace OpenNest.Forms
|
|||||||
TabsEnabled = p.TabsEnabled,
|
TabsEnabled = p.TabsEnabled,
|
||||||
TabWidth = p.TabConfig?.Size ?? 0.25,
|
TabWidth = p.TabConfig?.Size ?? 0.25,
|
||||||
PierceClearance = p.PierceClearance,
|
PierceClearance = p.PierceClearance,
|
||||||
|
RoundLeadInAngles = p.RoundLeadInAngles,
|
||||||
|
LeadInAngleIncrement = p.LeadInAngleIncrement,
|
||||||
AutoTabMinSize = p.AutoTabMinSize,
|
AutoTabMinSize = p.AutoTabMinSize,
|
||||||
AutoTabMaxSize = p.AutoTabMaxSize
|
AutoTabMaxSize = p.AutoTabMaxSize
|
||||||
};
|
};
|
||||||
@@ -47,6 +49,8 @@ namespace OpenNest.Forms
|
|||||||
TabsEnabled = dto.TabsEnabled,
|
TabsEnabled = dto.TabsEnabled,
|
||||||
TabConfig = new NormalTab { Size = dto.TabWidth },
|
TabConfig = new NormalTab { Size = dto.TabWidth },
|
||||||
PierceClearance = dto.PierceClearance,
|
PierceClearance = dto.PierceClearance,
|
||||||
|
RoundLeadInAngles = dto.RoundLeadInAngles,
|
||||||
|
LeadInAngleIncrement = dto.LeadInAngleIncrement > 0 ? dto.LeadInAngleIncrement : 5.0,
|
||||||
AutoTabMinSize = dto.AutoTabMinSize,
|
AutoTabMinSize = dto.AutoTabMinSize,
|
||||||
AutoTabMaxSize = dto.AutoTabMaxSize
|
AutoTabMaxSize = dto.AutoTabMaxSize
|
||||||
};
|
};
|
||||||
@@ -85,7 +89,6 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
LineLeadOut line => new LeadOutDto { Type = "Line", Length = line.Length, ApproachAngle = line.ApproachAngle },
|
LineLeadOut line => new LeadOutDto { Type = "Line", Length = line.Length, ApproachAngle = line.ApproachAngle },
|
||||||
ArcLeadOut arc => new LeadOutDto { Type = "Arc", Radius = arc.Radius },
|
ArcLeadOut arc => new LeadOutDto { Type = "Arc", Radius = arc.Radius },
|
||||||
MicrotabLeadOut mt => new LeadOutDto { Type = "Microtab", GapSize = mt.GapSize },
|
|
||||||
_ => new LeadOutDto { Type = "None" }
|
_ => new LeadOutDto { Type = "None" }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -97,7 +100,6 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
"Line" => new LineLeadOut { Length = dto.Length, ApproachAngle = dto.ApproachAngle },
|
"Line" => new LineLeadOut { Length = dto.Length, ApproachAngle = dto.ApproachAngle },
|
||||||
"Arc" => new ArcLeadOut { Radius = dto.Radius },
|
"Arc" => new ArcLeadOut { Radius = dto.Radius },
|
||||||
"Microtab" => new MicrotabLeadOut { GapSize = dto.GapSize },
|
|
||||||
_ => new NoLeadOut()
|
_ => new NoLeadOut()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -113,6 +115,8 @@ namespace OpenNest.Forms
|
|||||||
public bool TabsEnabled { get; set; }
|
public bool TabsEnabled { get; set; }
|
||||||
public double TabWidth { get; set; }
|
public double TabWidth { get; set; }
|
||||||
public double PierceClearance { get; set; }
|
public double PierceClearance { get; set; }
|
||||||
|
public bool RoundLeadInAngles { get; set; }
|
||||||
|
public double LeadInAngleIncrement { get; set; }
|
||||||
public double AutoTabMinSize { get; set; }
|
public double AutoTabMinSize { get; set; }
|
||||||
public double AutoTabMaxSize { get; set; }
|
public double AutoTabMaxSize { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+22
-3
@@ -47,6 +47,8 @@
|
|||||||
drawingListBox1 = new OpenNest.Controls.DrawingListBox();
|
drawingListBox1 = new OpenNest.Controls.DrawingListBox();
|
||||||
toolStrip2 = new System.Windows.Forms.ToolStrip();
|
toolStrip2 = new System.Windows.Forms.ToolStrip();
|
||||||
toolStripButton2 = new System.Windows.Forms.ToolStripButton();
|
toolStripButton2 = new System.Windows.Forms.ToolStripButton();
|
||||||
|
toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
|
editDrawingsButton = new System.Windows.Forms.ToolStripButton();
|
||||||
toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
|
toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
toolStripButton3 = new System.Windows.Forms.ToolStripButton();
|
toolStripButton3 = new System.Windows.Forms.ToolStripButton();
|
||||||
toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
@@ -79,8 +81,8 @@
|
|||||||
//
|
//
|
||||||
// tabControl1
|
// tabControl1
|
||||||
//
|
//
|
||||||
tabControl1.Controls.Add(tabPage1);
|
|
||||||
tabControl1.Controls.Add(tabPage2);
|
tabControl1.Controls.Add(tabPage2);
|
||||||
|
tabControl1.Controls.Add(tabPage1);
|
||||||
tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
|
tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
tabControl1.ItemSize = new System.Drawing.Size(100, 22);
|
tabControl1.ItemSize = new System.Drawing.Size(100, 22);
|
||||||
tabControl1.Location = new System.Drawing.Point(0, 0);
|
tabControl1.Location = new System.Drawing.Point(0, 0);
|
||||||
@@ -175,7 +177,7 @@
|
|||||||
// toolStripLabel2
|
// toolStripLabel2
|
||||||
//
|
//
|
||||||
toolStripLabel2.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
|
toolStripLabel2.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
|
||||||
toolStripLabel2.Image = Properties.Resources.delete;
|
toolStripLabel2.Image = (System.Drawing.Image)resources.GetObject("toolStripLabel2.Image");
|
||||||
toolStripLabel2.Name = "toolStripLabel2";
|
toolStripLabel2.Name = "toolStripLabel2";
|
||||||
toolStripLabel2.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
|
toolStripLabel2.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
|
||||||
toolStripLabel2.Size = new System.Drawing.Size(34, 24);
|
toolStripLabel2.Size = new System.Drawing.Size(34, 24);
|
||||||
@@ -217,7 +219,7 @@
|
|||||||
//
|
//
|
||||||
toolStrip2.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
|
toolStrip2.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
|
||||||
toolStrip2.ImageScalingSize = new System.Drawing.Size(20, 20);
|
toolStrip2.ImageScalingSize = new System.Drawing.Size(20, 20);
|
||||||
toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripButton2, toolStripSeparator1, toolStripButton3, toolStripSeparator2, hideNestedButton });
|
toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripButton2, toolStripSeparator4, editDrawingsButton, toolStripSeparator1, toolStripButton3, toolStripSeparator2, hideNestedButton });
|
||||||
toolStrip2.Location = new System.Drawing.Point(4, 3);
|
toolStrip2.Location = new System.Drawing.Point(4, 3);
|
||||||
toolStrip2.Name = "toolStrip2";
|
toolStrip2.Name = "toolStrip2";
|
||||||
toolStrip2.Size = new System.Drawing.Size(265, 27);
|
toolStrip2.Size = new System.Drawing.Size(265, 27);
|
||||||
@@ -236,6 +238,21 @@
|
|||||||
toolStripButton2.Text = "Import Drawings";
|
toolStripButton2.Text = "Import Drawings";
|
||||||
toolStripButton2.Click += ImportDrawings_Click;
|
toolStripButton2.Click += ImportDrawings_Click;
|
||||||
//
|
//
|
||||||
|
// toolStripSeparator4
|
||||||
|
//
|
||||||
|
toolStripSeparator4.Name = "toolStripSeparator4";
|
||||||
|
toolStripSeparator4.Size = new System.Drawing.Size(6, 27);
|
||||||
|
//
|
||||||
|
// editDrawingsButton
|
||||||
|
//
|
||||||
|
editDrawingsButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
|
||||||
|
editDrawingsButton.Image = (System.Drawing.Image)resources.GetObject("editDrawingsButton.Image");
|
||||||
|
editDrawingsButton.Name = "editDrawingsButton";
|
||||||
|
editDrawingsButton.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
|
||||||
|
editDrawingsButton.Size = new System.Drawing.Size(34, 24);
|
||||||
|
editDrawingsButton.Text = "Edit Drawings in Converter";
|
||||||
|
editDrawingsButton.Click += EditDrawingsInConverter_Click;
|
||||||
|
//
|
||||||
// toolStripSeparator1
|
// toolStripSeparator1
|
||||||
//
|
//
|
||||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||||
@@ -312,6 +329,8 @@
|
|||||||
private System.Windows.Forms.ColumnHeader utilColumn;
|
private System.Windows.Forms.ColumnHeader utilColumn;
|
||||||
private System.Windows.Forms.ToolStrip toolStrip2;
|
private System.Windows.Forms.ToolStrip toolStrip2;
|
||||||
private System.Windows.Forms.ToolStripButton toolStripButton2;
|
private System.Windows.Forms.ToolStripButton toolStripButton2;
|
||||||
|
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
|
||||||
|
private System.Windows.Forms.ToolStripButton editDrawingsButton;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
||||||
private System.Windows.Forms.ToolStripButton toolStripButton3;
|
private System.Windows.Forms.ToolStripButton toolStripButton3;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
|
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
|
||||||
|
|||||||
+101
-37
@@ -52,6 +52,7 @@ namespace OpenNest.Forms
|
|||||||
private EditNestForm()
|
private EditNestForm()
|
||||||
{
|
{
|
||||||
PlateView = new PlateView();
|
PlateView = new PlateView();
|
||||||
|
PlateView.MouseEnter += PlateView_MouseEnter;
|
||||||
PlateView.Enter += PlateView_Enter;
|
PlateView.Enter += PlateView_Enter;
|
||||||
PlateView.PartAdded += PlateView_PartAdded;
|
PlateView.PartAdded += PlateView_PartAdded;
|
||||||
PlateView.PartRemoved += PlateView_PartRemoved;
|
PlateView.PartRemoved += PlateView_PartRemoved;
|
||||||
@@ -718,19 +719,17 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
var plate = PlateView.Plate;
|
var plate = PlateView.Plate;
|
||||||
|
|
||||||
if (plate.CuttingParameters == null)
|
var parameters = LoadOrDefaultParameters(plate.CuttingParameters);
|
||||||
{
|
|
||||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
using var dlg = new CuttingParametersDialog();
|
||||||
if (!string.IsNullOrEmpty(json))
|
dlg.LoadParameters(parameters);
|
||||||
{
|
|
||||||
try { plate.CuttingParameters = CuttingParametersSerializer.Deserialize(json); }
|
if (dlg.ShowDialog() != DialogResult.OK)
|
||||||
catch { plate.CuttingParameters = new CuttingParameters(); }
|
return;
|
||||||
}
|
|
||||||
else
|
parameters = dlg.GetParameters();
|
||||||
{
|
plate.CuttingParameters = parameters;
|
||||||
plate.CuttingParameters = new CuttingParameters();
|
SaveCuttingParameters(parameters);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var assigner = new LeadInAssigner
|
var assigner = new LeadInAssigner
|
||||||
{
|
{
|
||||||
@@ -781,17 +780,16 @@ namespace OpenNest.Forms
|
|||||||
if (Nest == null)
|
if (Nest == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
CuttingParameters parameters;
|
var parameters = LoadOrDefaultParameters(PlateView?.Plate?.CuttingParameters);
|
||||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
|
||||||
if (!string.IsNullOrEmpty(json))
|
using var dlg = new CuttingParametersDialog();
|
||||||
{
|
dlg.LoadParameters(parameters);
|
||||||
try { parameters = CuttingParametersSerializer.Deserialize(json); }
|
|
||||||
catch { parameters = new CuttingParameters(); }
|
if (dlg.ShowDialog() != DialogResult.OK)
|
||||||
}
|
return;
|
||||||
else
|
|
||||||
{
|
parameters = dlg.GetParameters();
|
||||||
parameters = new CuttingParameters();
|
SaveCuttingParameters(parameters);
|
||||||
}
|
|
||||||
|
|
||||||
var assigner = new LeadInAssigner
|
var assigner = new LeadInAssigner
|
||||||
{
|
{
|
||||||
@@ -839,29 +837,88 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
var plate = PlateView.Plate;
|
var plate = PlateView.Plate;
|
||||||
|
|
||||||
// If no cutting parameters exist, initialize from saved settings or defaults
|
|
||||||
if (plate.CuttingParameters == null)
|
if (plate.CuttingParameters == null)
|
||||||
{
|
plate.CuttingParameters = LoadOrDefaultParameters(null);
|
||||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
|
||||||
if (!string.IsNullOrEmpty(json))
|
|
||||||
{
|
|
||||||
try { plate.CuttingParameters = CuttingParametersSerializer.Deserialize(json); }
|
|
||||||
catch { plate.CuttingParameters = new CuttingParameters(); }
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
plate.CuttingParameters = new CuttingParameters();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlateView.SetAction(typeof(Actions.ActionLeadIn));
|
PlateView.SetAction(typeof(Actions.ActionLeadIn));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CuttingParameters LoadOrDefaultParameters(CuttingParameters existing)
|
||||||
|
{
|
||||||
|
if (existing != null)
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
var json = Properties.Settings.Default.CuttingParametersJson;
|
||||||
|
if (!string.IsNullOrEmpty(json))
|
||||||
|
{
|
||||||
|
try { return CuttingParametersSerializer.Deserialize(json); }
|
||||||
|
catch { /* fall through */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CuttingParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveCuttingParameters(CuttingParameters parameters)
|
||||||
|
{
|
||||||
|
var json = CuttingParametersSerializer.Serialize(parameters);
|
||||||
|
Properties.Settings.Default.CuttingParametersJson = json;
|
||||||
|
Properties.Settings.Default.Save();
|
||||||
|
}
|
||||||
|
|
||||||
private void ImportDrawings_Click(object sender, EventArgs e)
|
private void ImportDrawings_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Import();
|
Import();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EditDrawingsInConverter_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (Nest.Drawings.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var converter = new CadConverterForm();
|
||||||
|
converter.LoadDrawings(Nest.Drawings);
|
||||||
|
|
||||||
|
if (converter.ShowDialog() != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var newDrawings = converter.GetDrawings();
|
||||||
|
var newByName = newDrawings.ToDictionary(d => d.Name);
|
||||||
|
|
||||||
|
// Update existing drawings in-place so parts keep their BaseDrawing references
|
||||||
|
foreach (var existing in Nest.Drawings.ToList())
|
||||||
|
{
|
||||||
|
if (newByName.TryGetValue(existing.Name, out var updated))
|
||||||
|
{
|
||||||
|
existing.Program = updated.Program;
|
||||||
|
existing.SourceEntities = updated.SourceEntities;
|
||||||
|
existing.SuppressedEntityIds = updated.SuppressedEntityIds;
|
||||||
|
existing.Source = updated.Source;
|
||||||
|
existing.Customer = updated.Customer;
|
||||||
|
existing.Quantity.Required = updated.Quantity.Required;
|
||||||
|
existing.Bends.Clear();
|
||||||
|
existing.Bends.AddRange(updated.Bends);
|
||||||
|
newByName.Remove(existing.Name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Nest.Drawings.Remove(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any new drawings that weren't in the original set
|
||||||
|
foreach (var d in newByName.Values)
|
||||||
|
Nest.Drawings.Add(d);
|
||||||
|
|
||||||
|
// Refresh all parts to use the updated programs
|
||||||
|
foreach (var plate in Nest.Plates)
|
||||||
|
foreach (var part in plate.Parts)
|
||||||
|
if (!part.BaseDrawing.IsCutOff)
|
||||||
|
part.Update();
|
||||||
|
|
||||||
|
UpdateDrawingList();
|
||||||
|
PlateView.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
private void CleanUnusedDrawings_Click(object sender, EventArgs e)
|
private void CleanUnusedDrawings_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
@@ -892,6 +949,7 @@ namespace OpenNest.Forms
|
|||||||
PlateView.Plate = PlateManager.CurrentPlate;
|
PlateView.Plate = PlateManager.CurrentPlate;
|
||||||
PlateView.ZoomToFit();
|
PlateView.ZoomToFit();
|
||||||
UpdatePlateHeader();
|
UpdatePlateHeader();
|
||||||
|
UpdateRemovePlateButton();
|
||||||
PlateChanged?.Invoke(this, EventArgs.Empty);
|
PlateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1025,6 +1083,12 @@ namespace OpenNest.Forms
|
|||||||
addPart = true;
|
addPart = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PlateView_MouseEnter(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (!PlateView.Focused)
|
||||||
|
PlateView.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
private void PlateView_Enter(object sender, EventArgs e)
|
private void PlateView_Enter(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (!addPart)
|
if (!addPart)
|
||||||
|
|||||||
+937
-589
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -63,7 +63,7 @@
|
|||||||
this.textBox2 = new System.Windows.Forms.TextBox();
|
this.textBox2 = new System.Windows.Forms.TextBox();
|
||||||
this.label5 = new System.Windows.Forms.Label();
|
this.label5 = new System.Windows.Forms.Label();
|
||||||
this.labelMaterial = new System.Windows.Forms.Label();
|
this.labelMaterial = new System.Windows.Forms.Label();
|
||||||
this.materialBox = new System.Windows.Forms.TextBox();
|
this.materialBox = new System.Windows.Forms.ComboBox();
|
||||||
this.tabPage2 = new System.Windows.Forms.TabPage();
|
this.tabPage2 = new System.Windows.Forms.TabPage();
|
||||||
this.tabPage3 = new System.Windows.Forms.TabPage();
|
this.tabPage3 = new System.Windows.Forms.TabPage();
|
||||||
this.notesBox = new System.Windows.Forms.TextBox();
|
this.notesBox = new System.Windows.Forms.TextBox();
|
||||||
@@ -516,9 +516,10 @@
|
|||||||
// materialBox
|
// materialBox
|
||||||
//
|
//
|
||||||
this.materialBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
|
this.materialBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.materialBox.FormattingEnabled = true;
|
||||||
this.materialBox.Location = new System.Drawing.Point(135, 159);
|
this.materialBox.Location = new System.Drawing.Point(135, 159);
|
||||||
this.materialBox.Name = "materialBox";
|
this.materialBox.Name = "materialBox";
|
||||||
this.materialBox.Size = new System.Drawing.Size(224, 22);
|
this.materialBox.Size = new System.Drawing.Size(224, 24);
|
||||||
this.materialBox.TabIndex = 11;
|
this.materialBox.TabIndex = 11;
|
||||||
//
|
//
|
||||||
// label3
|
// label3
|
||||||
@@ -729,6 +730,6 @@
|
|||||||
private System.Windows.Forms.RadioButton radioButton2;
|
private System.Windows.Forms.RadioButton radioButton2;
|
||||||
private System.Windows.Forms.Label label5;
|
private System.Windows.Forms.Label label5;
|
||||||
private System.Windows.Forms.Label labelMaterial;
|
private System.Windows.Forms.Label labelMaterial;
|
||||||
private System.Windows.Forms.TextBox materialBox;
|
private System.Windows.Forms.ComboBox materialBox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,9 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
foreach (var name in PostProcessorMaterials.Names)
|
||||||
|
materialBox.Items.Add(name);
|
||||||
|
|
||||||
timer = new Timer
|
timer = new Timer
|
||||||
{
|
{
|
||||||
SynchronizingObject = this,
|
SynchronizingObject = this,
|
||||||
|
|||||||
Generated
+11
-2
@@ -85,6 +85,7 @@
|
|||||||
mnuNest = new System.Windows.Forms.ToolStripMenuItem();
|
mnuNest = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem();
|
mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuNestImportDrawing = new System.Windows.Forms.ToolStripMenuItem();
|
mnuNestImportDrawing = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
mnuNestShapeLibrary = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
toolStripMenuItem7 = new System.Windows.Forms.ToolStripSeparator();
|
toolStripMenuItem7 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
mnuNestFirstPlate = new System.Windows.Forms.ToolStripMenuItem();
|
mnuNestFirstPlate = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuNestLastPlate = new System.Windows.Forms.ToolStripMenuItem();
|
mnuNestLastPlate = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
@@ -559,7 +560,7 @@
|
|||||||
//
|
//
|
||||||
// mnuNest
|
// mnuNest
|
||||||
//
|
//
|
||||||
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
|
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, mnuNestShapeLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
|
||||||
mnuNest.Name = "mnuNest";
|
mnuNest.Name = "mnuNest";
|
||||||
mnuNest.Size = new System.Drawing.Size(43, 20);
|
mnuNest.Size = new System.Drawing.Size(43, 20);
|
||||||
mnuNest.Text = "&Nest";
|
mnuNest.Text = "&Nest";
|
||||||
@@ -578,7 +579,14 @@
|
|||||||
mnuNestImportDrawing.Size = new System.Drawing.Size(205, 22);
|
mnuNestImportDrawing.Size = new System.Drawing.Size(205, 22);
|
||||||
mnuNestImportDrawing.Text = "Import Drawing";
|
mnuNestImportDrawing.Text = "Import Drawing";
|
||||||
mnuNestImportDrawing.Click += Import_Click;
|
mnuNestImportDrawing.Click += Import_Click;
|
||||||
//
|
//
|
||||||
|
// mnuNestShapeLibrary
|
||||||
|
//
|
||||||
|
mnuNestShapeLibrary.Name = "mnuNestShapeLibrary";
|
||||||
|
mnuNestShapeLibrary.Size = new System.Drawing.Size(205, 22);
|
||||||
|
mnuNestShapeLibrary.Text = "Shape Library";
|
||||||
|
mnuNestShapeLibrary.Click += ShapeLibrary_Click;
|
||||||
|
//
|
||||||
// toolStripMenuItem7
|
// toolStripMenuItem7
|
||||||
//
|
//
|
||||||
toolStripMenuItem7.Name = "toolStripMenuItem7";
|
toolStripMenuItem7.Name = "toolStripMenuItem7";
|
||||||
@@ -1213,6 +1221,7 @@
|
|||||||
private System.Windows.Forms.ToolStripMenuItem mnuNest;
|
private System.Windows.Forms.ToolStripMenuItem mnuNest;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuNestEdit;
|
private System.Windows.Forms.ToolStripMenuItem mnuNestEdit;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuNestImportDrawing;
|
private System.Windows.Forms.ToolStripMenuItem mnuNestImportDrawing;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuNestShapeLibrary;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem7;
|
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem7;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuNestFirstPlate;
|
private System.Windows.Forms.ToolStripMenuItem mnuNestFirstPlate;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuNestLastPlate;
|
private System.Windows.Forms.ToolStripMenuItem mnuNestLastPlate;
|
||||||
|
|||||||
@@ -351,6 +351,9 @@ namespace OpenNest.Forms
|
|||||||
postProcessorMenuItem.Tag = postProcessor;
|
postProcessorMenuItem.Tag = postProcessor;
|
||||||
postProcessorMenuItem.Click += PostProcessor_Click;
|
postProcessorMenuItem.Click += PostProcessor_Click;
|
||||||
mnuNestPost.DropDownItems.Add(postProcessorMenuItem);
|
mnuNestPost.DropDownItems.Add(postProcessorMenuItem);
|
||||||
|
|
||||||
|
if (postProcessor is IMaterialProvidingPostProcessor materialProvider)
|
||||||
|
PostProcessorMaterials.AddFrom(materialProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -829,6 +832,20 @@ namespace OpenNest.Forms
|
|||||||
activeForm.Import();
|
activeForm.Import();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShapeLibrary_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (activeForm == null) return;
|
||||||
|
|
||||||
|
var form = new ShapeLibraryForm();
|
||||||
|
form.ShowDialog();
|
||||||
|
|
||||||
|
var drawings = form.GetDrawings();
|
||||||
|
if (drawings.Count == 0) return;
|
||||||
|
|
||||||
|
drawings.ForEach(d => activeForm.Nest.Drawings.Add(d));
|
||||||
|
activeForm.UpdateDrawingList();
|
||||||
|
}
|
||||||
|
|
||||||
private void EditNest_Click(object sender, EventArgs e)
|
private void EditNest_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (activeForm == null) return;
|
if (activeForm == null) return;
|
||||||
@@ -1006,9 +1023,18 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
var template = activeForm.PlateView.Plate;
|
var template = activeForm.PlateView.Plate;
|
||||||
|
|
||||||
|
var nestOptions = new MultiPlateNestOptions
|
||||||
|
{
|
||||||
|
Template = template,
|
||||||
|
PlateOptions = plateOptions,
|
||||||
|
SalvageRate = salvageRate,
|
||||||
|
SortOrder = sortOrder,
|
||||||
|
MinRemnantSize = minRemnantSize,
|
||||||
|
AllowPlateCreation = allowPlateCreation,
|
||||||
|
};
|
||||||
|
|
||||||
var result = await Task.Run(() =>
|
var result = await Task.Run(() =>
|
||||||
MultiPlateNester.Nest(items, template, plateOptions, salvageRate,
|
MultiPlateNester.Nest(items, nestOptions, existingPlates, progress, token));
|
||||||
sortOrder, minRemnantSize, allowPlateCreation, existingPlates, progress, token));
|
|
||||||
|
|
||||||
foreach (var pr in result.Plates)
|
foreach (var pr in result.Plates)
|
||||||
{
|
{
|
||||||
@@ -1134,6 +1160,9 @@ namespace OpenNest.Forms
|
|||||||
if (postProcessor == null)
|
if (postProcessor == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (postProcessor is IPostProcessorNestAware nestAware)
|
||||||
|
nestAware.PrepareForNest(activeForm.Nest);
|
||||||
|
|
||||||
if (postProcessor is IConfigurablePostProcessor configurable)
|
if (postProcessor is IConfigurablePostProcessor configurable)
|
||||||
{
|
{
|
||||||
using var configForm = new PostProcessorConfigForm(configurable);
|
using var configForm = new PostProcessorConfigForm(configurable);
|
||||||
|
|||||||
+1
-1
@@ -427,7 +427,7 @@ namespace OpenNest.Forms
|
|||||||
plate1.Quantity = 0;
|
plate1.Quantity = 0;
|
||||||
previewPlateView.Plate = plate1;
|
previewPlateView.Plate = plate1;
|
||||||
previewPlateView.RotateIncrementAngle = 10D;
|
previewPlateView.RotateIncrementAngle = 10D;
|
||||||
previewPlateView.SelectedCutOff = null;
|
|
||||||
previewPlateView.ShowBendLines = false;
|
previewPlateView.ShowBendLines = false;
|
||||||
previewPlateView.Size = new System.Drawing.Size(356, 341);
|
previewPlateView.Size = new System.Drawing.Size(356, 341);
|
||||||
previewPlateView.Status = "Select";
|
previewPlateView.Status = "Select";
|
||||||
|
|||||||
+338
@@ -0,0 +1,338 @@
|
|||||||
|
namespace OpenNest.Forms
|
||||||
|
{
|
||||||
|
partial class ShapeLibraryForm
|
||||||
|
{
|
||||||
|
private System.ComponentModel.IContainer components = null;
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing && (components != null))
|
||||||
|
{
|
||||||
|
components.Dispose();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Windows Form Designer generated code
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
ColorScheme colorScheme1 = new ColorScheme();
|
||||||
|
CutOffSettings cutOffSettings1 = new CutOffSettings();
|
||||||
|
Plate plate1 = new Plate();
|
||||||
|
Collections.ObservableList<CutOff> observableList_11 = new Collections.ObservableList<CutOff>();
|
||||||
|
Collections.ObservableList<Part> observableList_12 = new Collections.ObservableList<Part>();
|
||||||
|
splitContainer = new System.Windows.Forms.SplitContainer();
|
||||||
|
shapeListBox = new System.Windows.Forms.ListBox();
|
||||||
|
layoutTable = new System.Windows.Forms.TableLayoutPanel();
|
||||||
|
fieldsTable = new System.Windows.Forms.TableLayoutPanel();
|
||||||
|
nameLabel = new System.Windows.Forms.Label();
|
||||||
|
nameTextBox = new System.Windows.Forms.TextBox();
|
||||||
|
qtyLabel = new System.Windows.Forms.Label();
|
||||||
|
quantityUpDown = new OpenNest.Controls.NumericUpDown();
|
||||||
|
configLabel = new System.Windows.Forms.Label();
|
||||||
|
configComboBox = new System.Windows.Forms.ComboBox();
|
||||||
|
contentPanel = new System.Windows.Forms.Panel();
|
||||||
|
previewBox = new OpenNest.Controls.ShapePreviewControl();
|
||||||
|
parametersPanel = new System.Windows.Forms.Panel();
|
||||||
|
buttonPanel = new System.Windows.Forms.Panel();
|
||||||
|
addButton = new System.Windows.Forms.Button();
|
||||||
|
closeButton = new System.Windows.Forms.Button();
|
||||||
|
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
|
||||||
|
splitContainer.Panel1.SuspendLayout();
|
||||||
|
splitContainer.Panel2.SuspendLayout();
|
||||||
|
splitContainer.SuspendLayout();
|
||||||
|
layoutTable.SuspendLayout();
|
||||||
|
fieldsTable.SuspendLayout();
|
||||||
|
((System.ComponentModel.ISupportInitialize)quantityUpDown).BeginInit();
|
||||||
|
contentPanel.SuspendLayout();
|
||||||
|
buttonPanel.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.Name = "splitContainer";
|
||||||
|
//
|
||||||
|
// splitContainer.Panel1
|
||||||
|
//
|
||||||
|
splitContainer.Panel1.Controls.Add(shapeListBox);
|
||||||
|
//
|
||||||
|
// splitContainer.Panel2
|
||||||
|
//
|
||||||
|
splitContainer.Panel2.Controls.Add(layoutTable);
|
||||||
|
splitContainer.Size = new System.Drawing.Size(750, 520);
|
||||||
|
splitContainer.SplitterDistance = 150;
|
||||||
|
splitContainer.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// shapeListBox
|
||||||
|
//
|
||||||
|
shapeListBox.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||||
|
shapeListBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
shapeListBox.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawFixed;
|
||||||
|
shapeListBox.Font = new System.Drawing.Font("Segoe UI", 10F);
|
||||||
|
shapeListBox.IntegralHeight = false;
|
||||||
|
shapeListBox.ItemHeight = 32;
|
||||||
|
shapeListBox.Location = new System.Drawing.Point(0, 0);
|
||||||
|
shapeListBox.Name = "shapeListBox";
|
||||||
|
shapeListBox.Size = new System.Drawing.Size(150, 520);
|
||||||
|
shapeListBox.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// layoutTable
|
||||||
|
//
|
||||||
|
layoutTable.ColumnCount = 1;
|
||||||
|
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
layoutTable.Controls.Add(fieldsTable, 0, 0);
|
||||||
|
layoutTable.Controls.Add(contentPanel, 0, 1);
|
||||||
|
layoutTable.Controls.Add(buttonPanel, 0, 2);
|
||||||
|
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
layoutTable.Location = new System.Drawing.Point(0, 0);
|
||||||
|
layoutTable.Name = "layoutTable";
|
||||||
|
layoutTable.Padding = new System.Windows.Forms.Padding(6, 4, 6, 0);
|
||||||
|
layoutTable.RowCount = 3;
|
||||||
|
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 44F));
|
||||||
|
layoutTable.Size = new System.Drawing.Size(596, 520);
|
||||||
|
layoutTable.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// fieldsTable
|
||||||
|
//
|
||||||
|
fieldsTable.AutoSize = true;
|
||||||
|
fieldsTable.ColumnCount = 2;
|
||||||
|
fieldsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||||
|
fieldsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
fieldsTable.Controls.Add(nameLabel, 0, 0);
|
||||||
|
fieldsTable.Controls.Add(nameTextBox, 1, 0);
|
||||||
|
fieldsTable.Controls.Add(qtyLabel, 0, 1);
|
||||||
|
fieldsTable.Controls.Add(quantityUpDown, 1, 1);
|
||||||
|
fieldsTable.Controls.Add(configLabel, 0, 2);
|
||||||
|
fieldsTable.Controls.Add(configComboBox, 1, 2);
|
||||||
|
fieldsTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
fieldsTable.Location = new System.Drawing.Point(6, 4);
|
||||||
|
fieldsTable.Margin = new System.Windows.Forms.Padding(0, 0, 0, 4);
|
||||||
|
fieldsTable.Name = "fieldsTable";
|
||||||
|
fieldsTable.RowCount = 3;
|
||||||
|
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
fieldsTable.Size = new System.Drawing.Size(584, 99);
|
||||||
|
fieldsTable.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// nameLabel
|
||||||
|
//
|
||||||
|
nameLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||||
|
nameLabel.AutoSize = true;
|
||||||
|
nameLabel.Location = new System.Drawing.Point(4, 8);
|
||||||
|
nameLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
|
||||||
|
nameLabel.Name = "nameLabel";
|
||||||
|
nameLabel.Size = new System.Drawing.Size(46, 17);
|
||||||
|
nameLabel.TabIndex = 0;
|
||||||
|
nameLabel.Text = "Name:";
|
||||||
|
//
|
||||||
|
// nameTextBox
|
||||||
|
//
|
||||||
|
nameTextBox.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||||
|
nameTextBox.Location = new System.Drawing.Point(106, 4);
|
||||||
|
nameTextBox.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
nameTextBox.Name = "nameTextBox";
|
||||||
|
nameTextBox.Size = new System.Drawing.Size(474, 25);
|
||||||
|
nameTextBox.TabIndex = 1;
|
||||||
|
//
|
||||||
|
// qtyLabel
|
||||||
|
//
|
||||||
|
qtyLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||||
|
qtyLabel.AutoSize = true;
|
||||||
|
qtyLabel.Location = new System.Drawing.Point(4, 41);
|
||||||
|
qtyLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
|
||||||
|
qtyLabel.Name = "qtyLabel";
|
||||||
|
qtyLabel.Size = new System.Drawing.Size(59, 17);
|
||||||
|
qtyLabel.TabIndex = 2;
|
||||||
|
qtyLabel.Text = "Quantity:";
|
||||||
|
//
|
||||||
|
// quantityUpDown
|
||||||
|
//
|
||||||
|
quantityUpDown.Location = new System.Drawing.Point(106, 37);
|
||||||
|
quantityUpDown.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
quantityUpDown.Maximum = new decimal(new int[] { 999999, 0, 0, 0 });
|
||||||
|
quantityUpDown.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||||
|
quantityUpDown.Name = "quantityUpDown";
|
||||||
|
quantityUpDown.Size = new System.Drawing.Size(100, 25);
|
||||||
|
quantityUpDown.Suffix = "";
|
||||||
|
quantityUpDown.TabIndex = 2;
|
||||||
|
quantityUpDown.Value = new decimal(new int[] { 1, 0, 0, 0 });
|
||||||
|
//
|
||||||
|
// configLabel
|
||||||
|
//
|
||||||
|
configLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||||
|
configLabel.AutoSize = true;
|
||||||
|
configLabel.Location = new System.Drawing.Point(4, 74);
|
||||||
|
configLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
|
||||||
|
configLabel.Name = "configLabel";
|
||||||
|
configLabel.Size = new System.Drawing.Size(90, 17);
|
||||||
|
configLabel.TabIndex = 3;
|
||||||
|
configLabel.Text = "Configuration:";
|
||||||
|
configLabel.Visible = false;
|
||||||
|
//
|
||||||
|
// configComboBox
|
||||||
|
//
|
||||||
|
configComboBox.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||||
|
configComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||||
|
configComboBox.Location = new System.Drawing.Point(106, 70);
|
||||||
|
configComboBox.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
configComboBox.Name = "configComboBox";
|
||||||
|
configComboBox.Size = new System.Drawing.Size(474, 25);
|
||||||
|
configComboBox.TabIndex = 3;
|
||||||
|
configComboBox.Visible = false;
|
||||||
|
//
|
||||||
|
// contentPanel
|
||||||
|
//
|
||||||
|
contentPanel.Controls.Add(previewBox);
|
||||||
|
contentPanel.Controls.Add(parametersPanel);
|
||||||
|
contentPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
contentPanel.Location = new System.Drawing.Point(9, 110);
|
||||||
|
contentPanel.Name = "contentPanel";
|
||||||
|
contentPanel.Size = new System.Drawing.Size(578, 363);
|
||||||
|
contentPanel.TabIndex = 1;
|
||||||
|
//
|
||||||
|
// previewBox
|
||||||
|
//
|
||||||
|
previewBox.ActiveWorkArea = null;
|
||||||
|
previewBox.AllowPan = false;
|
||||||
|
previewBox.AllowSelect = false;
|
||||||
|
previewBox.AllowZoom = false;
|
||||||
|
previewBox.BackColor = System.Drawing.Color.White;
|
||||||
|
colorScheme1.BackgroundColor = System.Drawing.Color.DarkGray;
|
||||||
|
colorScheme1.BoundingBoxColor = System.Drawing.Color.FromArgb(128, 128, 255);
|
||||||
|
colorScheme1.EdgeSpacingColor = System.Drawing.Color.FromArgb(180, 180, 180);
|
||||||
|
colorScheme1.LayoutFillColor = System.Drawing.Color.WhiteSmoke;
|
||||||
|
colorScheme1.LayoutOutlineColor = System.Drawing.Color.Gray;
|
||||||
|
colorScheme1.OriginColor = System.Drawing.Color.Gray;
|
||||||
|
colorScheme1.PreviewPartColor = System.Drawing.Color.FromArgb(255, 140, 0);
|
||||||
|
colorScheme1.RapidColor = System.Drawing.Color.DodgerBlue;
|
||||||
|
previewBox.ColorScheme = colorScheme1;
|
||||||
|
cutOffSettings1.CutDirection = CutDirection.AwayFromOrigin;
|
||||||
|
cutOffSettings1.MinSegmentLength = 0.05D;
|
||||||
|
cutOffSettings1.Overtravel = 0D;
|
||||||
|
cutOffSettings1.PartClearance = 0.02D;
|
||||||
|
previewBox.CutOffSettings = cutOffSettings1;
|
||||||
|
previewBox.DebugRemnantPriorities = null;
|
||||||
|
previewBox.DebugRemnants = null;
|
||||||
|
previewBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
previewBox.DrawBounds = false;
|
||||||
|
previewBox.DrawCutDirection = false;
|
||||||
|
previewBox.DrawOffset = false;
|
||||||
|
previewBox.DrawOrigin = false;
|
||||||
|
previewBox.DrawPiercePoints = false;
|
||||||
|
previewBox.DrawRapid = false;
|
||||||
|
previewBox.FillParts = true;
|
||||||
|
previewBox.Location = new System.Drawing.Point(0, 0);
|
||||||
|
previewBox.Name = "previewBox";
|
||||||
|
previewBox.OffsetIncrementDistance = 10D;
|
||||||
|
previewBox.OffsetTolerance = 0.001D;
|
||||||
|
plate1.CutOffs = observableList_11;
|
||||||
|
plate1.CuttingParameters = null;
|
||||||
|
plate1.GrainAngle = 0D;
|
||||||
|
plate1.Parts = observableList_12;
|
||||||
|
plate1.PartSpacing = 0D;
|
||||||
|
plate1.Quadrant = 1;
|
||||||
|
plate1.Quantity = 0;
|
||||||
|
previewBox.Plate = plate1;
|
||||||
|
previewBox.RotateIncrementAngle = 10D;
|
||||||
|
previewBox.ShowBendLines = false;
|
||||||
|
previewBox.Size = new System.Drawing.Size(318, 363);
|
||||||
|
previewBox.Status = "Select";
|
||||||
|
previewBox.TabIndex = 4;
|
||||||
|
previewBox.TabStop = false;
|
||||||
|
//
|
||||||
|
// parametersPanel
|
||||||
|
//
|
||||||
|
parametersPanel.AutoScroll = true;
|
||||||
|
parametersPanel.Dock = System.Windows.Forms.DockStyle.Right;
|
||||||
|
parametersPanel.Location = new System.Drawing.Point(318, 0);
|
||||||
|
parametersPanel.Name = "parametersPanel";
|
||||||
|
parametersPanel.Padding = new System.Windows.Forms.Padding(8, 0, 0, 0);
|
||||||
|
parametersPanel.Size = new System.Drawing.Size(260, 363);
|
||||||
|
parametersPanel.TabIndex = 5;
|
||||||
|
//
|
||||||
|
// buttonPanel
|
||||||
|
//
|
||||||
|
buttonPanel.Controls.Add(addButton);
|
||||||
|
buttonPanel.Controls.Add(closeButton);
|
||||||
|
buttonPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
buttonPanel.Location = new System.Drawing.Point(9, 479);
|
||||||
|
buttonPanel.Name = "buttonPanel";
|
||||||
|
buttonPanel.Size = new System.Drawing.Size(578, 38);
|
||||||
|
buttonPanel.TabIndex = 2;
|
||||||
|
//
|
||||||
|
// addButton
|
||||||
|
//
|
||||||
|
addButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||||
|
addButton.Location = new System.Drawing.Point(379, 5);
|
||||||
|
addButton.Name = "addButton";
|
||||||
|
addButton.Size = new System.Drawing.Size(100, 30);
|
||||||
|
addButton.TabIndex = 0;
|
||||||
|
addButton.Text = "Add to Nest";
|
||||||
|
addButton.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// closeButton
|
||||||
|
//
|
||||||
|
closeButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||||
|
closeButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||||
|
closeButton.Location = new System.Drawing.Point(485, 5);
|
||||||
|
closeButton.Name = "closeButton";
|
||||||
|
closeButton.Size = new System.Drawing.Size(90, 30);
|
||||||
|
closeButton.TabIndex = 1;
|
||||||
|
closeButton.Text = "Close";
|
||||||
|
closeButton.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// ShapeLibraryForm
|
||||||
|
//
|
||||||
|
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||||
|
CancelButton = closeButton;
|
||||||
|
ClientSize = new System.Drawing.Size(750, 520);
|
||||||
|
Controls.Add(splitContainer);
|
||||||
|
Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
||||||
|
MinimizeBox = false;
|
||||||
|
MinimumSize = new System.Drawing.Size(600, 400);
|
||||||
|
Name = "ShapeLibraryForm";
|
||||||
|
ShowIcon = false;
|
||||||
|
ShowInTaskbar = false;
|
||||||
|
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
|
Text = "Shape Library";
|
||||||
|
splitContainer.Panel1.ResumeLayout(false);
|
||||||
|
splitContainer.Panel2.ResumeLayout(false);
|
||||||
|
((System.ComponentModel.ISupportInitialize)splitContainer).EndInit();
|
||||||
|
splitContainer.ResumeLayout(false);
|
||||||
|
layoutTable.ResumeLayout(false);
|
||||||
|
layoutTable.PerformLayout();
|
||||||
|
fieldsTable.ResumeLayout(false);
|
||||||
|
fieldsTable.PerformLayout();
|
||||||
|
((System.ComponentModel.ISupportInitialize)quantityUpDown).EndInit();
|
||||||
|
contentPanel.ResumeLayout(false);
|
||||||
|
buttonPanel.ResumeLayout(false);
|
||||||
|
ResumeLayout(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private System.Windows.Forms.SplitContainer splitContainer;
|
||||||
|
private System.Windows.Forms.ListBox shapeListBox;
|
||||||
|
private System.Windows.Forms.TableLayoutPanel layoutTable;
|
||||||
|
private System.Windows.Forms.TableLayoutPanel fieldsTable;
|
||||||
|
private System.Windows.Forms.Label nameLabel;
|
||||||
|
private System.Windows.Forms.TextBox nameTextBox;
|
||||||
|
private System.Windows.Forms.Label qtyLabel;
|
||||||
|
private Controls.NumericUpDown quantityUpDown;
|
||||||
|
private System.Windows.Forms.Label configLabel;
|
||||||
|
private System.Windows.Forms.ComboBox configComboBox;
|
||||||
|
private System.Windows.Forms.Panel contentPanel;
|
||||||
|
private Controls.ShapePreviewControl previewBox;
|
||||||
|
private System.Windows.Forms.Panel parametersPanel;
|
||||||
|
private System.Windows.Forms.Panel buttonPanel;
|
||||||
|
private System.Windows.Forms.Button addButton;
|
||||||
|
private System.Windows.Forms.Button closeButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
using OpenNest.Shapes;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest.Forms
|
||||||
|
{
|
||||||
|
public partial class ShapeLibraryForm : Form
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly List<Drawing> addedDrawings = new List<Drawing>();
|
||||||
|
private readonly List<ShapeEntry> shapeEntries = new List<ShapeEntry>();
|
||||||
|
private readonly List<ParameterBinding> parameterBindings = new List<ParameterBinding>();
|
||||||
|
|
||||||
|
private ShapeEntry selectedEntry;
|
||||||
|
private bool suppressPreview;
|
||||||
|
|
||||||
|
public ShapeLibraryForm()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DiscoverShapes();
|
||||||
|
PopulateShapeList();
|
||||||
|
|
||||||
|
shapeListBox.DrawItem += ShapeListBox_DrawItem;
|
||||||
|
shapeListBox.SelectedIndexChanged += ShapeListBox_SelectedIndexChanged;
|
||||||
|
configComboBox.SelectedIndexChanged += ConfigComboBox_SelectedIndexChanged;
|
||||||
|
addButton.Click += AddButton_Click;
|
||||||
|
closeButton.Click += (s, e) => Close();
|
||||||
|
|
||||||
|
if (shapeListBox.Items.Count > 0)
|
||||||
|
shapeListBox.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Drawing> GetDrawings() => addedDrawings;
|
||||||
|
|
||||||
|
private void DiscoverShapes()
|
||||||
|
{
|
||||||
|
var baseType = typeof(ShapeDefinition);
|
||||||
|
var shapeTypes = baseType.Assembly.GetTypes()
|
||||||
|
.Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t))
|
||||||
|
.OrderBy(t => t.Name)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var configDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configurations");
|
||||||
|
|
||||||
|
foreach (var type in shapeTypes)
|
||||||
|
{
|
||||||
|
var entry = new ShapeEntry { ShapeType = type };
|
||||||
|
entry.DisplayName = FriendlyName(type.Name);
|
||||||
|
|
||||||
|
var configPath = Path.Combine(configDir, type.Name + ".json");
|
||||||
|
if (File.Exists(configPath))
|
||||||
|
entry.Configurations = LoadConfigurations(type, configPath);
|
||||||
|
|
||||||
|
shapeEntries.Add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ShapeDefinition> LoadConfigurations(Type shapeType, string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
var listType = typeof(List<>).MakeGenericType(shapeType);
|
||||||
|
var list = JsonSerializer.Deserialize(json, listType, JsonOptions);
|
||||||
|
return ((System.Collections.IEnumerable)list).Cast<ShapeDefinition>().ToList();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateShapeList()
|
||||||
|
{
|
||||||
|
foreach (var entry in shapeEntries)
|
||||||
|
shapeListBox.Items.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShapeListBox_DrawItem(object sender, DrawItemEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Index < 0) return;
|
||||||
|
|
||||||
|
e.DrawBackground();
|
||||||
|
|
||||||
|
var entry = (ShapeEntry)shapeListBox.Items[e.Index];
|
||||||
|
var textColor = (e.State & DrawItemState.Selected) != 0
|
||||||
|
? SystemColors.HighlightText
|
||||||
|
: SystemColors.ControlText;
|
||||||
|
|
||||||
|
var text = entry.DisplayName;
|
||||||
|
if (entry.HasConfigurations)
|
||||||
|
text += $" ({entry.Configurations.Count})";
|
||||||
|
|
||||||
|
using (var brush = new SolidBrush(textColor))
|
||||||
|
{
|
||||||
|
var format = new StringFormat { LineAlignment = StringAlignment.Center };
|
||||||
|
var rect = new RectangleF(8, e.Bounds.Y, e.Bounds.Width - 8, e.Bounds.Height);
|
||||||
|
e.Graphics.DrawString(text, e.Font, brush, rect, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.DrawFocusRectangle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShapeListBox_SelectedIndexChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (shapeListBox.SelectedIndex < 0) return;
|
||||||
|
|
||||||
|
selectedEntry = (ShapeEntry)shapeListBox.SelectedItem;
|
||||||
|
suppressPreview = true;
|
||||||
|
|
||||||
|
var hasConfigs = selectedEntry.HasConfigurations;
|
||||||
|
configLabel.Visible = hasConfigs;
|
||||||
|
configComboBox.Visible = hasConfigs;
|
||||||
|
|
||||||
|
if (hasConfigs)
|
||||||
|
{
|
||||||
|
configComboBox.Items.Clear();
|
||||||
|
foreach (var cfg in selectedEntry.Configurations)
|
||||||
|
configComboBox.Items.Add(cfg.Name);
|
||||||
|
|
||||||
|
configComboBox.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nameTextBox.Text = selectedEntry.DisplayName;
|
||||||
|
var defaults = (ShapeDefinition)Activator.CreateInstance(selectedEntry.ShapeType);
|
||||||
|
defaults.SetPreviewDefaults();
|
||||||
|
BuildParameterControls(selectedEntry.ShapeType, defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
suppressPreview = false;
|
||||||
|
UpdatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigComboBox_SelectedIndexChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (configComboBox.SelectedIndex < 0 || selectedEntry == null) return;
|
||||||
|
|
||||||
|
var config = selectedEntry.Configurations[configComboBox.SelectedIndex];
|
||||||
|
nameTextBox.Text = config.Name;
|
||||||
|
|
||||||
|
suppressPreview = true;
|
||||||
|
BuildParameterControls(selectedEntry.ShapeType, config);
|
||||||
|
suppressPreview = false;
|
||||||
|
UpdatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildParameterControls(Type shapeType, ShapeDefinition sourceValues)
|
||||||
|
{
|
||||||
|
parametersPanel.SuspendLayout();
|
||||||
|
parametersPanel.Controls.Clear();
|
||||||
|
parameterBindings.Clear();
|
||||||
|
|
||||||
|
var props = shapeType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||||
|
.Where(p => p.CanRead && p.CanWrite && p.Name != "Name")
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var panelWidth = parametersPanel.ClientSize.Width - parametersPanel.Padding.Horizontal;
|
||||||
|
var y = 4;
|
||||||
|
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
var label = new Label
|
||||||
|
{
|
||||||
|
Text = FriendlyName(prop.Name),
|
||||||
|
Location = new Point(parametersPanel.Padding.Left, y),
|
||||||
|
AutoSize = true
|
||||||
|
};
|
||||||
|
|
||||||
|
y += 18;
|
||||||
|
|
||||||
|
var tb = new TextBox
|
||||||
|
{
|
||||||
|
Location = new Point(parametersPanel.Padding.Left, y),
|
||||||
|
Width = panelWidth,
|
||||||
|
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sourceValues != null)
|
||||||
|
{
|
||||||
|
if (prop.PropertyType == typeof(int))
|
||||||
|
tb.Text = ((int)prop.GetValue(sourceValues)).ToString();
|
||||||
|
else
|
||||||
|
tb.Text = ((double)prop.GetValue(sourceValues)).ToString("G");
|
||||||
|
}
|
||||||
|
|
||||||
|
tb.TextChanged += (s, ev) => UpdatePreview();
|
||||||
|
|
||||||
|
parameterBindings.Add(new ParameterBinding { Property = prop, Control = tb });
|
||||||
|
|
||||||
|
parametersPanel.Controls.Add(label);
|
||||||
|
parametersPanel.Controls.Add(tb);
|
||||||
|
|
||||||
|
y += 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
parametersPanel.ResumeLayout(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePreview()
|
||||||
|
{
|
||||||
|
if (suppressPreview || selectedEntry == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var shape = CreateShapeFromInputs();
|
||||||
|
if (shape == null) return;
|
||||||
|
|
||||||
|
var drawing = shape.GetDrawing();
|
||||||
|
previewBox.ShowDrawing(drawing);
|
||||||
|
|
||||||
|
if (drawing?.Program != null)
|
||||||
|
{
|
||||||
|
var bb = drawing.Program.BoundingBox();
|
||||||
|
previewBox.SetInfo(
|
||||||
|
nameTextBox.Text,
|
||||||
|
string.Format("{0:F3} x {1:F3}", bb.Size.Length, bb.Size.Width));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
previewBox.ShowDrawing(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShapeDefinition CreateShapeFromInputs()
|
||||||
|
{
|
||||||
|
var shape = (ShapeDefinition)Activator.CreateInstance(selectedEntry.ShapeType);
|
||||||
|
shape.Name = nameTextBox.Text;
|
||||||
|
|
||||||
|
foreach (var binding in parameterBindings)
|
||||||
|
{
|
||||||
|
var tb = (TextBox)binding.Control;
|
||||||
|
|
||||||
|
if (binding.Property.PropertyType == typeof(int))
|
||||||
|
{
|
||||||
|
if (int.TryParse(tb.Text, out var intVal))
|
||||||
|
{
|
||||||
|
binding.Property.SetValue(shape, intVal);
|
||||||
|
tb.ForeColor = SystemColors.WindowText;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tb.ForeColor = Color.Red;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var val = ArchUnits.GetLengthInches(tb);
|
||||||
|
if (double.IsNaN(val))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
binding.Property.SetValue(shape, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddButton_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var shape = CreateShapeFromInputs();
|
||||||
|
if (shape == null) return;
|
||||||
|
|
||||||
|
var drawing = shape.GetDrawing();
|
||||||
|
drawing.Color = Drawing.GetNextColor();
|
||||||
|
drawing.Quantity.Required = (int)quantityUpDown.Value;
|
||||||
|
|
||||||
|
addedDrawings.Add(drawing);
|
||||||
|
DialogResult = DialogResult.OK;
|
||||||
|
|
||||||
|
addButton.Text = $"Added ({addedDrawings.Count})";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(
|
||||||
|
$"Failed to create shape: {ex.Message}",
|
||||||
|
"Error",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FriendlyName(string name)
|
||||||
|
{
|
||||||
|
if (name.EndsWith("Shape"))
|
||||||
|
name = name.Substring(0, name.Length - 5);
|
||||||
|
|
||||||
|
return Regex.Replace(name, @"(?<=[a-z0-9])([A-Z])", " $1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ShapeEntry
|
||||||
|
{
|
||||||
|
public Type ShapeType { get; set; }
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public List<ShapeDefinition> Configurations { get; set; }
|
||||||
|
public bool HasConfigurations => Configurations != null && Configurations.Count > 0;
|
||||||
|
|
||||||
|
public override string ToString() => DisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ParameterBinding
|
||||||
|
{
|
||||||
|
public PropertyInfo Property { get; set; }
|
||||||
|
public Control Control { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user