Compare commits
18
Commits
3c4d00baa4
...
810e37cacf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
810e37cacf | ||
|
|
8dfa45c446 | ||
|
|
b223f69572 | ||
|
|
98c574c2ad | ||
|
|
30f1008fa9 | ||
|
|
41c20eaf75 | ||
|
|
3a97253473 | ||
|
|
3eab3c5946 | ||
|
|
0e05ad04ea | ||
|
|
5ac985dc0f | ||
|
|
865754611c | ||
|
|
9db326ee5d | ||
|
|
25faba430c | ||
|
|
089df67627 | ||
|
|
11884e712d | ||
|
|
6bed736cf0 | ||
|
|
c20a079874 | ||
|
|
804a7fd9c1 |
@@ -596,6 +596,138 @@ namespace OpenNest.Geometry
|
|||||||
return minDist;
|
return minDist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+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,7 +14,7 @@ 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 results = new List<ProcessedPart>(sequenced.Count);
|
var results = new List<ProcessedPart>(sequenced.Count);
|
||||||
@@ -66,7 +66,7 @@ namespace OpenNest.Engine
|
|||||||
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)
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -152,6 +152,20 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
if (m.Msg == WM_ERASEBKGND)
|
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;
|
m.Result = (IntPtr)1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
+135
-411
@@ -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
|
||||||
@@ -260,19 +252,20 @@ namespace OpenNest.Controls
|
|||||||
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 +281,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 +309,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 +348,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 +388,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 +404,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 +424,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 +480,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 +539,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,14 +615,7 @@ 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)
|
||||||
@@ -863,6 +623,27 @@ namespace OpenNest.Controls
|
|||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void hoverTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||||
|
{
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
if (PartAdded != null)
|
if (PartAdded != null)
|
||||||
@@ -880,24 +661,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 +696,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,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>
|
||||||
@@ -1006,9 +1006,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)
|
||||||
{
|
{
|
||||||
|
|||||||
+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";
|
||||||
|
|||||||
Reference in New Issue
Block a user