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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var x = dx >= 0 ? box.Left : box.Right;
|
||||
|
||||
@@ -61,6 +61,91 @@ namespace OpenNest
|
||||
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,
|
||||
bool perimeterOnly = false)
|
||||
{
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace OpenNest.Engine.BestFit
|
||||
if (!result.Keep)
|
||||
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.Reason = "Exceeds plate dimensions";
|
||||
|
||||
@@ -11,8 +11,6 @@ namespace OpenNest.Engine.Fill
|
||||
/// </summary>
|
||||
public static class Compactor
|
||||
{
|
||||
private const double ChordTolerance = 0.001;
|
||||
|
||||
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
@@ -44,7 +42,7 @@ namespace OpenNest.Engine.Fill
|
||||
var opposite = -direction;
|
||||
|
||||
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++)
|
||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||
@@ -61,7 +59,19 @@ namespace OpenNest.Engine.Fill
|
||||
distance = edgeDist;
|
||||
|
||||
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++)
|
||||
{
|
||||
@@ -76,15 +86,19 @@ namespace OpenNest.Engine.Fill
|
||||
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
|
||||
continue;
|
||||
|
||||
movingLines ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
|
||||
movingEntities ??= halfSpacing > 0
|
||||
? (needCutouts
|
||||
? PartGeometry.GetOffsetPartEntities(moving, halfSpacing)
|
||||
: PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing))
|
||||
: (needCutouts
|
||||
? PartGeometry.GetPartEntities(moving)
|
||||
: PartGeometry.GetPerimeterEntities(moving));
|
||||
|
||||
obstacleLines[i] ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
|
||||
obstacleEntities[i] ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing)
|
||||
: PartGeometry.GetPerimeterEntities(obstacleParts[i]);
|
||||
|
||||
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
|
||||
var d = SpatialQuery.DirectionalDistance(movingEntities, obstacleEntities[i], direction);
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
@@ -157,7 +171,7 @@ namespace OpenNest.Engine.Fill
|
||||
continue;
|
||||
|
||||
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 < distance)
|
||||
distance = d;
|
||||
|
||||
+224
-160
@@ -19,22 +19,27 @@ namespace OpenNest
|
||||
{
|
||||
private readonly Plate _template;
|
||||
private readonly List<PlateOption> _plateOptions;
|
||||
private readonly List<PlateOption> _sortedOptions;
|
||||
private readonly double _salvageRate;
|
||||
private readonly double _minRemnantSize;
|
||||
private readonly List<PlateResult> _platePool;
|
||||
private readonly IProgress<NestProgress> _progress;
|
||||
private readonly CancellationToken _token;
|
||||
private readonly MultiPlateNestOptions _options;
|
||||
|
||||
private bool HasPlateOptions => _plateOptions != null && _plateOptions.Count > 0;
|
||||
|
||||
private MultiPlateNester(
|
||||
Plate template, List<PlateOption> plateOptions,
|
||||
double salvageRate, double minRemnantSize,
|
||||
MultiPlateNestOptions options,
|
||||
List<Plate> existingPlates,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
{
|
||||
_template = template;
|
||||
_plateOptions = plateOptions;
|
||||
_salvageRate = salvageRate;
|
||||
_minRemnantSize = minRemnantSize;
|
||||
_options = options;
|
||||
_template = options.Template;
|
||||
_plateOptions = options.PlateOptions;
|
||||
_sortedOptions = options.PlateOptions?.OrderBy(o => o.Cost).ToList();
|
||||
_salvageRate = options.SalvageRate;
|
||||
_minRemnantSize = options.MinRemnantSize;
|
||||
_platePool = InitializePlatePool(existingPlates);
|
||||
_progress = progress;
|
||||
_token = token;
|
||||
@@ -42,26 +47,31 @@ namespace OpenNest
|
||||
|
||||
// --- 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)
|
||||
{
|
||||
var withBounds = items.Select(i => (Item: i, Bounds: i.Drawing.Program.BoundingBox())).ToList();
|
||||
|
||||
switch (sortOrder)
|
||||
{
|
||||
case PartSortOrder.BoundingBoxArea:
|
||||
return items
|
||||
.OrderByDescending(i =>
|
||||
{
|
||||
var bb = i.Drawing.Program.BoundingBox();
|
||||
return bb.Width * bb.Length;
|
||||
})
|
||||
return withBounds
|
||||
.OrderByDescending(x => x.Bounds.Width * x.Bounds.Length)
|
||||
.Select(x => x.Item)
|
||||
.ToList();
|
||||
|
||||
case PartSortOrder.Size:
|
||||
return items
|
||||
.OrderByDescending(i =>
|
||||
{
|
||||
var bb = i.Drawing.Program.BoundingBox();
|
||||
return System.Math.Max(bb.Width, bb.Length);
|
||||
})
|
||||
return withBounds
|
||||
.OrderByDescending(x => System.Math.Max(x.Bounds.Width, x.Bounds.Length))
|
||||
.Select(x => x.Item)
|
||||
.ToList();
|
||||
|
||||
default:
|
||||
@@ -126,15 +136,7 @@ namespace OpenNest
|
||||
|
||||
foreach (var option in sorted)
|
||||
{
|
||||
var workW = option.Width - template.EdgeSpacing.Left - template.EdgeSpacing.Right;
|
||||
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)
|
||||
if (FitsBounds(OptionWorkArea(option, template), minBounds))
|
||||
{
|
||||
plate.Size = new Size(option.Width, option.Length);
|
||||
return plate;
|
||||
@@ -170,32 +172,47 @@ namespace OpenNest
|
||||
|
||||
public static MultiPlateResult Nest(
|
||||
List<NestItem> items,
|
||||
Plate template,
|
||||
List<PlateOption> plateOptions,
|
||||
double salvageRate,
|
||||
PartSortOrder sortOrder,
|
||||
double minRemnantSize,
|
||||
bool allowPlateCreation,
|
||||
List<Plate> existingPlates,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token)
|
||||
MultiPlateNestOptions options,
|
||||
List<Plate> existingPlates = null,
|
||||
IProgress<NestProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var nester = new MultiPlateNester(template, plateOptions, salvageRate,
|
||||
minRemnantSize, existingPlates, progress, token);
|
||||
return nester.Run(items, sortOrder, allowPlateCreation);
|
||||
var nester = new MultiPlateNester(options, existingPlates, progress, token);
|
||||
return nester.Run(items, options.SortOrder, options.AllowPlateCreation);
|
||||
}
|
||||
|
||||
// --- 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)
|
||||
{
|
||||
var fitsNormal = zone.Length >= partBounds.Length && zone.Width >= partBounds.Width;
|
||||
var fitsRotated = zone.Length >= partBounds.Width && zone.Width >= partBounds.Length;
|
||||
|
||||
if (!fitsNormal && !fitsRotated)
|
||||
if (!FitsBounds(zone, partBounds))
|
||||
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)
|
||||
@@ -206,9 +223,8 @@ namespace OpenNest
|
||||
|
||||
if (parts.Count > 0)
|
||||
{
|
||||
pr.Plate.Parts.AddRange(parts);
|
||||
pr.Parts.AddRange(parts);
|
||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
||||
pr.AddParts(parts);
|
||||
DecrementQuantity(item, parts.Count);
|
||||
}
|
||||
|
||||
return parts.Count;
|
||||
@@ -218,7 +234,7 @@ namespace OpenNest
|
||||
{
|
||||
var pr = new PlateResult { Plate = plate, IsNew = true };
|
||||
|
||||
if (_plateOptions != null)
|
||||
if (HasPlateOptions)
|
||||
{
|
||||
pr.ChosenSize = _plateOptions.FirstOrDefault(o =>
|
||||
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length));
|
||||
@@ -253,6 +269,29 @@ namespace OpenNest
|
||||
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 ---
|
||||
|
||||
private MultiPlateResult Run(List<NestItem> items, PartSortOrder sortOrder, bool allowPlateCreation)
|
||||
@@ -279,7 +318,7 @@ namespace OpenNest
|
||||
{
|
||||
PlaceOnNewPlates(item, bb);
|
||||
|
||||
if (item.Quantity > 0 && _plateOptions != null && _plateOptions.Count > 0)
|
||||
if (item.Quantity > 0 && HasPlateOptions)
|
||||
TryUpgradeOrNewPlate(item, bb);
|
||||
}
|
||||
}
|
||||
@@ -292,7 +331,7 @@ namespace OpenNest
|
||||
CreateSharedPlates(leftovers);
|
||||
}
|
||||
|
||||
if (_plateOptions != null && _plateOptions.Count > 0 && !_token.IsCancellationRequested)
|
||||
if (HasPlateOptions && !_token.IsCancellationRequested)
|
||||
TryConsolidateTailPlates();
|
||||
|
||||
foreach (var item in sorted.Where(i => i.Quantity > 0))
|
||||
@@ -323,19 +362,26 @@ namespace OpenNest
|
||||
break;
|
||||
|
||||
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);
|
||||
pr.Parts.AddRange(parts);
|
||||
anyPlaced = true;
|
||||
remaining = leftovers.Where(i => i.Quantity > 0).ToList();
|
||||
if (remaining.Count == 0)
|
||||
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);
|
||||
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
||||
pr.AddParts(parts);
|
||||
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)
|
||||
{
|
||||
var plate = CreatePlate(_template, _plateOptions, null);
|
||||
var pr = CreateNewPlateResult(plate);
|
||||
var placedAny = false;
|
||||
|
||||
foreach (var item in leftovers)
|
||||
@@ -364,22 +411,27 @@ namespace OpenNest
|
||||
break;
|
||||
|
||||
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);
|
||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
||||
placedAny = true;
|
||||
if (item.Quantity <= 0)
|
||||
break;
|
||||
|
||||
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)
|
||||
break;
|
||||
|
||||
var pr = CreateNewPlateResult(plate);
|
||||
pr.Parts.AddRange(plate.Parts);
|
||||
_platePool.Add(pr);
|
||||
leftovers.RemoveAll(i => i.Quantity <= 0);
|
||||
}
|
||||
@@ -388,6 +440,8 @@ namespace OpenNest
|
||||
private bool TryPlaceOnExistingPlates(NestItem item, Box partBounds)
|
||||
{
|
||||
var anyPlaced = false;
|
||||
var remnantCache = new Dictionary<PlateResult, List<Box>>();
|
||||
PlateResult lastModified = null;
|
||||
|
||||
while (item.Quantity > 0 && !_token.IsCancellationRequested)
|
||||
{
|
||||
@@ -400,14 +454,17 @@ namespace OpenNest
|
||||
if (_token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
var workArea = pr.Plate.WorkArea();
|
||||
var classification = Classify(partBounds, workArea);
|
||||
if (pr == lastModified || !remnantCache.ContainsKey(pr))
|
||||
{
|
||||
var workArea = pr.Plate.WorkArea();
|
||||
var classification = Classify(partBounds, workArea);
|
||||
|
||||
var remnants = classification == PartClass.Small
|
||||
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
|
||||
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
|
||||
remnantCache[pr] = classification == PartClass.Small
|
||||
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
|
||||
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
|
||||
}
|
||||
|
||||
foreach (var zone in remnants)
|
||||
foreach (var zone in remnantCache[pr])
|
||||
{
|
||||
var score = ScoreZone(zone, partBounds);
|
||||
if (score > bestScore)
|
||||
@@ -425,6 +482,7 @@ namespace OpenNest
|
||||
if (FillAndPlace(bestPlate, bestZone, item) == 0)
|
||||
break;
|
||||
|
||||
lastModified = bestPlate;
|
||||
anyPlaced = true;
|
||||
}
|
||||
|
||||
@@ -440,9 +498,7 @@ namespace OpenNest
|
||||
var plate = CreatePlate(_template, _plateOptions, partBounds);
|
||||
var workArea = plate.WorkArea();
|
||||
|
||||
if (partBounds.Length > workArea.Length && partBounds.Length > workArea.Width)
|
||||
break;
|
||||
if (partBounds.Width > workArea.Width && partBounds.Width > workArea.Length)
|
||||
if (!FitsBounds(workArea, partBounds))
|
||||
break;
|
||||
|
||||
var pr = CreateNewPlateResult(plate);
|
||||
@@ -459,36 +515,27 @@ namespace OpenNest
|
||||
|
||||
private bool TryUpgradeOrNewPlate(NestItem item, Box partBounds)
|
||||
{
|
||||
if (_plateOptions == null || _plateOptions.Count == 0)
|
||||
if (!HasPlateOptions)
|
||||
return false;
|
||||
|
||||
var sortedOptions = _plateOptions.OrderBy(o => o.Cost).ToList();
|
||||
|
||||
foreach (var pr in _platePool.Where(p => p.IsNew && p.ChosenSize != null))
|
||||
{
|
||||
var currentOption = pr.ChosenSize;
|
||||
var currentIdx = sortedOptions.FindIndex(o =>
|
||||
var currentIdx = _sortedOptions.FindIndex(o =>
|
||||
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;
|
||||
|
||||
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
|
||||
|| upgradeOption.Length < currentOption.Length - Tolerance.Epsilon)
|
||||
continue;
|
||||
|
||||
var smallestNew = sortedOptions.FirstOrDefault(o =>
|
||||
{
|
||||
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);
|
||||
});
|
||||
var smallestNew = FindSmallestFittingOption(partBounds);
|
||||
|
||||
if (smallestNew == null)
|
||||
continue;
|
||||
@@ -499,22 +546,19 @@ namespace OpenNest
|
||||
|
||||
if (decision.ShouldUpgrade)
|
||||
{
|
||||
var oldSize = pr.Plate.Size;
|
||||
var oldChosenSize = pr.ChosenSize;
|
||||
var placed = TryWithUpgradedSize(pr, upgradeOption, remnants =>
|
||||
{
|
||||
foreach (var remnant in remnants)
|
||||
{
|
||||
if (FillAndPlace(pr, remnant, item) > 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
pr.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length);
|
||||
pr.ChosenSize = upgradeOption;
|
||||
|
||||
var remainingArea = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
|
||||
|
||||
if (remainingArea.Count > 0 && FillAndPlace(pr, remainingArea[0], item) > 0)
|
||||
if (placed)
|
||||
return true;
|
||||
|
||||
// Revert if nothing was placed.
|
||||
pr.Plate.Size = oldSize;
|
||||
pr.ChosenSize = oldChosenSize;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,73 +567,93 @@ namespace OpenNest
|
||||
|
||||
private void TryConsolidateTailPlates()
|
||||
{
|
||||
var activePlates = _platePool.Where(p => p.Parts.Count > 0 && p.IsNew).ToList();
|
||||
if (activePlates.Count < 2)
|
||||
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)
|
||||
var consolidated = true;
|
||||
while (consolidated)
|
||||
{
|
||||
if (target == donor || target.ChosenSize == null)
|
||||
continue;
|
||||
consolidated = false;
|
||||
|
||||
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.
|
||||
foreach (var upgradeOption in sortedOptions.Where(o =>
|
||||
o.Width >= currentOption.Width - Tolerance.Epsilon
|
||||
&& o.Length >= currentOption.Length - Tolerance.Epsilon
|
||||
&& (o.Width > currentOption.Width + Tolerance.Epsilon
|
||||
|| o.Length > currentOption.Length + Tolerance.Epsilon)))
|
||||
var donors = activePlates.OrderBy(p => p.Plate.Utilization()).ToList();
|
||||
|
||||
foreach (var donor in donors)
|
||||
{
|
||||
var oldSize = target.Plate.Size;
|
||||
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;
|
||||
if (donor.Parts.Count == 0)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to pack all donor parts into the remnant space.
|
||||
var engine = NestEngineRegistry.Create(target.Plate);
|
||||
var tempItems = donorParts
|
||||
.GroupBy(p => p.BaseDrawing.Name)
|
||||
.Select(g => new NestItem
|
||||
{
|
||||
Drawing = g.First().BaseDrawing,
|
||||
Quantity = g.Count(),
|
||||
})
|
||||
.ToList();
|
||||
var donorParts = donor.Parts.ToList();
|
||||
var absorbed = false;
|
||||
|
||||
var placed = engine.PackArea(remnants[0], tempItems, _progress, _token);
|
||||
|
||||
if (placed.Count >= donorParts.Count)
|
||||
foreach (var target in activePlates)
|
||||
{
|
||||
// All donor parts fit — absorb them.
|
||||
target.Plate.Parts.AddRange(placed);
|
||||
target.Parts.AddRange(placed);
|
||||
if (target == donor || target.ChosenSize == null || target.Parts.Count == 0)
|
||||
continue;
|
||||
|
||||
foreach (var p in donorParts)
|
||||
donor.Plate.Parts.Remove(p);
|
||||
donor.Parts.Clear();
|
||||
_platePool.Remove(donor);
|
||||
return;
|
||||
var currentOption = target.ChosenSize;
|
||||
|
||||
foreach (var upgradeOption in _sortedOptions.Where(o =>
|
||||
o.Width >= currentOption.Width - Tolerance.Epsilon
|
||||
&& 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.
|
||||
target.Plate.Size = oldSize;
|
||||
target.ChosenSize = oldChosenSize;
|
||||
if (absorbed)
|
||||
{
|
||||
consolidated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,16 @@ using System.Collections.Generic;
|
||||
|
||||
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 List<PlateResult> Plates { get; set; } = new();
|
||||
@@ -14,5 +24,11 @@ namespace OpenNest
|
||||
public List<Part> Parts { get; set; } = new();
|
||||
public PlateOption ChosenSize { 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
|
||||
{
|
||||
public class PlateResult
|
||||
public class PlateProcessingResult
|
||||
{
|
||||
public List<ProcessedPart> Parts { get; init; }
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace OpenNest.Engine
|
||||
public ContourCuttingStrategy CuttingStrategy { 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 results = new List<ProcessedPart>(sequenced.Count);
|
||||
@@ -66,7 +66,7 @@ namespace OpenNest.Engine
|
||||
currentPoint = ToPlateSpace(lastCutLocal, part);
|
||||
}
|
||||
|
||||
return new PlateResult { Parts = results };
|
||||
return new PlateProcessingResult { Parts = results };
|
||||
}
|
||||
|
||||
private static Vector ToPartLocal(Vector platePoint, Part part)
|
||||
|
||||
@@ -229,16 +229,9 @@ public class MultiPlateNesterTests
|
||||
MakeItem("big2", 70, 35, 1),
|
||||
};
|
||||
|
||||
var result = MultiPlateNester.Nest(
|
||||
items, template,
|
||||
plateOptions: null,
|
||||
salvageRate: 0.5,
|
||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
||||
minRemnantSize: 12.0,
|
||||
allowPlateCreation: true,
|
||||
existingPlates: null,
|
||||
progress: null,
|
||||
token: CancellationToken.None);
|
||||
var options = new MultiPlateNestOptions { Template = template };
|
||||
|
||||
var result = MultiPlateNester.Nest(items, options);
|
||||
|
||||
// Each large part should be on its own plate.
|
||||
Assert.True(result.Plates.Count >= 2,
|
||||
@@ -261,16 +254,9 @@ public class MultiPlateNesterTests
|
||||
MakeItem("tinyB", 4, 4, 3),
|
||||
};
|
||||
|
||||
var result = MultiPlateNester.Nest(
|
||||
items, template,
|
||||
plateOptions: null,
|
||||
salvageRate: 0.5,
|
||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
||||
minRemnantSize: 12.0,
|
||||
allowPlateCreation: true,
|
||||
existingPlates: null,
|
||||
progress: null,
|
||||
token: CancellationToken.None);
|
||||
var options = new MultiPlateNestOptions { Template = template };
|
||||
|
||||
var result = MultiPlateNester.Nest(items, options);
|
||||
|
||||
// Both small drawing types should share space — not each on their own plate.
|
||||
// With consolidation, they pack into remaining space alongside the big part.
|
||||
@@ -291,16 +277,13 @@ public class MultiPlateNesterTests
|
||||
MakeItem("big2", 70, 35, 1),
|
||||
};
|
||||
|
||||
var result = MultiPlateNester.Nest(
|
||||
items, template,
|
||||
plateOptions: null,
|
||||
salvageRate: 0.5,
|
||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
||||
minRemnantSize: 12.0,
|
||||
allowPlateCreation: false,
|
||||
existingPlates: null,
|
||||
progress: null,
|
||||
token: CancellationToken.None);
|
||||
var options = new MultiPlateNestOptions
|
||||
{
|
||||
Template = template,
|
||||
AllowPlateCreation = false,
|
||||
};
|
||||
|
||||
var result = MultiPlateNester.Nest(items, options);
|
||||
|
||||
// No existing plates and no plate creation — nothing can be placed.
|
||||
Assert.Empty(result.Plates);
|
||||
@@ -325,16 +308,10 @@ public class MultiPlateNesterTests
|
||||
MakeItem("medium", 24, 22, 1),
|
||||
};
|
||||
|
||||
var result = MultiPlateNester.Nest(
|
||||
items, template,
|
||||
plateOptions: null,
|
||||
salvageRate: 0.5,
|
||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
||||
minRemnantSize: 12.0,
|
||||
allowPlateCreation: true,
|
||||
existingPlates: new List<Plate> { existingPlate },
|
||||
progress: null,
|
||||
token: CancellationToken.None);
|
||||
var options = new MultiPlateNestOptions { Template = template };
|
||||
|
||||
var result = MultiPlateNester.Nest(items, options,
|
||||
existingPlates: new List<Plate> { existingPlate });
|
||||
|
||||
// Part should be placed on the existing plate, not a new one.
|
||||
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("");
|
||||
|
||||
var result = MultiPlateNester.Nest(
|
||||
items, template,
|
||||
plateOptions: plateOptions,
|
||||
salvageRate: 0.5,
|
||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
||||
minRemnantSize: 12.0,
|
||||
allowPlateCreation: true,
|
||||
existingPlates: null,
|
||||
progress: null,
|
||||
token: CancellationToken.None);
|
||||
var options = new MultiPlateNestOptions
|
||||
{
|
||||
Template = template,
|
||||
PlateOptions = plateOptions,
|
||||
};
|
||||
|
||||
var result = MultiPlateNester.Nest(items, options);
|
||||
|
||||
_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)
|
||||
{
|
||||
var itemBottom = 0;
|
||||
|
||||
if (Items.Count > 0)
|
||||
{
|
||||
var lastVisible = System.Math.Min(TopIndex + (ClientSize.Height / ItemHeight), Items.Count - 1);
|
||||
itemBottom = GetItemRectangle(lastVisible).Bottom;
|
||||
}
|
||||
|
||||
if (itemBottom < ClientSize.Height)
|
||||
{
|
||||
using var g = Graphics.FromHdc(m.WParam);
|
||||
g.FillRectangle(Brushes.White, 0, itemBottom, ClientSize.Width, ClientSize.Height - itemBottom);
|
||||
}
|
||||
|
||||
m.Result = (IntPtr)1;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ namespace OpenNest.Controls
|
||||
if (program == null || program.Codes.Count == 0)
|
||||
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)
|
||||
{
|
||||
|
||||
+135
-411
@@ -1,5 +1,4 @@
|
||||
using OpenNest.Actions;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Collections;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Forms;
|
||||
@@ -8,7 +7,6 @@ using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
@@ -16,31 +14,30 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Action = OpenNest.Actions.Action;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
public class PlateView : DrawControl
|
||||
{
|
||||
private readonly Font programIdFont;
|
||||
private readonly Timer redrawTimer;
|
||||
|
||||
private string status;
|
||||
private Plate plate;
|
||||
private Action currentAction;
|
||||
private Action previousAction;
|
||||
private ActionManager actionManager;
|
||||
private CutOffSettings cutOffSettings = new CutOffSettings();
|
||||
private CutOff selectedCutOff;
|
||||
private bool draggingCutOff;
|
||||
private Dictionary<Part, Geometry.Entity> dragPerimeterCache;
|
||||
private SelectionManager selection;
|
||||
private CutOffHandler cutOffHandler;
|
||||
private PreviewManager previewManager;
|
||||
protected List<LayoutPart> parts;
|
||||
private List<LayoutPart> stationaryParts = new List<LayoutPart>();
|
||||
private List<LayoutPart> activeParts = new List<LayoutPart>();
|
||||
private Point middleMouseDownPoint;
|
||||
private Box activeWorkArea;
|
||||
private List<Box> debugRemnants;
|
||||
private PlateRenderer renderer;
|
||||
private LayoutPart hoveredPart;
|
||||
private Point hoverPoint;
|
||||
private bool showTooltip;
|
||||
private Timer hoverTimer;
|
||||
|
||||
public Box ActiveWorkArea
|
||||
{
|
||||
@@ -64,13 +61,23 @@ namespace OpenNest.Controls
|
||||
|
||||
public List<int> DebugRemnantPriorities { get; set; }
|
||||
|
||||
public List<LayoutPart> SelectedParts;
|
||||
public ReadOnlyCollection<LayoutPart> Parts;
|
||||
public List<LayoutPart> SelectedParts => selection.SelectedParts;
|
||||
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<ItemRemovedEventArgs<Part>> PartRemoved;
|
||||
public event EventHandler StatusChanged;
|
||||
public event EventHandler SelectionChanged;
|
||||
|
||||
public event EventHandler SelectionChanged
|
||||
{
|
||||
add => selection.SelectionChanged += value;
|
||||
remove => selection.SelectionChanged -= value;
|
||||
}
|
||||
|
||||
public PlateView()
|
||||
: this(ColorScheme.Default)
|
||||
@@ -80,11 +87,11 @@ namespace OpenNest.Controls
|
||||
public PlateView(ColorScheme colorScheme)
|
||||
{
|
||||
Plate = new Plate(60, 120);
|
||||
programIdFont = new Font(DefaultFont, FontStyle.Bold | FontStyle.Underline);
|
||||
origin = new PointF();
|
||||
parts = new List<LayoutPart>();
|
||||
Parts = new ReadOnlyCollection<LayoutPart>(parts);
|
||||
SelectedParts = new List<LayoutPart>();
|
||||
selection = new SelectionManager(this);
|
||||
cutOffHandler = new CutOffHandler(this);
|
||||
previewManager = new PreviewManager(this);
|
||||
|
||||
redrawTimer = new Timer()
|
||||
{
|
||||
@@ -94,6 +101,9 @@ namespace OpenNest.Controls
|
||||
};
|
||||
redrawTimer.Elapsed += redrawTimer_Elapsed;
|
||||
|
||||
hoverTimer = new Timer() { AutoReset = false, Interval = 1000 };
|
||||
hoverTimer.Elapsed += hoverTimer_Elapsed;
|
||||
|
||||
SetStyle(
|
||||
ControlStyles.AllPaintingInWmPaint |
|
||||
ControlStyles.OptimizedDoubleBuffer |
|
||||
@@ -115,7 +125,8 @@ namespace OpenNest.Controls
|
||||
DrawOffset = false;
|
||||
FillParts = true;
|
||||
renderer = new PlateRenderer(this);
|
||||
SetAction(typeof(ActionSelect));
|
||||
actionManager = new ActionManager(this);
|
||||
actionManager.SetAction(typeof(ActionSelect));
|
||||
|
||||
UpdateMatrix();
|
||||
}
|
||||
@@ -148,14 +159,9 @@ namespace OpenNest.Controls
|
||||
|
||||
internal List<LayoutPart> LayoutParts => parts;
|
||||
|
||||
internal IReadOnlyList<LayoutPart> PreviewParts =>
|
||||
activeParts.Count > 0 ? activeParts : stationaryParts;
|
||||
|
||||
internal Brush PreviewBrush =>
|
||||
activeParts.Count > 0 ? ColorScheme.ActivePreviewPartBrush : ColorScheme.PreviewPartBrush;
|
||||
|
||||
internal Pen PreviewPen =>
|
||||
activeParts.Count > 0 ? ColorScheme.ActivePreviewPartPen : ColorScheme.PreviewPartPen;
|
||||
internal IReadOnlyList<LayoutPart> PreviewParts => previewManager.PreviewParts;
|
||||
internal Brush PreviewBrush => previewManager.PreviewBrush;
|
||||
internal Pen PreviewPen => previewManager.PreviewPen;
|
||||
|
||||
internal RectangleF GetViewBounds() =>
|
||||
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 OffsetIncrementDistance { get; set; }
|
||||
@@ -200,9 +196,8 @@ namespace OpenNest.Controls
|
||||
plate.PartAdded -= plate_PartAdded;
|
||||
plate.PartRemoved -= plate_PartRemoved;
|
||||
parts.Clear();
|
||||
stationaryParts.Clear();
|
||||
activeParts.Clear();
|
||||
SelectedParts.Clear();
|
||||
previewManager.Clear();
|
||||
selection.Clear();
|
||||
}
|
||||
|
||||
plate = p;
|
||||
@@ -212,10 +207,7 @@ namespace OpenNest.Controls
|
||||
foreach (var part in plate.Parts)
|
||||
parts.Add(LayoutPart.Create(part, this));
|
||||
|
||||
if (currentAction == null || !currentAction.SurvivesPlateChange)
|
||||
SetAction(typeof(ActionSelect));
|
||||
else
|
||||
currentAction.OnPlateChanged();
|
||||
actionManager?.OnPlateChanged();
|
||||
}
|
||||
|
||||
public string Status
|
||||
@@ -260,19 +252,20 @@ namespace OpenNest.Controls
|
||||
if (e.Button == MouseButtons.Middle)
|
||||
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)
|
||||
{
|
||||
SelectedCutOff = hitCutOff;
|
||||
draggingCutOff = true;
|
||||
dragPerimeterCache = Plate.BuildPerimeterCache(Plate);
|
||||
selection.DeselectParts();
|
||||
selection.SelectedCutOffs.Clear();
|
||||
selection.SelectedCutOffs.Add(hitCutOff);
|
||||
Invalidate();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedCutOff = null;
|
||||
selection.DeselectCutOffs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,17 +281,14 @@ namespace OpenNest.Controls
|
||||
|
||||
if (dx * dx + dy * dy < 25)
|
||||
{
|
||||
RotateSelectedParts(Angle.ToRadians(90));
|
||||
selection.RotateSelectedParts(Angle.ToRadians(90));
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
if (draggingCutOff && selectedCutOff != null)
|
||||
if (cutOffHandler.IsDragging && selection.SelectedCutOffs.Count > 0)
|
||||
{
|
||||
draggingCutOff = false;
|
||||
dragPerimeterCache = null;
|
||||
Plate.RegenerateCutOffs(cutOffSettings);
|
||||
Invalidate();
|
||||
cutOffHandler.EndDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -319,7 +309,7 @@ namespace OpenNest.Controls
|
||||
|
||||
var angle = Angle.ToRadians((e.Delta > 0 ? -increment : increment) * multiplier);
|
||||
|
||||
RotateSelectedParts(angle);
|
||||
selection.RotateSelectedParts(angle);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -358,18 +348,30 @@ namespace OpenNest.Controls
|
||||
|
||||
lastPoint = e.Location;
|
||||
|
||||
if (draggingCutOff && selectedCutOff != null)
|
||||
if (cutOffHandler.IsDragging && selection.SelectedCutOffs.Count > 0)
|
||||
{
|
||||
if (selectedCutOff.Axis == CutOffAxis.Vertical)
|
||||
selectedCutOff.Position = new Vector(CurrentPoint.X, selectedCutOff.Position.Y);
|
||||
else
|
||||
selectedCutOff.Position = new Vector(selectedCutOff.Position.X, CurrentPoint.Y);
|
||||
|
||||
selectedCutOff.Regenerate(Plate, cutOffSettings, dragPerimeterCache);
|
||||
Invalidate();
|
||||
cutOffHandler.UpdateDrag(CurrentPoint, selection.SelectedCutOffs[0]);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -386,17 +388,7 @@ namespace OpenNest.Controls
|
||||
switch (e.KeyCode)
|
||||
{
|
||||
case Keys.Delete:
|
||||
if (selectedCutOff != null)
|
||||
{
|
||||
Plate.CutOffs.Remove(selectedCutOff);
|
||||
selectedCutOff = null;
|
||||
Plate.RegenerateCutOffs(cutOffSettings);
|
||||
Invalidate();
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveSelectedParts();
|
||||
}
|
||||
selection.DeleteSelected();
|
||||
break;
|
||||
|
||||
case Keys.F:
|
||||
@@ -412,15 +404,7 @@ namespace OpenNest.Controls
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessEscapeKey()
|
||||
{
|
||||
if (currentAction.IsBusy())
|
||||
currentAction.CancelAction();
|
||||
else if (currentAction is ActionSelect && previousAction != null)
|
||||
RestorePreviousAction();
|
||||
else
|
||||
SetAction(typeof(ActionSelect));
|
||||
}
|
||||
public void ProcessEscapeKey() => actionManager.ProcessEscapeKey();
|
||||
|
||||
protected override bool ProcessDialogKey(Keys keyData)
|
||||
{
|
||||
@@ -440,22 +424,22 @@ namespace OpenNest.Controls
|
||||
|
||||
case Keys.X:
|
||||
case Keys.Shift | Keys.Left:
|
||||
PushSelected(PushDirection.Left);
|
||||
selection.PushSelected(PushDirection.Left);
|
||||
break;
|
||||
|
||||
case Keys.Shift | Keys.X:
|
||||
case Keys.Shift | Keys.Right:
|
||||
PushSelected(PushDirection.Right);
|
||||
selection.PushSelected(PushDirection.Right);
|
||||
break;
|
||||
|
||||
case Keys.Shift | Keys.Y:
|
||||
case Keys.Shift | Keys.Up:
|
||||
PushSelected(PushDirection.Up);
|
||||
selection.PushSelected(PushDirection.Up);
|
||||
break;
|
||||
|
||||
case Keys.Y:
|
||||
case Keys.Shift | Keys.Down:
|
||||
PushSelected(PushDirection.Down);
|
||||
selection.PushSelected(PushDirection.Down);
|
||||
break;
|
||||
|
||||
case Keys.Right:
|
||||
@@ -496,229 +480,53 @@ namespace OpenNest.Controls
|
||||
renderer.DrawDebugRemnants(e.Graphics);
|
||||
|
||||
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)
|
||||
{
|
||||
base.OnHandleDestroyed(e);
|
||||
|
||||
if (currentAction != null)
|
||||
{
|
||||
currentAction.CancelAction();
|
||||
currentAction.DisconnectEvents();
|
||||
currentAction = null;
|
||||
}
|
||||
actionManager.Cleanup();
|
||||
}
|
||||
|
||||
public override void Refresh()
|
||||
{
|
||||
parts.ForEach(p => p.Update(this));
|
||||
stationaryParts.ForEach(p => p.Update(this));
|
||||
activeParts.ForEach(p => p.Update(this));
|
||||
previewManager.Update();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
|
||||
{
|
||||
if (Plate?.CutOffs == null)
|
||||
return null;
|
||||
public CutOff GetCutOffAtPoint(Vector point, double tolerance) => cutOffHandler.GetCutOffAtPoint(point, tolerance);
|
||||
|
||||
foreach (var cutoff in Plate.CutOffs)
|
||||
{
|
||||
var program = cutoff.Drawing?.Program;
|
||||
if (program == null)
|
||||
continue;
|
||||
public LayoutPart GetPartAtControlPoint(Point pt) => selection.GetPartAtControlPoint(pt);
|
||||
public LayoutPart GetPartAtGraphPoint(PointF pt) => selection.GetPartAtGraphPoint(pt);
|
||||
public LayoutPart GetPartAtPoint(Vector pt) => selection.GetPartAtPoint(pt);
|
||||
public IList<LayoutPart> GetPartsFromWindow(RectangleF rect, SelectionType selectionType) => selection.GetPartsFromWindow(rect, selectionType);
|
||||
|
||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||
{
|
||||
if (program.Codes[i] is RapidMove rapid &&
|
||||
program.Codes[i + 1] is LinearMove linear)
|
||||
{
|
||||
var line = new Geometry.Line(rapid.EndPoint, linear.EndPoint);
|
||||
if (line.ClosestPointTo(point).DistanceTo(point) <= tolerance)
|
||||
return cutoff;
|
||||
}
|
||||
}
|
||||
}
|
||||
public void SetAction(Type type) => actionManager.SetAction(type);
|
||||
public void SetAction(Type type, params object[] args) => actionManager.SetAction(type, args);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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 AlignSelected(AlignType alignType) => selection.AlignSelected(alignType);
|
||||
public void AlignSelected(AlignType alignType, LayoutPart fixedPart) => selection.AlignSelected(alignType, fixedPart);
|
||||
|
||||
public void AddPartFromDrawing(Drawing dwg, Vector location)
|
||||
{
|
||||
@@ -731,51 +539,10 @@ namespace OpenNest.Controls
|
||||
Plate.Parts.Add(part);
|
||||
}
|
||||
|
||||
public void SetStationaryParts(List<Part> parts)
|
||||
{
|
||||
stationaryParts.Clear();
|
||||
activeParts.Clear();
|
||||
|
||||
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 void SetStationaryParts(List<Part> parts) => previewManager.SetStationaryParts(parts);
|
||||
public void SetActiveParts(List<Part> parts) => previewManager.SetActiveParts(parts);
|
||||
public void ClearPreviewParts() => previewManager.ClearPreviewParts();
|
||||
public void AcceptPreviewParts(List<Part> parts) => previewManager.AcceptPreviewParts(parts);
|
||||
|
||||
public async void FillWithProgress(List<Part> groupParts, Box workArea)
|
||||
{
|
||||
@@ -848,14 +615,7 @@ namespace OpenNest.Controls
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveSelectedParts()
|
||||
{
|
||||
foreach (var part in SelectedParts)
|
||||
Plate.Parts.Remove(part.BasePart);
|
||||
|
||||
DeselectAll();
|
||||
Invalidate();
|
||||
}
|
||||
public void RemoveSelectedParts() => selection.RemoveSelectedParts();
|
||||
|
||||
|
||||
private void redrawTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||
@@ -863,6 +623,27 @@ namespace OpenNest.Controls
|
||||
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)
|
||||
{
|
||||
if (PartAdded != null)
|
||||
@@ -880,24 +661,9 @@ namespace OpenNest.Controls
|
||||
parts.RemoveAll(p => p.BasePart == e.Item);
|
||||
}
|
||||
|
||||
public void DeselectAll()
|
||||
{
|
||||
SelectedParts.ForEach(p => p.IsSelected = false);
|
||||
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 void DeselectAll() => selection.DeselectAll();
|
||||
public void SelectAll() => selection.SelectAll();
|
||||
public void NotifySelectionChanged() => selection.NotifySelectionChanged();
|
||||
|
||||
public override void ZoomToPoint(Vector pt, float zoomFactor, bool redraw = true)
|
||||
{
|
||||
@@ -930,57 +696,15 @@ namespace OpenNest.Controls
|
||||
ZoomToArea(plate.BoundingBox(false), redraw);
|
||||
}
|
||||
|
||||
public void PushSelected(PushDirection direction)
|
||||
{
|
||||
var movingParts = SelectedParts.Select(p => p.BasePart).ToList();
|
||||
Compactor.Push(movingParts, Plate, direction);
|
||||
SelectedParts.ForEach(p => p.IsDirty = true);
|
||||
Invalidate();
|
||||
}
|
||||
public void PushSelected(PushDirection direction) => selection.PushSelected(direction);
|
||||
|
||||
private string GetDisplayName(Type type)
|
||||
{
|
||||
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);
|
||||
}
|
||||
public void RotateSelectedParts(double angle) => selection.RotateSelectedParts(angle);
|
||||
|
||||
protected override void UpdateMatrix()
|
||||
{
|
||||
base.UpdateMatrix();
|
||||
parts.ForEach(p => p.Update(this));
|
||||
stationaryParts.ForEach(p => p.Update(this));
|
||||
activeParts.ForEach(p => p.Update(this));
|
||||
previewManager.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 nestOptions = new MultiPlateNestOptions
|
||||
{
|
||||
Template = template,
|
||||
PlateOptions = plateOptions,
|
||||
SalvageRate = salvageRate,
|
||||
SortOrder = sortOrder,
|
||||
MinRemnantSize = minRemnantSize,
|
||||
AllowPlateCreation = allowPlateCreation,
|
||||
};
|
||||
|
||||
var result = await Task.Run(() =>
|
||||
MultiPlateNester.Nest(items, template, plateOptions, salvageRate,
|
||||
sortOrder, minRemnantSize, allowPlateCreation, existingPlates, progress, token));
|
||||
MultiPlateNester.Nest(items, nestOptions, existingPlates, progress, token));
|
||||
|
||||
foreach (var pr in result.Plates)
|
||||
{
|
||||
|
||||
+1
-1
@@ -427,7 +427,7 @@ namespace OpenNest.Forms
|
||||
plate1.Quantity = 0;
|
||||
previewPlateView.Plate = plate1;
|
||||
previewPlateView.RotateIncrementAngle = 10D;
|
||||
previewPlateView.SelectedCutOff = null;
|
||||
|
||||
previewPlateView.ShowBendLines = false;
|
||||
previewPlateView.Size = new System.Drawing.Size(356, 341);
|
||||
previewPlateView.Status = "Select";
|
||||
|
||||
Reference in New Issue
Block a user