18 Commits
Author SHA1 Message Date
ajandClaude Opus 4.6 810e37cacf feat: improve multi-plate nesting with multi-remnant filling and better zone scoring
- Iterate all remnants instead of only the first when packing and filling
- Improve ScoreZone with estimated part count and aspect ratio matching
- Cache bounding boxes in SortItems and remnants in TryPlaceOnExistingPlates
- Make TryConsolidateTailPlates loop until stable, trying all donor/target pairs
- Fix consolidation grouping to use BaseDrawing reference instead of name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:20:29 -04:00
ajandClaude Opus 4.6 8dfa45c446 refactor: rename PlateResult to PlateProcessingResult
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:20:14 -04:00
ajandClaude Opus 4.6 b223f69572 chore: add missing BendLineDialog designer resource
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:20:11 -04:00
ajandClaude Opus 4.6 98c574c2ad perf: defer Path.IsVisible hit-test to hover timer callback
Move the expensive per-part hit-test out of OnMouseMove and into
the hoverTimer callback. The hit-test now only runs once after
1000ms of stillness, not on every mouse move event.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:19:48 -04:00
ajandClaude Opus 4.6 30f1008fa9 feat: show hover tooltip only after 1000ms of mouse stillness
Add a hoverTimer that restarts on each mouse move over a part.
Tooltip only renders after the timer fires, hiding while the
cursor is in motion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:12:02 -04:00
ajandClaude Opus 4.6 41c20eaf75 feat: make hover tooltip follow the cursor
Update hoverPoint on every mouse move while over a part, not just
when the hovered part changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:59:14 -04:00
ajandClaude Opus 4.6 3a97253473 perf: add bounding box pre-check before Path.IsVisible in hover detection
Path.IsVisible was consuming 52% of CPU on mouse move. Add a cheap
GetBounds().Contains() check first so only parts under the cursor
hit the expensive GDI+ path test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:55:22 -04:00
ajandClaude Opus 4.6 3eab3c5946 fix: guard against null actionManager during PlateView construction
Plate setter is called in the constructor before actionManager is
initialized, causing a NullReferenceException on startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:45:41 -04:00
ajandClaude Opus 4.6 0e05ad04ea refactor: clean up PlateView after component extraction
Remove dead programIdFont field, unused imports (OpenNest.CNC,
System.ComponentModel, OpenNest.Math, System.Collections.ObjectModel).
PlateView is now 692 lines (down from 1035).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:20:28 -04:00
ajandClaude Opus 4.6 5ac985dc0f refactor: update PlateRenderer for SelectionManager cut-off list
PlateRenderer now checks Selection.SelectedCutOffs.Contains() instead
of comparing against a single SelectedCutOff property. Remove temporary
SelectedCutOff shim from PlateView and unused Designer assignment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:18:59 -04:00
ajandClaude Sonnet 4.6 865754611c refactor: extract PreviewManager from PlateView
Moves preview part lifecycle (stationaryParts, activeParts) into a dedicated
PreviewManager class. PlateView retains forwarding properties and methods for
backward compatibility. Adds Previews property for direct access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 15:18:17 -04:00
ajandClaude Sonnet 4.6 9db326ee5d refactor: extract ActionManager from PlateView
Move action lifecycle (currentAction, previousAction, SetAction, ProcessEscapeKey,
RestorePreviousAction, GetDisplayName) into a dedicated ActionManager class.
PlateView retains public forwarding methods and exposes Actions property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 15:16:15 -04:00
ajandClaude Sonnet 4.6 25faba430c refactor: extract CutOffHandler from PlateView
Move cut-off drag interaction mechanics into a dedicated CutOffHandler
class, reducing PlateView complexity and following the same pattern
established by SelectionManager extraction in Task 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 15:13:42 -04:00
ajandClaude Sonnet 4.6 089df67627 refactor: extract SelectionManager from PlateView
Move all selection state and operations (SelectedParts, SelectedCutOffs, DeselectAll, SelectAll, AlignSelected, RotateSelectedParts, PushSelected, GetPartAt*, GetPartsFromWindow, DeleteSelected) into a new internal SelectionManager class. PlateView retains public forwarding methods and properties to preserve the existing API surface. SelectedCutOff property kept public for WinForms designer compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 15:09:08 -04:00
ajandClaude Opus 4.6 11884e712d fix: clear empty area below items in drawing list on resize
The WM_ERASEBKGND suppression from 3c4d00b left stale artifacts
in the non-item region when the control was resized. Fill only
the area below the last visible item so items still don't flicker.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:23:30 -04:00
ajandClaude Opus 4.6 6bed736cf0 perf: use actual geometry instead of tessellated polygons for push distance
- Add entity-based DirectionalDistance overload to SpatialQuery that
  uses RayArcDistance/RayCircleDistance instead of tessellating arcs
  and circles into line segments
- Add GetOffsetPartEntities, GetPerimeterEntities, GetPartEntities to
  PartGeometry for non-tessellated entity extraction
- Update Compactor.Push to use native entities instead of tessellated
  lines — 952 circles = 952 entities vs ~47,600 line segments
- Use bounding box containment check to skip cutout entities when no
  obstacle is inside the moving part (perimeter-only for common case)
- Obstacles always use perimeter-only entities since cutout edges are
  inside the solid and cannot block external parts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:06:37 -04:00
ajandClaude Opus 4.6 c20a079874 refactor: clean up MultiPlateNester code smells and duplication
Extract shared patterns into reusable helpers: FitsBounds (fits-normal/
rotated check), OptionWorkArea (edge-spacing subtraction), DecrementQuantity,
TryWithUpgradedSize (upgrade-try-revert), FindSmallestFittingOption.
Add PlateResult.AddParts to consolidate dual parts-list bookkeeping.
Cache sorted plate options and add HasPlateOptions property. Introduce
MultiPlateNestOptions to replace 10-parameter Nest signature with a
clean options object. Fix fragile Drawing.Name matching with reference
equality in PackIntoExistingRemnants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:43:58 -04:00
ajandClaude Opus 4.6 804a7fd9c1 fix: check longest side against plate dimensions in best fit filter
The filter only checked ShortestSide against the plate's short dimension,
allowing results where the long side far exceeded the plate length.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:28:59 -04:00
19 changed files with 1293 additions and 641 deletions
+132
View File
@@ -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;
+85
View File
@@ -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)
{
+2 -1
View File
@@ -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";
+26 -12
View File
@@ -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
View File
@@ -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;
}
}
}
}
+16
View File
@@ -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; }
}
+2 -2
View File
@@ -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)
+24 -50
View File
@@ -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 ===");
+118
View File
@@ -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;
}
}
}
+81
View File
@@ -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;
}
}
}
+14
View File
@@ -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;
}
+1 -1
View File
@@ -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
View File
@@ -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();
}
}
}
+84
View File
@@ -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();
}
}
}
+216
View File
@@ -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();
}
}
}
+120
View File
@@ -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>
+11 -2
View File
@@ -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
View File
@@ -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";