Compare commits
59 Commits
ba88ac253a
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 786b6e2e88 | |||
| ba89967448 | |||
| b566d984b0 | |||
| c1e6092e83 | |||
| df86d4367b | |||
| 40026ab4dc | |||
| b18a82df7a | |||
| f090a2e299 | |||
| 55192a4888 | |||
| 7c28a35ad8 | |||
| b2a723ca60 | |||
| 3dca25c601 | |||
| ebc1a5f980 | |||
| b729f92cd6 | |||
| 5d6e018b81 | |||
| 5163b02f89 | |||
| a59911b38a | |||
| 810e37cacf | |||
| 8dfa45c446 | |||
| b223f69572 | |||
| 98c574c2ad | |||
| 30f1008fa9 | |||
| 41c20eaf75 | |||
| 3a97253473 | |||
| 3eab3c5946 | |||
| 0e05ad04ea | |||
| 5ac985dc0f | |||
| 865754611c | |||
| 9db326ee5d | |||
| 25faba430c | |||
| 089df67627 | |||
| 11884e712d | |||
| 6bed736cf0 | |||
| c20a079874 | |||
| 804a7fd9c1 | |||
| 3c4d00baa4 | |||
| 959ab15491 | |||
| cca70db547 | |||
| 62d9dce0b1 | |||
| 1f88453d4c | |||
| 0697bebbc2 | |||
| beadb14acc | |||
| 09f1140f54 | |||
| 7c918a2378 | |||
| feb08a5f60 | |||
| f1fd211ba5 | |||
| fd3c2462df | |||
| a4773748a1 | |||
| af57153269 | |||
| 35e89600d0 | |||
| 89a4e6b981 | |||
| ebad3577dd | |||
| a8dc275da4 | |||
| d84becdaee | |||
| 9cba3a6cd7 | |||
| e93523d7a2 | |||
| 3bdbf21881 | |||
| a8e42fb4b5 | |||
| ea3c6afbdd |
@@ -309,7 +309,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
if (shape.Entities.Count == 1 && shape.Entities[0] is Circle circle)
|
||||
return circle.Rotation;
|
||||
|
||||
return shape.ToPolygon().RotationDirection();
|
||||
var polygon = shape.ToPolygon();
|
||||
|
||||
if (polygon.Vertices.Count < 3)
|
||||
return RotationType.CCW;
|
||||
|
||||
return polygon.RotationDirection();
|
||||
}
|
||||
|
||||
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class MicrotabLeadOut : LeadOut
|
||||
{
|
||||
public double GapSize { get; set; } = 0.03;
|
||||
|
||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
return new List<ICode>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ namespace OpenNest.Converters
|
||||
if (startpt != lastpt)
|
||||
pgm.MoveTo(startpt);
|
||||
|
||||
pgm.ArcTo(startpt, circle.Center, RotationType.CCW);
|
||||
pgm.ArcTo(startpt, circle.Center, circle.Rotation);
|
||||
|
||||
lastpt = startpt;
|
||||
return lastpt;
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace OpenNest.Converters
|
||||
var layer = ConvertLayer(arcMove.Layer);
|
||||
|
||||
if (startAngle.IsEqualTo(endAngle))
|
||||
geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color });
|
||||
geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color, Rotation = arcMove.Rotation });
|
||||
else
|
||||
geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer, Color = layer.Color });
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
@@ -90,6 +91,18 @@ namespace OpenNest
|
||||
|
||||
public List<Bend> Bends { get; set; } = new List<Bend>();
|
||||
|
||||
/// <summary>
|
||||
/// Complete set of source entities with stable GUIDs.
|
||||
/// Null when the drawing was created from G-code or an older nest file.
|
||||
/// </summary>
|
||||
public List<Entity> SourceEntities { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// IDs of entities in <see cref="SourceEntities"/> that are suppressed (hidden).
|
||||
/// Suppressed entities are excluded from the active Program but preserved for re-enabling.
|
||||
/// </summary>
|
||||
public HashSet<Guid> SuppressedEntityIds { get; set; } = new HashSet<Guid>();
|
||||
|
||||
public double Area { get; protected set; }
|
||||
|
||||
public void UpdateArea()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
@@ -10,10 +11,16 @@ namespace OpenNest.Geometry
|
||||
|
||||
protected Entity()
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
Layer = OpenNest.Geometry.Layer.Default;
|
||||
boundingBox = new Box();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier for this entity, stable across edit sessions.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Entity color (resolved from DXF ByLayer/ByBlock to actual color).
|
||||
/// </summary>
|
||||
|
||||
@@ -605,7 +605,7 @@ namespace OpenNest.Geometry
|
||||
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
||||
break;
|
||||
case Circle c:
|
||||
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer });
|
||||
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CW });
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -640,7 +640,7 @@ namespace OpenNest.Geometry
|
||||
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
||||
break;
|
||||
case Circle c:
|
||||
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer });
|
||||
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CCW });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,98 @@ namespace OpenNest.Geometry
|
||||
return double.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Solves ray-circle intersection, returning the two parametric t values.
|
||||
/// Returns false if no real intersection exists.
|
||||
/// </summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(
|
||||
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
private static bool SolveRayCircle(
|
||||
double vx, double vy,
|
||||
double cx, double cy, double r,
|
||||
double dirX, double dirY,
|
||||
out double t1, out double t2)
|
||||
{
|
||||
var ox = vx - cx;
|
||||
var oy = vy - cy;
|
||||
|
||||
var a = dirX * dirX + dirY * dirY;
|
||||
var b = 2.0 * (ox * dirX + oy * dirY);
|
||||
var c = ox * ox + oy * oy - r * r;
|
||||
|
||||
var discriminant = b * b - 4.0 * a * c;
|
||||
if (discriminant < 0)
|
||||
{
|
||||
t1 = t2 = double.MaxValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
var sqrtD = System.Math.Sqrt(discriminant);
|
||||
var inv2a = 1.0 / (2.0 * a);
|
||||
t1 = (-b - sqrtD) * inv2a;
|
||||
t2 = (-b + sqrtD) * inv2a;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the distance from a point along a direction to an arc.
|
||||
/// Solves ray-circle intersection, then constrains hits to the arc's
|
||||
/// angular span. Returns double.MaxValue if no hit.
|
||||
/// </summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(
|
||||
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static double RayArcDistance(
|
||||
double vx, double vy,
|
||||
double cx, double cy, double r,
|
||||
double startAngle, double endAngle, bool reversed,
|
||||
double dirX, double dirY)
|
||||
{
|
||||
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||
return double.MaxValue;
|
||||
|
||||
var best = double.MaxValue;
|
||||
|
||||
if (t1 > -Tolerance.Epsilon)
|
||||
{
|
||||
var hitAngle = Angle.NormalizeRad(System.Math.Atan2(
|
||||
vy + t1 * dirY - cy, vx + t1 * dirX - cx));
|
||||
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
|
||||
best = t1 > Tolerance.Epsilon ? t1 : 0;
|
||||
}
|
||||
|
||||
if (t2 > -Tolerance.Epsilon && t2 < best)
|
||||
{
|
||||
var hitAngle = Angle.NormalizeRad(System.Math.Atan2(
|
||||
vy + t2 * dirY - cy, vx + t2 * dirX - cx));
|
||||
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
|
||||
best = t2 > Tolerance.Epsilon ? t2 : 0;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the distance from a point along a direction to a full circle.
|
||||
/// Returns double.MaxValue if no hit.
|
||||
/// </summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(
|
||||
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static double RayCircleDistance(
|
||||
double vx, double vy,
|
||||
double cx, double cy, double r,
|
||||
double dirX, double dirY)
|
||||
{
|
||||
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||
return double.MaxValue;
|
||||
|
||||
if (t1 > Tolerance.Epsilon) return t1;
|
||||
if (t1 >= -Tolerance.Epsilon) return 0;
|
||||
if (t2 > Tolerance.Epsilon) return t2;
|
||||
if (t2 >= -Tolerance.Epsilon) return 0;
|
||||
|
||||
return double.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum translation distance along a push direction before
|
||||
/// any edge of movingLines contacts any edge of stationaryLines.
|
||||
@@ -111,57 +203,7 @@ namespace OpenNest.Geometry
|
||||
/// </summary>
|
||||
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
|
||||
{
|
||||
var minDist = double.MaxValue;
|
||||
|
||||
// Case 1: Each moving vertex -> each stationary edge
|
||||
var movingVertices = new HashSet<Vector>();
|
||||
for (int i = 0; i < movingLines.Count; i++)
|
||||
{
|
||||
movingVertices.Add(movingLines[i].pt1);
|
||||
movingVertices.Add(movingLines[i].pt2);
|
||||
}
|
||||
|
||||
var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count];
|
||||
for (int i = 0; i < stationaryLines.Count; i++)
|
||||
stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2);
|
||||
|
||||
// Sort edges for pruning if not already sorted (usually they aren't here)
|
||||
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
||||
else
|
||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
||||
|
||||
foreach (var mv in movingVertices)
|
||||
{
|
||||
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
|
||||
if (d < minDist) minDist = d;
|
||||
}
|
||||
|
||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||
var opposite = OppositeDirection(direction);
|
||||
var stationaryVertices = new HashSet<Vector>();
|
||||
for (int i = 0; i < stationaryLines.Count; i++)
|
||||
{
|
||||
stationaryVertices.Add(stationaryLines[i].pt1);
|
||||
stationaryVertices.Add(stationaryLines[i].pt2);
|
||||
}
|
||||
|
||||
var movingEdges = new (Vector start, Vector end)[movingLines.Count];
|
||||
for (int i = 0; i < movingLines.Count; i++)
|
||||
movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2);
|
||||
|
||||
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
|
||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
||||
else
|
||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
||||
|
||||
foreach (var sv in stationaryVertices)
|
||||
{
|
||||
var d = OneWayDistance(sv, movingEdges, Vector.Zero, opposite);
|
||||
if (d < minDist) minDist = d;
|
||||
}
|
||||
|
||||
return minDist;
|
||||
return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -176,21 +218,10 @@ namespace OpenNest.Geometry
|
||||
var movingOffset = new Vector(movingDx, movingDy);
|
||||
|
||||
// Case 1: Each moving vertex -> each stationary edge
|
||||
var movingVertices = new HashSet<Vector>();
|
||||
for (int i = 0; i < movingLines.Count; i++)
|
||||
{
|
||||
movingVertices.Add(movingLines[i].pt1 + movingOffset);
|
||||
movingVertices.Add(movingLines[i].pt2 + movingOffset);
|
||||
}
|
||||
var movingVertices = CollectVertices(movingLines, movingOffset);
|
||||
|
||||
var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count];
|
||||
for (int i = 0; i < stationaryLines.Count; i++)
|
||||
stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2);
|
||||
|
||||
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
||||
else
|
||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
||||
var stationaryEdges = ToEdgeArray(stationaryLines);
|
||||
SortEdgesForPruning(stationaryEdges, direction);
|
||||
|
||||
foreach (var mv in movingVertices)
|
||||
{
|
||||
@@ -200,21 +231,10 @@ namespace OpenNest.Geometry
|
||||
|
||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||
var opposite = OppositeDirection(direction);
|
||||
var stationaryVertices = new HashSet<Vector>();
|
||||
for (int i = 0; i < stationaryLines.Count; i++)
|
||||
{
|
||||
stationaryVertices.Add(stationaryLines[i].pt1);
|
||||
stationaryVertices.Add(stationaryLines[i].pt2);
|
||||
}
|
||||
var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero);
|
||||
|
||||
var movingEdges = new (Vector start, Vector end)[movingLines.Count];
|
||||
for (int i = 0; i < movingLines.Count; i++)
|
||||
movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2);
|
||||
|
||||
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
|
||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
||||
else
|
||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
||||
var movingEdges = ToEdgeArray(movingLines);
|
||||
SortEdgesForPruning(movingEdges, opposite);
|
||||
|
||||
foreach (var sv in stationaryVertices)
|
||||
{
|
||||
@@ -253,15 +273,11 @@ namespace OpenNest.Geometry
|
||||
{
|
||||
var minDist = double.MaxValue;
|
||||
|
||||
// Extract unique vertices from moving edges.
|
||||
var movingVertices = new HashSet<Vector>();
|
||||
for (var i = 0; i < movingEdges.Length; i++)
|
||||
{
|
||||
movingVertices.Add(movingEdges[i].start + movingOffset);
|
||||
movingVertices.Add(movingEdges[i].end + movingOffset);
|
||||
}
|
||||
SortEdgesForPruning(stationaryEdges, direction);
|
||||
|
||||
// Case 1: Each moving vertex -> each stationary edge
|
||||
var movingVertices = CollectVertices(movingEdges, movingOffset);
|
||||
|
||||
foreach (var mv in movingVertices)
|
||||
{
|
||||
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
|
||||
@@ -270,12 +286,9 @@ namespace OpenNest.Geometry
|
||||
|
||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||
var opposite = OppositeDirection(direction);
|
||||
var stationaryVertices = new HashSet<Vector>();
|
||||
for (var i = 0; i < stationaryEdges.Length; i++)
|
||||
{
|
||||
stationaryVertices.Add(stationaryEdges[i].start + stationaryOffset);
|
||||
stationaryVertices.Add(stationaryEdges[i].end + stationaryOffset);
|
||||
}
|
||||
SortEdgesForPruning(movingEdges, opposite);
|
||||
|
||||
var stationaryVertices = CollectVertices(stationaryEdges, stationaryOffset);
|
||||
|
||||
foreach (var sv in stationaryVertices)
|
||||
{
|
||||
@@ -467,12 +480,7 @@ namespace OpenNest.Geometry
|
||||
var dirX = direction.X;
|
||||
var dirY = direction.Y;
|
||||
|
||||
var movingVertices = new HashSet<Vector>();
|
||||
for (var i = 0; i < movingLines.Count; i++)
|
||||
{
|
||||
movingVertices.Add(movingLines[i].pt1);
|
||||
movingVertices.Add(movingLines[i].pt2);
|
||||
}
|
||||
var movingVertices = CollectVertices(movingLines, Vector.Zero);
|
||||
|
||||
foreach (var mv in movingVertices)
|
||||
{
|
||||
@@ -487,12 +495,7 @@ namespace OpenNest.Geometry
|
||||
var oppX = -dirX;
|
||||
var oppY = -dirY;
|
||||
|
||||
var stationaryVertices = new HashSet<Vector>();
|
||||
for (var i = 0; i < stationaryLines.Count; i++)
|
||||
{
|
||||
stationaryVertices.Add(stationaryLines[i].pt1);
|
||||
stationaryVertices.Add(stationaryLines[i].pt2);
|
||||
}
|
||||
var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero);
|
||||
|
||||
foreach (var sv in stationaryVertices)
|
||||
{
|
||||
@@ -507,6 +510,284 @@ namespace OpenNest.Geometry
|
||||
return minDist;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum translation distance along a push direction
|
||||
/// before any vertex/edge of movingEntities contacts any vertex/edge of
|
||||
/// stationaryEntities. Delegates to the Vector-based overload.
|
||||
/// </summary>
|
||||
public static double DirectionalDistance(
|
||||
List<Entity> movingEntities, List<Entity> stationaryEntities, PushDirection direction)
|
||||
{
|
||||
return DirectionalDistance(movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum translation distance along an arbitrary unit direction
|
||||
/// before any vertex/edge of movingEntities contacts any vertex/edge of
|
||||
/// stationaryEntities. Works with native Line, Arc, and Circle entities
|
||||
/// without tessellation.
|
||||
/// </summary>
|
||||
public static double DirectionalDistance(
|
||||
List<Entity> movingEntities, List<Entity> stationaryEntities, Vector direction)
|
||||
{
|
||||
var minDist = double.MaxValue;
|
||||
var dirX = direction.X;
|
||||
var dirY = direction.Y;
|
||||
|
||||
var movingVertices = ExtractEntityVertices(movingEntities);
|
||||
|
||||
for (var v = 0; v < movingVertices.Length; v++)
|
||||
{
|
||||
var vx = movingVertices[v].X;
|
||||
var vy = movingVertices[v].Y;
|
||||
|
||||
for (var j = 0; j < stationaryEntities.Count; j++)
|
||||
{
|
||||
var d = RayEntityDistance(vx, vy, stationaryEntities[j], dirX, dirY);
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0) return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var oppX = -dirX;
|
||||
var oppY = -dirY;
|
||||
|
||||
var stationaryVertices = ExtractEntityVertices(stationaryEntities);
|
||||
|
||||
for (var v = 0; v < stationaryVertices.Length; v++)
|
||||
{
|
||||
var vx = stationaryVertices[v].X;
|
||||
var vy = stationaryVertices[v].Y;
|
||||
|
||||
for (var j = 0; j < movingEntities.Count; j++)
|
||||
{
|
||||
var d = RayEntityDistance(vx, vy, movingEntities[j], oppX, oppY);
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0) return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Arc-to-line closest-point check.
|
||||
// Phases 1-2 sample arc endpoints and cardinal extremes, but the actual
|
||||
// closest point on a small corner arc to a straight edge may lie between
|
||||
// those samples. Use ClosestPointTo to find it and fire a ray from there.
|
||||
minDist = ArcToLineClosestDistance(movingEntities, stationaryEntities, dirX, dirY, minDist);
|
||||
if (minDist <= 0) return 0;
|
||||
minDist = ArcToLineClosestDistance(stationaryEntities, movingEntities, oppX, oppY, minDist);
|
||||
if (minDist <= 0) return 0;
|
||||
|
||||
// Phase 4: Curve-to-curve direct distance.
|
||||
// The vertex-to-entity approach misses the closest contact between two
|
||||
// curved entities (circles/arcs) because only a few cardinal vertices are
|
||||
// sampled. The true closest contact along the push direction is found by
|
||||
// treating it as a ray from one center to an expanded circle at the other
|
||||
// center (radius = r1 + r2).
|
||||
for (var i = 0; i < movingEntities.Count; i++)
|
||||
{
|
||||
var me = movingEntities[i];
|
||||
if (!TryGetCurveParams(me, out var mcx, out var mcy, out var mr))
|
||||
continue;
|
||||
|
||||
for (var j = 0; j < stationaryEntities.Count; j++)
|
||||
{
|
||||
var se = stationaryEntities[j];
|
||||
if (!TryGetCurveParams(se, out var scx, out var scy, out var sr))
|
||||
continue;
|
||||
|
||||
var d = RayCircleDistance(mcx, mcy, scx, scy, mr + sr, dirX, dirY);
|
||||
|
||||
if (d >= minDist)
|
||||
continue;
|
||||
|
||||
// For arcs, verify the contact point falls within both arcs' angular ranges.
|
||||
if (me is Arc || se is Arc)
|
||||
{
|
||||
var mx = mcx + d * dirX;
|
||||
var my = mcy + d * dirY;
|
||||
var toCx = scx - mx;
|
||||
var toCy = scy - my;
|
||||
|
||||
if (me is Arc mArc)
|
||||
{
|
||||
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
||||
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (se is Arc sArc)
|
||||
{
|
||||
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
|
||||
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
minDist = d;
|
||||
if (d <= 0) return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return minDist;
|
||||
}
|
||||
|
||||
private static double ArcToLineClosestDistance(
|
||||
List<Entity> arcEntities, List<Entity> lineEntities,
|
||||
double dirX, double dirY, double minDist)
|
||||
{
|
||||
for (var i = 0; i < arcEntities.Count; i++)
|
||||
{
|
||||
if (arcEntities[i] is Arc arc)
|
||||
{
|
||||
for (var j = 0; j < lineEntities.Count; j++)
|
||||
{
|
||||
if (lineEntities[j] is Line line)
|
||||
{
|
||||
var linePt = line.ClosestPointTo(arc.Center);
|
||||
var arcPt = arc.ClosestPointTo(linePt);
|
||||
var d = RayEdgeDistance(arcPt.X, arcPt.Y,
|
||||
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
|
||||
dirX, dirY);
|
||||
if (d < minDist) { minDist = d; if (d <= 0) return 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return minDist;
|
||||
}
|
||||
|
||||
private static double RayEntityDistance(
|
||||
double vx, double vy, Entity entity, double dirX, double dirY)
|
||||
{
|
||||
if (entity is Line line)
|
||||
{
|
||||
return RayEdgeDistance(vx, vy,
|
||||
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
|
||||
dirX, dirY);
|
||||
}
|
||||
|
||||
if (entity is Arc arc)
|
||||
{
|
||||
return RayArcDistance(vx, vy,
|
||||
arc.Center.X, arc.Center.Y, arc.Radius,
|
||||
arc.StartAngle, arc.EndAngle, arc.IsReversed,
|
||||
dirX, dirY);
|
||||
}
|
||||
|
||||
if (entity is Circle circle)
|
||||
{
|
||||
return RayCircleDistance(vx, vy,
|
||||
circle.Center.X, circle.Center.Y, circle.Radius,
|
||||
dirX, dirY);
|
||||
}
|
||||
|
||||
return double.MaxValue;
|
||||
}
|
||||
|
||||
private static Vector[] ExtractEntityVertices(List<Entity> entities)
|
||||
{
|
||||
var vertices = new HashSet<Vector>();
|
||||
|
||||
for (var i = 0; i < entities.Count; i++)
|
||||
{
|
||||
var entity = entities[i];
|
||||
|
||||
if (entity is Line line)
|
||||
{
|
||||
vertices.Add(line.pt1);
|
||||
vertices.Add(line.pt2);
|
||||
}
|
||||
else if (entity is Arc arc)
|
||||
{
|
||||
vertices.Add(arc.StartPoint());
|
||||
vertices.Add(arc.EndPoint());
|
||||
AddArcExtremeVertices(vertices, arc);
|
||||
}
|
||||
else if (entity is Circle circle)
|
||||
{
|
||||
vertices.Add(new Vector(circle.Center.X + circle.Radius, circle.Center.Y));
|
||||
vertices.Add(new Vector(circle.Center.X - circle.Radius, circle.Center.Y));
|
||||
vertices.Add(new Vector(circle.Center.X, circle.Center.Y + circle.Radius));
|
||||
vertices.Add(new Vector(circle.Center.X, circle.Center.Y - circle.Radius));
|
||||
}
|
||||
}
|
||||
|
||||
return vertices.ToArray();
|
||||
}
|
||||
|
||||
private static void AddArcExtremeVertices(HashSet<Vector> points, Arc arc)
|
||||
{
|
||||
var a1 = arc.StartAngle;
|
||||
var a2 = arc.EndAngle;
|
||||
|
||||
if (arc.IsReversed)
|
||||
Generic.Swap(ref a1, ref a2);
|
||||
|
||||
if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2))
|
||||
points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y));
|
||||
if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2))
|
||||
points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius));
|
||||
if (Angle.IsBetweenRad(System.Math.PI, a1, a2))
|
||||
points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y));
|
||||
if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2))
|
||||
points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
|
||||
}
|
||||
|
||||
private static HashSet<Vector> CollectVertices(List<Line> lines, Vector offset)
|
||||
{
|
||||
return CollectVertices(ToEdgeArray(lines), offset);
|
||||
}
|
||||
|
||||
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
|
||||
{
|
||||
var vertices = new HashSet<Vector>();
|
||||
for (var i = 0; i < edges.Length; i++)
|
||||
{
|
||||
vertices.Add(edges[i].start + offset);
|
||||
vertices.Add(edges[i].end + offset);
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
private static (Vector start, Vector end)[] ToEdgeArray(List<Line> lines)
|
||||
{
|
||||
var edges = new (Vector start, Vector end)[lines.Count];
|
||||
for (var i = 0; i < lines.Count; i++)
|
||||
edges[i] = (lines[i].pt1, lines[i].pt2);
|
||||
return edges;
|
||||
}
|
||||
|
||||
private static void SortEdgesForPruning((Vector start, Vector end)[] edges, PushDirection direction)
|
||||
{
|
||||
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||
System.Array.Sort(edges, (a, b) =>
|
||||
System.Math.Min(a.start.Y, a.end.Y).CompareTo(System.Math.Min(b.start.Y, b.end.Y)));
|
||||
else
|
||||
System.Array.Sort(edges, (a, b) =>
|
||||
System.Math.Min(a.start.X, a.end.X).CompareTo(System.Math.Min(b.start.X, b.end.X)));
|
||||
}
|
||||
|
||||
private static bool TryGetCurveParams(Entity entity, out double cx, out double cy, out double r)
|
||||
{
|
||||
if (entity is Circle circle)
|
||||
{
|
||||
cx = circle.Center.X; cy = circle.Center.Y; r = circle.Radius;
|
||||
return true;
|
||||
}
|
||||
if (entity is Arc arc)
|
||||
{
|
||||
cx = arc.Center.X; cy = arc.Center.Y; r = arc.Radius;
|
||||
return true;
|
||||
}
|
||||
cx = cy = r = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static double BoxProjectionMin(Box box, double dx, double dy)
|
||||
{
|
||||
var x = dx >= 0 ? box.Left : box.Right;
|
||||
|
||||
@@ -190,7 +190,14 @@ namespace OpenNest
|
||||
{
|
||||
var rotation = Rotation;
|
||||
Program = BaseDrawing.Program.Clone() as Program;
|
||||
Program.Rotate(Program.Rotation - rotation);
|
||||
|
||||
if (!Math.Tolerance.IsEqualTo(rotation, 0))
|
||||
Program.Rotate(rotation);
|
||||
|
||||
HasManualLeadIns = false;
|
||||
LeadInsLocked = false;
|
||||
CuttingParameters = null;
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -39,7 +39,115 @@ namespace OpenNest
|
||||
return lines;
|
||||
}
|
||||
|
||||
public static List<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001)
|
||||
/// <summary>
|
||||
/// Returns the perimeter entities (Line, Arc, Circle) with spacing offset applied,
|
||||
/// without tessellation. Much faster than GetOffsetPartLines for parts with many arcs.
|
||||
/// </summary>
|
||||
public static List<Entity> GetOffsetPerimeterEntities(Part part, double spacing)
|
||||
{
|
||||
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||
var profile = new ShapeProfile(
|
||||
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||
|
||||
var offsetShape = profile.Perimeter.OffsetOutward(spacing);
|
||||
if (offsetShape == null)
|
||||
return new List<Entity>();
|
||||
|
||||
// Offset the shape's entities to the part's location.
|
||||
// OffsetOutward creates a new Shape, so mutating is safe.
|
||||
foreach (var entity in offsetShape.Entities)
|
||||
entity.Offset(part.Location);
|
||||
|
||||
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)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||
var profile = new ShapeProfile(
|
||||
@@ -50,9 +158,12 @@ namespace OpenNest
|
||||
AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||
chordTolerance, part.Location);
|
||||
|
||||
if (!perimeterOnly)
|
||||
{
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
AddOffsetLines(lines, cutout.OffsetInward(totalSpacing),
|
||||
chordTolerance, part.Location);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace OpenNest.Shapes
|
||||
{
|
||||
public double Diameter { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
Diameter = 8;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
|
||||
@@ -11,6 +11,15 @@ namespace OpenNest.Shapes
|
||||
public double HolePatternDiameter { get; set; }
|
||||
public int HoleCount { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
NominalPipeSize = 2;
|
||||
OD = 7.5;
|
||||
HoleDiameter = 0.875;
|
||||
HolePatternDiameter = 5.5;
|
||||
HoleCount = 8;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var entities = new List<Entity>();
|
||||
|
||||
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
|
||||
public double Base { get; set; }
|
||||
public double Height { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
Base = 8;
|
||||
Height = 10;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var midX = Base / 2.0;
|
||||
|
||||
@@ -10,6 +10,14 @@ namespace OpenNest.Shapes
|
||||
public double LegWidth { get; set; }
|
||||
public double LegHeight { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
Width = 8;
|
||||
Height = 10;
|
||||
LegWidth = 3;
|
||||
LegHeight = 3;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var lw = LegWidth > 0 ? LegWidth : Width / 2.0;
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace OpenNest.Shapes
|
||||
{
|
||||
public double Width { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
Width = 8;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var center = Width / 2.0;
|
||||
|
||||
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
|
||||
public double Length { get; set; }
|
||||
public double Width { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
Length = 12;
|
||||
Width = 6;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
|
||||
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
Width = 8;
|
||||
Height = 6;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
|
||||
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
|
||||
public double OuterDiameter { get; set; }
|
||||
public double InnerDiameter { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
OuterDiameter = 10;
|
||||
InnerDiameter = 6;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
|
||||
@@ -10,6 +10,13 @@ namespace OpenNest.Shapes
|
||||
public double Width { get; set; }
|
||||
public double Radius { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
Length = 12;
|
||||
Width = 6;
|
||||
Radius = 1;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var r = Radius;
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace OpenNest.Shapes
|
||||
|
||||
public abstract Drawing GetDrawing();
|
||||
|
||||
public virtual void SetPreviewDefaults() { }
|
||||
|
||||
public static List<T> LoadFromJson<T>(string path) where T : ShapeDefinition
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
|
||||
@@ -10,6 +10,14 @@ namespace OpenNest.Shapes
|
||||
public double StemWidth { get; set; }
|
||||
public double BarHeight { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
Width = 10;
|
||||
Height = 8;
|
||||
StemWidth = 3;
|
||||
BarHeight = 3;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var sw = StemWidth > 0 ? StemWidth : Width / 3.0;
|
||||
|
||||
@@ -9,6 +9,13 @@ namespace OpenNest.Shapes
|
||||
public double BottomWidth { get; set; }
|
||||
public double Height { get; set; }
|
||||
|
||||
public override void SetPreviewDefaults()
|
||||
{
|
||||
TopWidth = 6;
|
||||
BottomWidth = 10;
|
||||
Height = 6;
|
||||
}
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var offset = (BottomWidth - TopWidth) / 2.0;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -4,6 +4,7 @@ using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -49,6 +50,8 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
var allCandidates = candidateBags.SelectMany(c => c).ToList();
|
||||
|
||||
Debug.WriteLine($"[BestFitFinder] {strategies.Count} strategies, {allCandidates.Count} candidates");
|
||||
|
||||
var results = _evaluator.EvaluateAll(allCandidates);
|
||||
|
||||
_filter.Apply(results);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -17,7 +18,6 @@ namespace OpenNest.Engine.BestFit
|
||||
var allMovingVerts = ExtractUniqueVertices(movingTemplateLines);
|
||||
var allStationaryVerts = ExtractUniqueVertices(stationaryLines);
|
||||
|
||||
// Pre-filter vertices per unique direction (typically 4 cardinal directions).
|
||||
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
||||
|
||||
foreach (var offset in offsets)
|
||||
@@ -43,7 +43,6 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
var minDist = double.MaxValue;
|
||||
|
||||
// Case 1: Leading moving vertices → stationary edges
|
||||
for (var v = 0; v < leadingMoving.Length; v++)
|
||||
{
|
||||
var vx = leadingMoving[v].X + offset.Dx;
|
||||
@@ -66,7 +65,6 @@ namespace OpenNest.Engine.BestFit
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Facing stationary vertices → moving edges (opposite direction)
|
||||
for (var v = 0; v < facingStationary.Length; v++)
|
||||
{
|
||||
var svx = facingStationary[v].X;
|
||||
@@ -95,6 +93,253 @@ namespace OpenNest.Engine.BestFit
|
||||
return results;
|
||||
}
|
||||
|
||||
public double[] ComputeDistances(
|
||||
List<Entity> stationaryEntities,
|
||||
List<Entity> movingEntities,
|
||||
SlideOffset[] offsets)
|
||||
{
|
||||
var count = offsets.Length;
|
||||
var results = new double[count];
|
||||
|
||||
var allMovingVerts = ExtractVerticesFromEntities(movingEntities);
|
||||
var allStationaryVerts = ExtractVerticesFromEntities(stationaryEntities);
|
||||
|
||||
var movingCurves = ExtractCurveParams(movingEntities);
|
||||
var stationaryCurves = ExtractCurveParams(stationaryEntities);
|
||||
|
||||
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
||||
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
var key = (offset.DirX, offset.DirY);
|
||||
if (vertexCache.ContainsKey(key))
|
||||
continue;
|
||||
|
||||
var leading = FilterVerticesByProjection(allMovingVerts, offset.DirX, offset.DirY, keepHigh: true);
|
||||
var facing = FilterVerticesByProjection(allStationaryVerts, offset.DirX, offset.DirY, keepHigh: false);
|
||||
vertexCache[key] = (leading, facing);
|
||||
}
|
||||
|
||||
System.Threading.Tasks.Parallel.For(0, count, i =>
|
||||
{
|
||||
var offset = offsets[i];
|
||||
var dirX = offset.DirX;
|
||||
var dirY = offset.DirY;
|
||||
var oppX = -dirX;
|
||||
var oppY = -dirY;
|
||||
|
||||
var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)];
|
||||
|
||||
var minDist = double.MaxValue;
|
||||
|
||||
// Case 1: Leading moving vertices → stationary entities
|
||||
for (var v = 0; v < leadingMoving.Length; v++)
|
||||
{
|
||||
var vx = leadingMoving[v].X + offset.Dx;
|
||||
var vy = leadingMoving[v].Y + offset.Dy;
|
||||
|
||||
for (var j = 0; j < stationaryEntities.Count; j++)
|
||||
{
|
||||
var d = RayEntityDistance(vx, vy, stationaryEntities[j], 0, 0, dirX, dirY);
|
||||
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Facing stationary vertices → moving entities (opposite direction)
|
||||
for (var v = 0; v < facingStationary.Length; v++)
|
||||
{
|
||||
var svx = facingStationary[v].X;
|
||||
var svy = facingStationary[v].Y;
|
||||
|
||||
for (var j = 0; j < movingEntities.Count; j++)
|
||||
{
|
||||
var d = RayEntityDistance(svx, svy, movingEntities[j], offset.Dx, offset.Dy, oppX, oppY);
|
||||
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Curve-to-curve direct distance.
|
||||
// Vertex sampling misses the true contact between two curved entities
|
||||
// when the approach angle doesn't align with a sampled vertex.
|
||||
for (var m = 0; m < movingCurves.Length; m++)
|
||||
{
|
||||
var mc = movingCurves[m];
|
||||
var mcx = mc.Cx + offset.Dx;
|
||||
var mcy = mc.Cy + offset.Dy;
|
||||
|
||||
for (var s = 0; s < stationaryCurves.Length; s++)
|
||||
{
|
||||
var sc = stationaryCurves[s];
|
||||
var d = SpatialQuery.RayCircleDistance(
|
||||
mcx, mcy, sc.Cx, sc.Cy, mc.Radius + sc.Radius, dirX, dirY);
|
||||
|
||||
if (d >= minDist || d == double.MaxValue)
|
||||
continue;
|
||||
|
||||
if (mc.Entity is Arc || sc.Entity is Arc)
|
||||
{
|
||||
var mx = mcx + d * dirX;
|
||||
var my = mcy + d * dirY;
|
||||
var toCx = sc.Cx - mx;
|
||||
var toCy = sc.Cy - my;
|
||||
|
||||
if (mc.Entity is Arc mArc)
|
||||
{
|
||||
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
||||
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sc.Entity is Arc sArc)
|
||||
{
|
||||
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
|
||||
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
}
|
||||
}
|
||||
|
||||
results[i] = minDist;
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private readonly struct CurveParams
|
||||
{
|
||||
public readonly Entity Entity;
|
||||
public readonly double Cx, Cy, Radius;
|
||||
|
||||
public CurveParams(Entity entity, double cx, double cy, double radius)
|
||||
{
|
||||
Entity = entity;
|
||||
Cx = cx;
|
||||
Cy = cy;
|
||||
Radius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
private static CurveParams[] ExtractCurveParams(List<Entity> entities)
|
||||
{
|
||||
var curves = new List<CurveParams>();
|
||||
for (var i = 0; i < entities.Count; i++)
|
||||
{
|
||||
if (entities[i] is Circle circle)
|
||||
curves.Add(new CurveParams(circle, circle.Center.X, circle.Center.Y, circle.Radius));
|
||||
else if (entities[i] is Arc arc)
|
||||
curves.Add(new CurveParams(arc, arc.Center.X, arc.Center.Y, arc.Radius));
|
||||
}
|
||||
return curves.ToArray();
|
||||
}
|
||||
|
||||
private static double RayEntityDistance(
|
||||
double vx, double vy, Entity entity,
|
||||
double entityOffsetX, double entityOffsetY,
|
||||
double dirX, double dirY)
|
||||
{
|
||||
if (entity is Line line)
|
||||
{
|
||||
return SpatialQuery.RayEdgeDistance(
|
||||
vx, vy,
|
||||
line.StartPoint.X + entityOffsetX, line.StartPoint.Y + entityOffsetY,
|
||||
line.EndPoint.X + entityOffsetX, line.EndPoint.Y + entityOffsetY,
|
||||
dirX, dirY);
|
||||
}
|
||||
|
||||
if (entity is Arc arc)
|
||||
{
|
||||
return SpatialQuery.RayArcDistance(
|
||||
vx, vy,
|
||||
arc.Center.X + entityOffsetX, arc.Center.Y + entityOffsetY,
|
||||
arc.Radius,
|
||||
arc.StartAngle, arc.EndAngle, arc.IsReversed,
|
||||
dirX, dirY);
|
||||
}
|
||||
|
||||
if (entity is Circle circle)
|
||||
{
|
||||
return SpatialQuery.RayCircleDistance(
|
||||
vx, vy,
|
||||
circle.Center.X + entityOffsetX, circle.Center.Y + entityOffsetY,
|
||||
circle.Radius,
|
||||
dirX, dirY);
|
||||
}
|
||||
|
||||
return double.MaxValue;
|
||||
}
|
||||
|
||||
private static Vector[] ExtractVerticesFromEntities(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.StartPoint);
|
||||
vertices.Add(line.EndPoint);
|
||||
}
|
||||
else if (entity is Arc arc)
|
||||
{
|
||||
vertices.Add(arc.StartPoint());
|
||||
vertices.Add(arc.EndPoint());
|
||||
AddArcExtremes(vertices, arc);
|
||||
}
|
||||
else if (entity is Circle circle)
|
||||
{
|
||||
// Four cardinal points
|
||||
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 AddArcExtremes(HashSet<Vector> points, Arc arc)
|
||||
{
|
||||
var a1 = arc.StartAngle;
|
||||
var a2 = arc.EndAngle;
|
||||
var reversed = arc.IsReversed;
|
||||
|
||||
if (reversed)
|
||||
Generic.Swap(ref a1, ref a2);
|
||||
|
||||
// Right (0°)
|
||||
if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2))
|
||||
points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y));
|
||||
|
||||
// Top (90°)
|
||||
if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2))
|
||||
points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius));
|
||||
|
||||
// Left (180°)
|
||||
if (Angle.IsBetweenRad(System.Math.PI, a1, a2))
|
||||
points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y));
|
||||
|
||||
// Bottom (270°)
|
||||
if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2))
|
||||
points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
|
||||
}
|
||||
|
||||
private static Vector[] ExtractUniqueVertices(List<Line> lines)
|
||||
{
|
||||
var vertices = new HashSet<Vector>();
|
||||
@@ -106,11 +351,6 @@ namespace OpenNest.Engine.BestFit
|
||||
return vertices.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters vertices by their projection onto the push direction.
|
||||
/// keepHigh=true returns the leading half (front face, closest to target).
|
||||
/// keepHigh=false returns the facing half (side facing the approaching part).
|
||||
/// </summary>
|
||||
private static Vector[] FilterVerticesByProjection(
|
||||
Vector[] vertices, double dirX, double dirY, bool keepHigh)
|
||||
{
|
||||
|
||||
@@ -36,6 +36,16 @@ namespace OpenNest.Engine.BestFit
|
||||
flatOffsets, count, directions);
|
||||
}
|
||||
|
||||
public double[] ComputeDistances(
|
||||
List<Entity> stationaryEntities,
|
||||
List<Entity> movingEntities,
|
||||
SlideOffset[] offsets)
|
||||
{
|
||||
// GPU path doesn't support native entities yet — fall back to CPU.
|
||||
var cpu = new CpuDistanceComputer();
|
||||
return cpu.ComputeDistances(stationaryEntities, movingEntities, offsets);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a unit direction vector to a PushDirection int for the GPU interface.
|
||||
/// Left=0, Down=1, Right=2, Up=3.
|
||||
|
||||
@@ -9,5 +9,10 @@ namespace OpenNest.Engine.BestFit
|
||||
List<Line> stationaryLines,
|
||||
List<Line> movingTemplateLines,
|
||||
SlideOffset[] offsets);
|
||||
|
||||
double[] ComputeDistances(
|
||||
List<Entity> stationaryEntities,
|
||||
List<Entity> movingEntities,
|
||||
SlideOffset[] offsets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ namespace OpenNest.Engine.BestFit
|
||||
var part2Template = Part.CreateAtOrigin(drawing, Part2Rotation);
|
||||
|
||||
var halfSpacing = spacing / 2;
|
||||
var part1Lines = PartGeometry.GetOffsetPartLines(part1, halfSpacing);
|
||||
var part2TemplateLines = PartGeometry.GetOffsetPartLines(part2Template, halfSpacing);
|
||||
var part1Entities = PartGeometry.GetOffsetPerimeterEntities(part1, halfSpacing);
|
||||
var part2Entities = PartGeometry.GetOffsetPerimeterEntities(part2Template, halfSpacing);
|
||||
|
||||
var bbox1 = part1.BoundingBox;
|
||||
var bbox2 = part2Template.BoundingBox;
|
||||
@@ -48,7 +48,7 @@ namespace OpenNest.Engine.BestFit
|
||||
return candidates;
|
||||
|
||||
var distances = _distanceComputer.ComputeDistances(
|
||||
part1Lines, part2TemplateLines, offsets);
|
||||
part1Entities, part2Entities, offsets);
|
||||
|
||||
var testNumber = 0;
|
||||
|
||||
@@ -90,15 +90,18 @@ namespace OpenNest.Engine.BestFit
|
||||
if (isHorizontalPush)
|
||||
{
|
||||
// Perpendicular sweep along Y → Width; push extent along X → Length
|
||||
perpMin = -(bbox2.Width + spacing);
|
||||
perpMax = bbox1.Width + bbox2.Width + spacing;
|
||||
// Trim to offsets where the parts overlap by at least 50%.
|
||||
var halfOverlap = bbox2.Width * 0.5;
|
||||
perpMin = -(halfOverlap - spacing);
|
||||
perpMax = bbox1.Width + halfOverlap + spacing;
|
||||
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Perpendicular sweep along X → Length; push extent along Y → Width
|
||||
perpMin = -(bbox2.Length + spacing);
|
||||
perpMax = bbox1.Length + bbox2.Length + spacing;
|
||||
var halfOverlap = bbox2.Length * 0.5;
|
||||
perpMin = -(halfOverlap - spacing);
|
||||
perpMax = bbox1.Length + halfOverlap + spacing;
|
||||
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -139,24 +139,42 @@ namespace OpenNest
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||
|
||||
var best = SelectBestFitPair(bestFits);
|
||||
if (best == null)
|
||||
return null;
|
||||
List<Part> bestPlacement = null;
|
||||
|
||||
// BuildParts produces landscape orientation (Width >= Height).
|
||||
// Try both landscape and portrait (90° rotated) and let the
|
||||
// engine's comparer pick the better orientation.
|
||||
var landscape = best.BuildParts(drawing);
|
||||
foreach (var fit in bestFits)
|
||||
{
|
||||
if (!fit.Keep)
|
||||
continue;
|
||||
|
||||
// Skip pairs that can't possibly fit the work area in either orientation.
|
||||
if (fit.ShortestSide > System.Math.Min(workArea.Width, workArea.Length) + Tolerance.Epsilon)
|
||||
continue;
|
||||
if (fit.LongestSide > System.Math.Max(workArea.Width, workArea.Length) + Tolerance.Epsilon)
|
||||
continue;
|
||||
|
||||
var landscape = fit.BuildParts(drawing);
|
||||
var portrait = RotatePair90(landscape);
|
||||
|
||||
var lFits = TryOffsetToWorkArea(landscape, workArea);
|
||||
var pFits = TryOffsetToWorkArea(portrait, workArea);
|
||||
|
||||
if (!lFits && !pFits)
|
||||
return null;
|
||||
// Pick the better orientation for this pair.
|
||||
List<Part> candidate = null;
|
||||
if (lFits && pFits)
|
||||
return IsBetterFill(portrait, landscape, workArea) ? portrait : landscape;
|
||||
return lFits ? landscape : portrait;
|
||||
candidate = IsBetterFill(portrait, landscape, workArea) ? portrait : landscape;
|
||||
else if (lFits)
|
||||
candidate = landscape;
|
||||
else if (pFits)
|
||||
candidate = portrait;
|
||||
|
||||
if (candidate == null)
|
||||
continue;
|
||||
|
||||
if (bestPlacement == null || IsBetterFill(candidate, bestPlacement, workArea))
|
||||
bestPlacement = candidate;
|
||||
}
|
||||
|
||||
return bestPlacement;
|
||||
}
|
||||
|
||||
private static List<Part> RotatePair90(List<Part> parts)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -119,10 +119,11 @@ namespace OpenNest.Engine.Fill
|
||||
var maxCopyDistance = FindMaxPairDistance(
|
||||
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
|
||||
|
||||
if (maxCopyDistance < Tolerance.Epsilon)
|
||||
return bboxDim + PartSpacing;
|
||||
|
||||
return maxCopyDistance;
|
||||
// The copy distance must be at least bboxDim + PartSpacing to prevent
|
||||
// bounding box overlap. Cross-pair slides can underestimate when the
|
||||
// circumscribed polygon boundary overshoots the true arc, creating
|
||||
// spurious contacts between diagonal parts in adjacent copies.
|
||||
return System.Math.Max(maxCopyDistance, bboxDim + PartSpacing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
@@ -27,20 +26,6 @@ namespace OpenNest
|
||||
|
||||
public override ShrinkAxis TrimAxis => ShrinkAxis.Length;
|
||||
|
||||
protected override BestFitResult SelectBestFitPair(List<BestFitResult> results)
|
||||
{
|
||||
BestFitResult best = null;
|
||||
|
||||
foreach (var r in results)
|
||||
{
|
||||
if (!r.Keep) continue;
|
||||
if (best == null || r.BoundingHeight < best.BoundingHeight)
|
||||
best = r;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||
{
|
||||
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
||||
|
||||
@@ -0,0 +1,661 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
public enum PartClass
|
||||
{
|
||||
Large,
|
||||
Medium,
|
||||
Small,
|
||||
}
|
||||
|
||||
public class MultiPlateNester
|
||||
{
|
||||
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(
|
||||
MultiPlateNestOptions options,
|
||||
List<Plate> existingPlates,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
{
|
||||
_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;
|
||||
}
|
||||
|
||||
// --- 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 withBounds
|
||||
.OrderByDescending(x => x.Bounds.Width * x.Bounds.Length)
|
||||
.Select(x => x.Item)
|
||||
.ToList();
|
||||
|
||||
case PartSortOrder.Size:
|
||||
return withBounds
|
||||
.OrderByDescending(x => System.Math.Max(x.Bounds.Width, x.Bounds.Length))
|
||||
.Select(x => x.Item)
|
||||
.ToList();
|
||||
|
||||
default:
|
||||
return items.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public static PartClass Classify(Box partBounds, Box workArea)
|
||||
{
|
||||
var halfWidth = workArea.Width / 2.0;
|
||||
var halfLength = workArea.Length / 2.0;
|
||||
|
||||
if (partBounds.Width > halfWidth || partBounds.Length > halfLength)
|
||||
return PartClass.Large;
|
||||
|
||||
var workAreaArea = workArea.Width * workArea.Length;
|
||||
var partArea = partBounds.Width * partBounds.Length;
|
||||
|
||||
if (partArea > workAreaArea / 9.0)
|
||||
return PartClass.Medium;
|
||||
|
||||
return PartClass.Small;
|
||||
}
|
||||
|
||||
public static bool IsScrapRemnant(Box remnant, double minRemnantSize)
|
||||
{
|
||||
return remnant.Width < minRemnantSize && remnant.Length < minRemnantSize;
|
||||
}
|
||||
|
||||
public static List<Box> FindRemnants(Plate plate, double minRemnantSize, bool scrapOnly)
|
||||
{
|
||||
var remnants = RemnantFinder.FromPlate(plate).FindRemnants();
|
||||
return remnants.Where(r => IsScrapRemnant(r, minRemnantSize) == scrapOnly).ToList();
|
||||
}
|
||||
|
||||
public struct UpgradeDecision
|
||||
{
|
||||
public bool ShouldUpgrade;
|
||||
public double UpgradeCost;
|
||||
public double NewPlateCost;
|
||||
}
|
||||
|
||||
public static Plate CreatePlate(Plate template, List<PlateOption> options, Box minBounds)
|
||||
{
|
||||
var plate = new Plate(template.Size)
|
||||
{
|
||||
PartSpacing = template.PartSpacing,
|
||||
Quadrant = template.Quadrant,
|
||||
};
|
||||
plate.EdgeSpacing = new Spacing
|
||||
{
|
||||
Left = template.EdgeSpacing.Left,
|
||||
Right = template.EdgeSpacing.Right,
|
||||
Top = template.EdgeSpacing.Top,
|
||||
Bottom = template.EdgeSpacing.Bottom,
|
||||
};
|
||||
|
||||
if (options == null || options.Count == 0 || minBounds == null)
|
||||
return plate;
|
||||
|
||||
var sorted = options.OrderBy(o => o.Cost).ToList();
|
||||
|
||||
foreach (var option in sorted)
|
||||
{
|
||||
if (FitsBounds(OptionWorkArea(option, template), minBounds))
|
||||
{
|
||||
plate.Size = new Size(option.Width, option.Length);
|
||||
return plate;
|
||||
}
|
||||
}
|
||||
|
||||
return plate;
|
||||
}
|
||||
|
||||
public static UpgradeDecision EvaluateUpgradeVsNew(
|
||||
PlateOption currentSize,
|
||||
PlateOption upgradeSize,
|
||||
PlateOption newPlateSize,
|
||||
double salvageRate,
|
||||
double estimatedNewPlateUtilization)
|
||||
{
|
||||
var upgradeCost = upgradeSize.Cost - currentSize.Cost;
|
||||
|
||||
var newPlateCost = newPlateSize.Cost;
|
||||
var remnantFraction = 1.0 - estimatedNewPlateUtilization;
|
||||
var salvageCredit = remnantFraction * newPlateSize.Cost * salvageRate;
|
||||
var netNewCost = newPlateCost - salvageCredit;
|
||||
|
||||
return new UpgradeDecision
|
||||
{
|
||||
ShouldUpgrade = upgradeCost <= netNewCost,
|
||||
UpgradeCost = upgradeCost,
|
||||
NewPlateCost = netNewCost,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Main Entry Point ---
|
||||
|
||||
public static MultiPlateResult Nest(
|
||||
List<NestItem> items,
|
||||
MultiPlateNestOptions options,
|
||||
List<Plate> existingPlates = null,
|
||||
IProgress<NestProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (!FitsBounds(zone, partBounds))
|
||||
return -1;
|
||||
|
||||
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)
|
||||
{
|
||||
var engine = NestEngineRegistry.Create(pr.Plate);
|
||||
var clonedItem = CloneItem(item);
|
||||
var parts = engine.Fill(clonedItem, zone, _progress, _token);
|
||||
|
||||
if (parts.Count > 0)
|
||||
{
|
||||
pr.AddParts(parts);
|
||||
DecrementQuantity(item, parts.Count);
|
||||
}
|
||||
|
||||
return parts.Count;
|
||||
}
|
||||
|
||||
private PlateResult CreateNewPlateResult(Plate plate)
|
||||
{
|
||||
var pr = new PlateResult { Plate = plate, IsNew = true };
|
||||
|
||||
if (HasPlateOptions)
|
||||
{
|
||||
pr.ChosenSize = _plateOptions.FirstOrDefault(o =>
|
||||
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length));
|
||||
}
|
||||
|
||||
return pr;
|
||||
}
|
||||
|
||||
private static NestItem CloneItem(NestItem item)
|
||||
{
|
||||
return new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Priority = item.Priority,
|
||||
Quantity = item.Quantity,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd,
|
||||
};
|
||||
}
|
||||
|
||||
private static List<PlateResult> InitializePlatePool(List<Plate> existingPlates)
|
||||
{
|
||||
var pool = new List<PlateResult>();
|
||||
|
||||
if (existingPlates != null)
|
||||
{
|
||||
foreach (var plate in existingPlates)
|
||||
pool.Add(new PlateResult { Plate = plate, IsNew = false });
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var result = new MultiPlateResult();
|
||||
|
||||
if (items == null || items.Count == 0)
|
||||
return result;
|
||||
|
||||
var sorted = SortItems(items.Where(i => i.Quantity > 0).ToList(), sortOrder);
|
||||
|
||||
foreach (var item in sorted)
|
||||
{
|
||||
if (_token.IsCancellationRequested || item.Quantity <= 0)
|
||||
continue;
|
||||
|
||||
var bb = item.Drawing.Program.BoundingBox();
|
||||
|
||||
TryPlaceOnExistingPlates(item, bb);
|
||||
|
||||
var templateClass = Classify(bb, _template.WorkArea());
|
||||
|
||||
if (item.Quantity > 0 && allowPlateCreation && templateClass != PartClass.Small)
|
||||
{
|
||||
PlaceOnNewPlates(item, bb);
|
||||
|
||||
if (item.Quantity > 0 && HasPlateOptions)
|
||||
TryUpgradeOrNewPlate(item, bb);
|
||||
}
|
||||
}
|
||||
|
||||
var leftovers = sorted.Where(i => i.Quantity > 0).ToList();
|
||||
|
||||
if (leftovers.Count > 0 && allowPlateCreation && !_token.IsCancellationRequested)
|
||||
{
|
||||
PackIntoExistingRemnants(leftovers);
|
||||
CreateSharedPlates(leftovers);
|
||||
}
|
||||
|
||||
if (HasPlateOptions && !_token.IsCancellationRequested)
|
||||
TryConsolidateTailPlates();
|
||||
|
||||
foreach (var item in sorted.Where(i => i.Quantity > 0))
|
||||
result.UnplacedItems.Add(item);
|
||||
|
||||
result.Plates.AddRange(_platePool.Where(p => p.Parts.Count > 0 || p.IsNew));
|
||||
return result;
|
||||
}
|
||||
|
||||
private void PackIntoExistingRemnants(List<NestItem> leftovers)
|
||||
{
|
||||
foreach (var pr in _platePool)
|
||||
{
|
||||
if (_token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
var anyPlaced = true;
|
||||
while (anyPlaced && !_token.IsCancellationRequested)
|
||||
{
|
||||
anyPlaced = false;
|
||||
|
||||
var remaining = leftovers.Where(i => i.Quantity > 0).ToList();
|
||||
if (remaining.Count == 0)
|
||||
break;
|
||||
|
||||
var remnants = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
|
||||
if (remnants.Count == 0)
|
||||
break;
|
||||
|
||||
var engine = NestEngineRegistry.Create(pr.Plate);
|
||||
|
||||
foreach (var remnant in remnants)
|
||||
{
|
||||
remaining = leftovers.Where(i => i.Quantity > 0).ToList();
|
||||
if (remaining.Count == 0)
|
||||
break;
|
||||
|
||||
var cloned = remaining.Select(CloneItem).ToList();
|
||||
var parts = engine.PackArea(remnant, cloned, _progress, _token);
|
||||
|
||||
if (parts.Count > 0)
|
||||
{
|
||||
pr.AddParts(parts);
|
||||
anyPlaced = true;
|
||||
|
||||
foreach (var item in remaining)
|
||||
{
|
||||
var placed = parts.Count(p => p.BaseDrawing == item.Drawing);
|
||||
DecrementQuantity(item, placed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateSharedPlates(List<NestItem> leftovers)
|
||||
{
|
||||
leftovers.RemoveAll(i => i.Quantity <= 0);
|
||||
|
||||
while (leftovers.Count > 0 && !_token.IsCancellationRequested)
|
||||
{
|
||||
var plate = CreatePlate(_template, _plateOptions, null);
|
||||
var pr = CreateNewPlateResult(plate);
|
||||
var placedAny = false;
|
||||
|
||||
foreach (var item in leftovers)
|
||||
{
|
||||
if (item.Quantity <= 0 || _token.IsCancellationRequested)
|
||||
continue;
|
||||
|
||||
var remnants = !placedAny
|
||||
? new List<Box> { plate.WorkArea() }
|
||||
: RemnantFinder.FromPlate(plate).FindRemnants();
|
||||
|
||||
if (remnants.Count == 0)
|
||||
break;
|
||||
|
||||
var engine = NestEngineRegistry.Create(plate);
|
||||
|
||||
foreach (var remnant in remnants)
|
||||
{
|
||||
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;
|
||||
|
||||
_platePool.Add(pr);
|
||||
leftovers.RemoveAll(i => i.Quantity <= 0);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
PlateResult bestPlate = null;
|
||||
Box bestZone = null;
|
||||
var bestScore = double.MinValue;
|
||||
|
||||
foreach (var pr in _platePool)
|
||||
{
|
||||
if (_token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
if (pr == lastModified || !remnantCache.ContainsKey(pr))
|
||||
{
|
||||
var workArea = pr.Plate.WorkArea();
|
||||
var classification = Classify(partBounds, workArea);
|
||||
|
||||
remnantCache[pr] = classification == PartClass.Small
|
||||
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
|
||||
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
|
||||
}
|
||||
|
||||
foreach (var zone in remnantCache[pr])
|
||||
{
|
||||
var score = ScoreZone(zone, partBounds);
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestPlate = pr;
|
||||
bestZone = zone;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPlate == null || bestZone == null)
|
||||
break;
|
||||
|
||||
if (FillAndPlace(bestPlate, bestZone, item) == 0)
|
||||
break;
|
||||
|
||||
lastModified = bestPlate;
|
||||
anyPlaced = true;
|
||||
}
|
||||
|
||||
return anyPlaced;
|
||||
}
|
||||
|
||||
private bool PlaceOnNewPlates(NestItem item, Box partBounds)
|
||||
{
|
||||
var anyPlaced = false;
|
||||
|
||||
while (item.Quantity > 0 && !_token.IsCancellationRequested)
|
||||
{
|
||||
var plate = CreatePlate(_template, _plateOptions, partBounds);
|
||||
var workArea = plate.WorkArea();
|
||||
|
||||
if (!FitsBounds(workArea, partBounds))
|
||||
break;
|
||||
|
||||
var pr = CreateNewPlateResult(plate);
|
||||
|
||||
if (FillAndPlace(pr, workArea, item) == 0)
|
||||
break;
|
||||
|
||||
_platePool.Add(pr);
|
||||
anyPlaced = true;
|
||||
}
|
||||
|
||||
return anyPlaced;
|
||||
}
|
||||
|
||||
private bool TryUpgradeOrNewPlate(NestItem item, Box partBounds)
|
||||
{
|
||||
if (!HasPlateOptions)
|
||||
return false;
|
||||
|
||||
foreach (var pr in _platePool.Where(p => p.IsNew && p.ChosenSize != null))
|
||||
{
|
||||
var currentOption = pr.ChosenSize;
|
||||
var currentIdx = _sortedOptions.FindIndex(o =>
|
||||
o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length));
|
||||
|
||||
if (currentIdx < 0 || currentIdx >= _sortedOptions.Count - 1)
|
||||
continue;
|
||||
|
||||
for (var i = currentIdx + 1; i < _sortedOptions.Count; i++)
|
||||
{
|
||||
var upgradeOption = _sortedOptions[i];
|
||||
|
||||
if (upgradeOption.Width < currentOption.Width - Tolerance.Epsilon
|
||||
|| upgradeOption.Length < currentOption.Length - Tolerance.Epsilon)
|
||||
continue;
|
||||
|
||||
var smallestNew = FindSmallestFittingOption(partBounds);
|
||||
|
||||
if (smallestNew == null)
|
||||
continue;
|
||||
|
||||
var utilEst = pr.Plate.Utilization();
|
||||
var decision = EvaluateUpgradeVsNew(currentOption, upgradeOption, smallestNew,
|
||||
_salvageRate, utilEst);
|
||||
|
||||
if (decision.ShouldUpgrade)
|
||||
{
|
||||
var placed = TryWithUpgradedSize(pr, upgradeOption, remnants =>
|
||||
{
|
||||
foreach (var remnant in remnants)
|
||||
{
|
||||
if (FillAndPlace(pr, remnant, item) > 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (placed)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void TryConsolidateTailPlates()
|
||||
{
|
||||
var consolidated = true;
|
||||
while (consolidated)
|
||||
{
|
||||
consolidated = false;
|
||||
|
||||
var activePlates = _platePool.Where(p => p.Parts.Count > 0 && p.IsNew).ToList();
|
||||
if (activePlates.Count < 2)
|
||||
return;
|
||||
|
||||
var donors = activePlates.OrderBy(p => p.Plate.Utilization()).ToList();
|
||||
|
||||
foreach (var donor in donors)
|
||||
{
|
||||
if (donor.Parts.Count == 0)
|
||||
continue;
|
||||
|
||||
var donorParts = donor.Parts.ToList();
|
||||
var absorbed = false;
|
||||
|
||||
foreach (var target in activePlates)
|
||||
{
|
||||
if (target == donor || target.ChosenSize == null || target.Parts.Count == 0)
|
||||
continue;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (absorbed)
|
||||
{
|
||||
consolidated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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();
|
||||
public List<NestItem> UnplacedItems { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PlateResult
|
||||
{
|
||||
public Plate Plate { get; set; }
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,11 +56,6 @@ namespace OpenNest
|
||||
|
||||
protected FillPolicy BuildPolicy() => new FillPolicy(Comparer, PreferredDirection);
|
||||
|
||||
protected virtual BestFitResult SelectBestFitPair(List<BestFitResult> results)
|
||||
{
|
||||
return results.FirstOrDefault(r => r.Keep);
|
||||
}
|
||||
|
||||
// --- Virtual methods (side-effect-free, return parts) ---
|
||||
|
||||
public virtual List<Part> Fill(NestItem item, Box workArea,
|
||||
@@ -338,45 +333,56 @@ namespace OpenNest
|
||||
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||
var bestFit = SelectBestFitPair(bestFits);
|
||||
if (bestFit == null) continue;
|
||||
|
||||
var parts = bestFit.BuildParts(item.Drawing);
|
||||
List<Part> bestPlacement = null;
|
||||
Box bestTarget = null;
|
||||
|
||||
foreach (var fit in bestFits)
|
||||
{
|
||||
if (!fit.Keep)
|
||||
continue;
|
||||
|
||||
var parts = fit.BuildParts(item.Drawing);
|
||||
var pairBbox = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
|
||||
var pairW = pairBbox.Width;
|
||||
var pairL = pairBbox.Length;
|
||||
var minDim = System.Math.Min(pairW, pairL);
|
||||
|
||||
var remnants = finder.FindRemnants(minDim);
|
||||
Box target = null;
|
||||
|
||||
foreach (var r in remnants)
|
||||
{
|
||||
if (pairW <= r.Width + Tolerance.Epsilon &&
|
||||
pairL <= r.Length + Tolerance.Epsilon)
|
||||
{
|
||||
target = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (target == null) continue;
|
||||
|
||||
var offset = target.Location - pairBbox.Location;
|
||||
var offset = r.Location - pairBbox.Location;
|
||||
foreach (var p in parts)
|
||||
{
|
||||
p.Offset(offset);
|
||||
p.UpdateBounds();
|
||||
}
|
||||
|
||||
result.AddRange(parts);
|
||||
if (bestPlacement == null || IsBetterFill(parts, bestPlacement, r))
|
||||
{
|
||||
bestPlacement = parts;
|
||||
bestTarget = r;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPlacement == null) continue;
|
||||
|
||||
result.AddRange(bestPlacement);
|
||||
item.Quantity = 0;
|
||||
|
||||
var envelope = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
|
||||
var envelope = ((IEnumerable<IBoundable>)bestPlacement).GetBoundingBox();
|
||||
finder.AddObstacle(envelope.Offset(Plate.PartSpacing));
|
||||
|
||||
Debug.WriteLine($"[Nest] Placed best-fit pair for {item.Drawing.Name} " +
|
||||
$"at ({target.X:F1},{target.Y:F1}), size {pairW:F1}x{pairL:F1}");
|
||||
$"at ({bestTarget.X:F1},{bestTarget.Y:F1}), " +
|
||||
$"size {envelope.Width:F1}x{envelope.Length:F1}");
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace OpenNest
|
||||
{
|
||||
public enum PartSortOrder
|
||||
{
|
||||
BoundingBoxArea,
|
||||
Size,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
@@ -22,7 +23,8 @@ namespace OpenNest
|
||||
if (items == null || items.Count == 0 || plateOptions == null || plateOptions.Count == 0)
|
||||
return null;
|
||||
|
||||
// Find the minimum dimension needed to fit the largest part.
|
||||
// Find the minimum dimension needed to fit the largest part,
|
||||
// skipping items that are too large for every plate option.
|
||||
var minPartWidth = 0.0;
|
||||
var minPartLength = 0.0;
|
||||
foreach (var item in items)
|
||||
@@ -31,6 +33,14 @@ namespace OpenNest
|
||||
var bb = item.Drawing.Program.BoundingBox();
|
||||
var shortSide = System.Math.Min(bb.Width, bb.Length);
|
||||
var longSide = System.Math.Max(bb.Width, bb.Length);
|
||||
|
||||
if (!plateOptions.Any(o => FitsPart(o, shortSide, longSide, templatePlate.EdgeSpacing)))
|
||||
{
|
||||
Debug.WriteLine($"[PlateOptimizer] Skipping oversized item '{item.Drawing.Name}' " +
|
||||
$"({shortSide:F1}x{longSide:F1}) — does not fit any plate option");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shortSide > minPartWidth) minPartWidth = shortSide;
|
||||
if (longSide > minPartLength) minPartLength = longSide;
|
||||
}
|
||||
@@ -44,6 +54,19 @@ namespace OpenNest
|
||||
if (candidates.Count == 0)
|
||||
return null;
|
||||
|
||||
// Pre-compute best fits for all candidate plate sizes at once.
|
||||
// This runs the expensive GPU evaluation once on the largest plate
|
||||
// and filters the results for each smaller size.
|
||||
var plateSizes = candidates
|
||||
.Select(o => (Width: o.Length, Height: o.Width))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Quantity <= 0) continue;
|
||||
BestFitCache.ComputeForSizes(item.Drawing, templatePlate.PartSpacing, plateSizes);
|
||||
}
|
||||
|
||||
PlateOptimizerResult best = null;
|
||||
|
||||
foreach (var option in candidates)
|
||||
@@ -58,9 +81,10 @@ namespace OpenNest
|
||||
if (IsBetter(result, best))
|
||||
best = result;
|
||||
|
||||
// Early exit: when salvage is zero, cheapest plate that fits everything wins.
|
||||
// With salvage > 0, larger plates may have lower net cost, so keep searching.
|
||||
if (salvageRate <= 0)
|
||||
// Early exit: when all items fit, larger plates can only have
|
||||
// worse utilization and higher cost. With salvage < 100%, the
|
||||
// remnant credit never offsets the extra plate cost, so skip.
|
||||
if (salvageRate < 1.0)
|
||||
{
|
||||
var allPlaced = items.All(i => i.Quantity <= 0 ||
|
||||
result.Parts.Count(p => p.BaseDrawing.Name == i.Drawing.Name) >= i.Quantity);
|
||||
@@ -158,8 +182,8 @@ namespace OpenNest
|
||||
if (!candidate.NetCost.IsEqualTo(current.NetCost))
|
||||
return candidate.NetCost < current.NetCost;
|
||||
|
||||
// 3. Smaller plate area as tiebreak.
|
||||
return candidate.ChosenSize.Area < current.ChosenSize.Area;
|
||||
// 3. Higher utilization (tighter density) as tiebreak.
|
||||
return candidate.Utilization > current.Utilization;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
@@ -25,20 +24,6 @@ namespace OpenNest
|
||||
|
||||
public override NestDirection? PreferredDirection => NestDirection.Horizontal;
|
||||
|
||||
protected override BestFitResult SelectBestFitPair(List<BestFitResult> results)
|
||||
{
|
||||
BestFitResult best = null;
|
||||
|
||||
foreach (var r in results)
|
||||
{
|
||||
if (!r.Keep) continue;
|
||||
if (best == null || r.BoundingHeight < best.BoundingHeight)
|
||||
best = r;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||
{
|
||||
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static OpenNest.IO.NestFormat;
|
||||
|
||||
namespace OpenNest.IO
|
||||
{
|
||||
public static class EntitySerializer
|
||||
{
|
||||
public static EntitySetDto ToDto(List<Entity> entities, HashSet<Guid> suppressed)
|
||||
{
|
||||
return new EntitySetDto
|
||||
{
|
||||
Entities = entities.Select(ToEntityDto).ToList(),
|
||||
Suppressed = suppressed.Select(id => id.ToString()).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public static (List<Entity> entities, HashSet<Guid> suppressed) FromDto(EntitySetDto dto)
|
||||
{
|
||||
var entities = dto.Entities.Select(FromEntityDto).ToList();
|
||||
var suppressed = new HashSet<Guid>(dto.Suppressed.Select(Guid.Parse));
|
||||
return (entities, suppressed);
|
||||
}
|
||||
|
||||
private static EntityDto ToEntityDto(Entity entity)
|
||||
{
|
||||
switch (entity.Type)
|
||||
{
|
||||
case EntityType.Line:
|
||||
var line = (Line)entity;
|
||||
return new EntityDto
|
||||
{
|
||||
Id = entity.Id.ToString(),
|
||||
Type = "line",
|
||||
Layer = entity.Layer?.Name ?? "",
|
||||
LineType = entity.LineTypeName ?? "",
|
||||
X1 = line.StartPoint.X,
|
||||
Y1 = line.StartPoint.Y,
|
||||
X2 = line.EndPoint.X,
|
||||
Y2 = line.EndPoint.Y
|
||||
};
|
||||
|
||||
case EntityType.Arc:
|
||||
var arc = (Arc)entity;
|
||||
return new EntityDto
|
||||
{
|
||||
Id = entity.Id.ToString(),
|
||||
Type = "arc",
|
||||
Layer = entity.Layer?.Name ?? "",
|
||||
LineType = entity.LineTypeName ?? "",
|
||||
CX = arc.Center.X,
|
||||
CY = arc.Center.Y,
|
||||
R = arc.Radius,
|
||||
StartAngle = arc.StartAngle,
|
||||
EndAngle = arc.EndAngle,
|
||||
Reversed = arc.IsReversed
|
||||
};
|
||||
|
||||
case EntityType.Circle:
|
||||
var circle = (Circle)entity;
|
||||
return new EntityDto
|
||||
{
|
||||
Id = entity.Id.ToString(),
|
||||
Type = "circle",
|
||||
Layer = entity.Layer?.Name ?? "",
|
||||
LineType = entity.LineTypeName ?? "",
|
||||
CX = circle.Center.X,
|
||||
CY = circle.Center.Y,
|
||||
R = circle.Radius,
|
||||
Rotation = circle.Rotation == RotationType.CW ? "CW" : "CCW"
|
||||
};
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Entity type {entity.Type} is not supported for serialization.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Entity FromEntityDto(EntityDto dto)
|
||||
{
|
||||
Entity entity;
|
||||
|
||||
switch (dto.Type)
|
||||
{
|
||||
case "line":
|
||||
entity = new Line(
|
||||
new Vector(dto.X1, dto.Y1),
|
||||
new Vector(dto.X2, dto.Y2));
|
||||
break;
|
||||
|
||||
case "arc":
|
||||
entity = new Arc(
|
||||
new Vector(dto.CX, dto.CY),
|
||||
dto.R,
|
||||
dto.StartAngle,
|
||||
dto.EndAngle,
|
||||
dto.Reversed);
|
||||
break;
|
||||
|
||||
case "circle":
|
||||
var circle = new Circle(new Vector(dto.CX, dto.CY), dto.R);
|
||||
circle.Rotation = dto.Rotation == "CW" ? RotationType.CW : RotationType.CCW;
|
||||
entity = circle;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Entity type '{dto.Type}' is not supported for deserialization.");
|
||||
}
|
||||
|
||||
entity.Id = Guid.Parse(dto.Id);
|
||||
entity.Layer = ResolveLayer(dto.Layer);
|
||||
entity.LineTypeName = dto.LineType;
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static Layer ResolveLayer(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name) || name == "0")
|
||||
return Layer.Default;
|
||||
|
||||
if (string.Equals(name, SpecialLayers.Cut.Name, StringComparison.OrdinalIgnoreCase))
|
||||
return SpecialLayers.Cut;
|
||||
if (string.Equals(name, SpecialLayers.Rapid.Name, StringComparison.OrdinalIgnoreCase))
|
||||
return SpecialLayers.Rapid;
|
||||
if (string.Equals(name, SpecialLayers.Display.Name, StringComparison.OrdinalIgnoreCase))
|
||||
return SpecialLayers.Display;
|
||||
if (string.Equals(name, SpecialLayers.Leadin.Name, StringComparison.OrdinalIgnoreCase))
|
||||
return SpecialLayers.Leadin;
|
||||
if (string.Equals(name, SpecialLayers.Leadout.Name, StringComparison.OrdinalIgnoreCase))
|
||||
return SpecialLayers.Leadout;
|
||||
if (string.Equals(name, SpecialLayers.Scribe.Name, StringComparison.OrdinalIgnoreCase))
|
||||
return SpecialLayers.Scribe;
|
||||
|
||||
return new Layer(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,35 @@ namespace OpenNest.IO
|
||||
public double Cost { get; init; }
|
||||
}
|
||||
|
||||
public record EntitySetDto
|
||||
{
|
||||
public List<EntityDto> Entities { get; init; } = new();
|
||||
public List<string> Suppressed { get; init; } = new();
|
||||
}
|
||||
|
||||
public record EntityDto
|
||||
{
|
||||
public string Id { get; init; } = "";
|
||||
public string Type { get; init; } = "";
|
||||
public string Layer { get; init; } = "";
|
||||
public string LineType { get; init; } = "";
|
||||
|
||||
// Line
|
||||
public double X1 { get; init; }
|
||||
public double Y1 { get; init; }
|
||||
public double X2 { get; init; }
|
||||
public double Y2 { get; init; }
|
||||
|
||||
// Arc / Circle
|
||||
public double CX { get; init; }
|
||||
public double CY { get; init; }
|
||||
public double R { get; init; }
|
||||
public double StartAngle { get; init; }
|
||||
public double EndAngle { get; init; }
|
||||
public bool Reversed { get; init; }
|
||||
public string Rotation { get; init; } = "";
|
||||
}
|
||||
|
||||
public record BestFitSetDto
|
||||
{
|
||||
public double PlateWidth { get; init; }
|
||||
|
||||
@@ -36,7 +36,8 @@ namespace OpenNest.IO
|
||||
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
|
||||
|
||||
var programs = ReadPrograms(dto.Drawings.Count);
|
||||
var drawingMap = BuildDrawings(dto, programs);
|
||||
var entitySets = ReadEntitySets(dto.Drawings.Count);
|
||||
var drawingMap = BuildDrawings(dto, programs, entitySets);
|
||||
ReadBestFits(drawingMap);
|
||||
var nest = BuildNest(dto, drawingMap);
|
||||
|
||||
@@ -74,7 +75,25 @@ namespace OpenNest.IO
|
||||
return programs;
|
||||
}
|
||||
|
||||
private Dictionary<int, Drawing> BuildDrawings(NestDto dto, Dictionary<int, Program> programs)
|
||||
private Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed)> ReadEntitySets(int count)
|
||||
{
|
||||
var result = new Dictionary<int, (List<Entity>, HashSet<Guid>)>();
|
||||
for (var i = 1; i <= count; i++)
|
||||
{
|
||||
var entry = zipArchive.GetEntry($"entities/entities-{i}");
|
||||
if (entry == null) continue;
|
||||
|
||||
using var entryStream = entry.Open();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var json = reader.ReadToEnd();
|
||||
var dto = JsonSerializer.Deserialize<EntitySetDto>(json, JsonOptions);
|
||||
result[i] = EntitySerializer.FromDto(dto);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Dictionary<int, Drawing> BuildDrawings(NestDto dto, Dictionary<int, Program> programs,
|
||||
Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed)> entitySets)
|
||||
{
|
||||
var map = new Dictionary<int, Drawing>();
|
||||
foreach (var d in dto.Drawings)
|
||||
@@ -112,6 +131,12 @@ namespace OpenNest.IO
|
||||
if (programs.TryGetValue(d.Id, out var pgm))
|
||||
drawing.Program = pgm;
|
||||
|
||||
if (entitySets.TryGetValue(d.Id, out var entitySet))
|
||||
{
|
||||
drawing.SourceEntities = entitySet.entities;
|
||||
drawing.SuppressedEntityIds = entitySet.suppressed;
|
||||
}
|
||||
|
||||
map[d.Id] = drawing;
|
||||
}
|
||||
return map;
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace OpenNest.IO
|
||||
|
||||
WriteNestJson(zipArchive);
|
||||
WritePrograms(zipArchive);
|
||||
WriteEntities(zipArchive);
|
||||
WriteBestFits(zipArchive);
|
||||
|
||||
return true;
|
||||
@@ -312,6 +313,24 @@ namespace OpenNest.IO
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteEntities(ZipArchive zipArchive)
|
||||
{
|
||||
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||
{
|
||||
var drawing = kvp.Value;
|
||||
if (drawing.SourceEntities == null || drawing.SourceEntities.Count == 0)
|
||||
continue;
|
||||
|
||||
var dto = EntitySerializer.ToDto(drawing.SourceEntities, drawing.SuppressedEntityIds);
|
||||
var json = JsonSerializer.Serialize(dto, JsonOptions);
|
||||
|
||||
var entry = zipArchive.CreateEntry($"entities/entities-{kvp.Key}");
|
||||
using var stream = entry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write(json);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteDrawing(Stream stream, Drawing drawing)
|
||||
{
|
||||
var program = drawing.Program;
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
public class MultiPlateNesterTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public MultiPlateNesterTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
private static Drawing MakeDrawing(string name, double width, double length)
|
||||
{
|
||||
var program = new OpenNest.CNC.Program();
|
||||
program.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(width, 0)));
|
||||
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(width, length)));
|
||||
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, length)));
|
||||
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
var drawing = new Drawing(name, program);
|
||||
drawing.UpdateArea();
|
||||
return drawing;
|
||||
}
|
||||
|
||||
private static NestItem MakeItem(string name, double width, double length, int qty = 1)
|
||||
{
|
||||
return new NestItem
|
||||
{
|
||||
Drawing = MakeDrawing(name, width, length),
|
||||
Quantity = qty,
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SortByBoundingBoxArea_OrdersLargestFirst()
|
||||
{
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
MakeItem("small", 10, 10),
|
||||
MakeItem("large", 40, 60),
|
||||
MakeItem("medium", 20, 30),
|
||||
};
|
||||
|
||||
var sorted = MultiPlateNester.SortItems(items, PartSortOrder.BoundingBoxArea);
|
||||
|
||||
Assert.Equal("large", sorted[0].Drawing.Name);
|
||||
Assert.Equal("medium", sorted[1].Drawing.Name);
|
||||
Assert.Equal("small", sorted[2].Drawing.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SortBySize_OrdersByLongestDimension()
|
||||
{
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
MakeItem("short-wide", 50, 20), // longest = 50
|
||||
MakeItem("tall-narrow", 10, 80), // longest = 80
|
||||
MakeItem("square", 30, 30), // longest = 30
|
||||
};
|
||||
|
||||
var sorted = MultiPlateNester.SortItems(items, PartSortOrder.Size);
|
||||
|
||||
Assert.Equal("tall-narrow", sorted[0].Drawing.Name);
|
||||
Assert.Equal("short-wide", sorted[1].Drawing.Name);
|
||||
Assert.Equal("square", sorted[2].Drawing.Name);
|
||||
}
|
||||
|
||||
// --- Task 4: Part Classification ---
|
||||
|
||||
[Fact]
|
||||
public void Classify_LargePart_WhenWidthExceedsHalfWorkArea()
|
||||
{
|
||||
var workArea = new Box(0, 0, 96, 48);
|
||||
var bb = new Box(0, 0, 50, 20); // width 50 > half of 96 = 48
|
||||
var result = MultiPlateNester.Classify(bb, workArea);
|
||||
Assert.Equal(PartClass.Large, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_LargePart_WhenLengthExceedsHalfWorkArea()
|
||||
{
|
||||
var workArea = new Box(0, 0, 96, 48);
|
||||
var bb = new Box(0, 0, 20, 30); // length 30 > half of 48 = 24
|
||||
var result = MultiPlateNester.Classify(bb, workArea);
|
||||
Assert.Equal(PartClass.Large, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_MediumPart_NotLargeButAreaAboveThreshold()
|
||||
{
|
||||
var workArea = new Box(0, 0, 96, 48);
|
||||
// workArea = 4608, 1/9 = 512. bb = 40*15 = 600 > 512
|
||||
// 40 < 48 (half of 96), 15 < 24 (half of 48) — not Large
|
||||
var bb = new Box(0, 0, 40, 15);
|
||||
var result = MultiPlateNester.Classify(bb, workArea);
|
||||
Assert.Equal(PartClass.Medium, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_SmallPart()
|
||||
{
|
||||
var workArea = new Box(0, 0, 96, 48);
|
||||
// workArea = 4608, 1/9 = 512. bb = 10*10 = 100 < 512
|
||||
var bb = new Box(0, 0, 10, 10);
|
||||
var result = MultiPlateNester.Classify(bb, workArea);
|
||||
Assert.Equal(PartClass.Small, result);
|
||||
}
|
||||
|
||||
// --- Task 5: Scrap Zone Identification ---
|
||||
|
||||
[Fact]
|
||||
public void IsScrapRemnant_BothDimensionsBelowThreshold_ReturnsTrue()
|
||||
{
|
||||
var remnant = new Box(0, 0, 10, 8);
|
||||
Assert.True(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsScrapRemnant_OneDimensionAboveThreshold_ReturnsFalse()
|
||||
{
|
||||
// 11 x 120 — narrow but long, should be preserved
|
||||
var remnant = new Box(0, 0, 11, 120);
|
||||
Assert.False(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsScrapRemnant_BothDimensionsAboveThreshold_ReturnsFalse()
|
||||
{
|
||||
var remnant = new Box(0, 0, 20, 30);
|
||||
Assert.False(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindRemnants_ScrapOnly_ReturnsOnlyScrapRemnants()
|
||||
{
|
||||
// 96x48 plate with a 70x40 part placed at origin
|
||||
var plate = new Plate(96, 48) { PartSpacing = 0.25 };
|
||||
var drawing = MakeDrawing("big", 70, 40);
|
||||
var part = new Part(drawing);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var scrap = MultiPlateNester.FindRemnants(plate, 12.0, scrapOnly: true);
|
||||
|
||||
// All returned zones should have both dims < 12
|
||||
foreach (var zone in scrap)
|
||||
{
|
||||
Assert.True(zone.Width < 12.0 && zone.Length < 12.0,
|
||||
$"Zone {zone.Width:F1}x{zone.Length:F1} is not scrap — at least one dimension >= 12");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Task 6: Plate Creation Helper ---
|
||||
|
||||
[Fact]
|
||||
public void CreatePlate_UsesTemplateWhenNoOptions()
|
||||
{
|
||||
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
|
||||
template.EdgeSpacing = new Spacing { Left = 1, Right = 1, Top = 1, Bottom = 1 };
|
||||
|
||||
var plate = MultiPlateNester.CreatePlate(template, null, null);
|
||||
|
||||
Assert.Equal(96, plate.Size.Width);
|
||||
Assert.Equal(48, plate.Size.Length);
|
||||
Assert.Equal(0.25, plate.PartSpacing);
|
||||
Assert.Equal(1, plate.Quadrant);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatePlate_PicksSmallestFittingOption()
|
||||
{
|
||||
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
|
||||
template.EdgeSpacing = new Spacing { Left = 1, Right = 1, Top = 1, Bottom = 1 };
|
||||
|
||||
var options = new List<PlateOption>
|
||||
{
|
||||
new() { Width = 48, Length = 96, Cost = 100 },
|
||||
new() { Width = 60, Length = 120, Cost = 200 },
|
||||
new() { Width = 72, Length = 144, Cost = 300 },
|
||||
};
|
||||
|
||||
// Part needs 50x50 work area — 48x96 (after edge spacing: 46x94) — 46 < 50, doesn't fit.
|
||||
// 60x120 (58x118) does fit.
|
||||
var minBounds = new Box(0, 0, 50, 50);
|
||||
|
||||
var plate = MultiPlateNester.CreatePlate(template, options, minBounds);
|
||||
|
||||
Assert.Equal(60, plate.Size.Width);
|
||||
Assert.Equal(120, plate.Size.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateUpgrade_PrefersCheaperOption()
|
||||
{
|
||||
var currentOption = new PlateOption { Width = 48, Length = 96, Cost = 100 };
|
||||
var upgradeOption = new PlateOption { Width = 60, Length = 120, Cost = 160 };
|
||||
var newPlateOption = new PlateOption { Width = 48, Length = 96, Cost = 100 };
|
||||
|
||||
// Upgrade cost = 160 - 100 = 60
|
||||
// New plate cost with 50% utilization, 50% salvage:
|
||||
// remnantFraction = 0.5, salvageCredit = 0.5 * 100 * 0.5 = 25
|
||||
// netNewCost = 100 - 25 = 75
|
||||
// Upgrade (60) < new plate (75), so upgrade wins
|
||||
var decision = MultiPlateNester.EvaluateUpgradeVsNew(
|
||||
currentOption, upgradeOption, newPlateOption, 0.5, 0.5);
|
||||
|
||||
Assert.True(decision.ShouldUpgrade);
|
||||
}
|
||||
|
||||
// --- Task 7: Main Orchestration ---
|
||||
|
||||
[Fact]
|
||||
public void Nest_LargePartsGetOwnPlates()
|
||||
{
|
||||
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
|
||||
template.EdgeSpacing = new Spacing();
|
||||
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
MakeItem("big1", 80, 40, 1),
|
||||
MakeItem("big2", 70, 35, 1),
|
||||
};
|
||||
|
||||
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,
|
||||
$"Expected at least 2 plates, got {result.Plates.Count}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Nest_SmallPartsConsolidateOntoSharedPlates()
|
||||
{
|
||||
// Small parts should be packed together on shared plates rather than
|
||||
// each drawing getting its own plate. The consolidation pass fills
|
||||
// small parts into remaining space on existing plates.
|
||||
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
|
||||
template.EdgeSpacing = new Spacing();
|
||||
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
MakeItem("big", 80, 40, 1),
|
||||
MakeItem("tinyA", 5, 5, 3),
|
||||
MakeItem("tinyB", 4, 4, 3),
|
||||
};
|
||||
|
||||
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.
|
||||
Assert.True(result.Plates.Count <= 2,
|
||||
$"Expected at most 2 plates (small parts consolidated), got {result.Plates.Count}");
|
||||
Assert.Equal(0, result.UnplacedItems.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Nest_RespectsAllowPlateCreation()
|
||||
{
|
||||
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
|
||||
template.EdgeSpacing = new Spacing();
|
||||
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
MakeItem("big1", 80, 40, 1),
|
||||
MakeItem("big2", 70, 35, 1),
|
||||
};
|
||||
|
||||
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);
|
||||
Assert.Equal(2, result.UnplacedItems.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Nest_UsesExistingPlates()
|
||||
{
|
||||
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
|
||||
template.EdgeSpacing = new Spacing();
|
||||
|
||||
var existingPlate = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
|
||||
existingPlate.EdgeSpacing = new Spacing();
|
||||
|
||||
// Use a part small enough to be classified as Medium on a 96x48 plate.
|
||||
// Plate WorkArea: Width=96, Length=48. Half: 48, 24.
|
||||
// Part 24x22: Length=24 (not > 24), Width=22 (not > 48) — not Large.
|
||||
// Area = 528 > 4608/9 = 512 — Medium.
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
MakeItem("medium", 24, 22, 1),
|
||||
};
|
||||
|
||||
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);
|
||||
Assert.False(result.Plates[0].IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Nest_RealNestFile_PartFirst()
|
||||
{
|
||||
var nestPath = @"C:\Users\aisaacs\Desktop\4526 A14 - 0.188 AISI 304.nest";
|
||||
if (!File.Exists(nestPath))
|
||||
{
|
||||
_output.WriteLine("SKIP: nest file not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var nest = new NestReader(nestPath).Read();
|
||||
var template = nest.PlateDefaults.CreateNew();
|
||||
|
||||
_output.WriteLine($"Plate: {template.Size.Width}x{template.Size.Length}, " +
|
||||
$"spacing={template.PartSpacing}, edge=({template.EdgeSpacing.Left},{template.EdgeSpacing.Bottom},{template.EdgeSpacing.Right},{template.EdgeSpacing.Top})");
|
||||
|
||||
var wa = template.WorkArea();
|
||||
_output.WriteLine($"Work area: {wa.Width:F1}x{wa.Length:F1}");
|
||||
_output.WriteLine($"Classification thresholds: Large if dim > {wa.Width / 2:F1} or {wa.Length / 2:F1}, " +
|
||||
$"Medium if area > {wa.Width * wa.Length / 9:F0}");
|
||||
_output.WriteLine("---");
|
||||
|
||||
var items = new List<NestItem>();
|
||||
foreach (var d in nest.Drawings)
|
||||
{
|
||||
var qty = d.Quantity.Required > 0 ? d.Quantity.Required : d.Quantity.Remaining;
|
||||
if (qty <= 0) qty = 1;
|
||||
|
||||
var bb = d.Program.BoundingBox();
|
||||
var classification = MultiPlateNester.Classify(bb, wa);
|
||||
|
||||
_output.WriteLine($" {d.Name,-25} {bb.Width:F1}x{bb.Length:F1} (area={bb.Width * bb.Length:F0}) qty={qty} class={classification}");
|
||||
|
||||
items.Add(new NestItem
|
||||
{
|
||||
Drawing = d,
|
||||
Quantity = qty,
|
||||
StepAngle = d.Constraints.StepAngle,
|
||||
RotationStart = d.Constraints.StartAngle,
|
||||
RotationEnd = d.Constraints.EndAngle,
|
||||
});
|
||||
}
|
||||
|
||||
_output.WriteLine("---");
|
||||
_output.WriteLine($"Total: {items.Count} drawings, {items.Sum(i => i.Quantity)} parts");
|
||||
|
||||
var plateOptions = new List<PlateOption>
|
||||
{
|
||||
new() { Width = 48, Length = 96, Cost = 0 },
|
||||
new() { Width = 48, Length = 120, Cost = 0 },
|
||||
new() { Width = 48, Length = 144, Cost = 0 },
|
||||
new() { Width = 60, Length = 96, Cost = 0 },
|
||||
new() { Width = 60, Length = 120, Cost = 0 },
|
||||
new() { Width = 60, Length = 144, Cost = 0 },
|
||||
new() { Width = 72, Length = 96, Cost = 0 },
|
||||
new() { Width = 72, Length = 120, Cost = 0 },
|
||||
new() { Width = 72, Length = 144, Cost = 0 },
|
||||
};
|
||||
|
||||
_output.WriteLine($"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}");
|
||||
_output.WriteLine("");
|
||||
|
||||
var options = new MultiPlateNestOptions
|
||||
{
|
||||
Template = template,
|
||||
PlateOptions = plateOptions,
|
||||
};
|
||||
|
||||
var result = MultiPlateNester.Nest(items, options);
|
||||
|
||||
_output.WriteLine($"=== RESULTS: {result.Plates.Count} plates ===");
|
||||
|
||||
for (var i = 0; i < result.Plates.Count; i++)
|
||||
{
|
||||
var pr = result.Plates[i];
|
||||
var groups = pr.Parts.GroupBy(p => p.BaseDrawing.Name)
|
||||
.Select(g => $"{g.Key} x{g.Count()}")
|
||||
.ToList();
|
||||
_output.WriteLine($" Plate {i + 1} ({pr.Plate.Size.Width}x{pr.Plate.Size.Length}): " +
|
||||
$"{pr.Parts.Count} parts, util={pr.Plate.Utilization():P1} [{string.Join(", ", groups)}]");
|
||||
}
|
||||
|
||||
if (result.UnplacedItems.Count > 0)
|
||||
{
|
||||
_output.WriteLine($" Unplaced: {string.Join(", ", result.UnplacedItems.Select(i => $"{i.Drawing.Name} x{i.Quantity}"))}");
|
||||
}
|
||||
|
||||
_output.WriteLine($"\nTotal parts placed: {result.Plates.Sum(p => p.Parts.Count)}");
|
||||
_output.WriteLine($"Total plates used: {result.Plates.Count}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using OpenNest;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Tests.Fill
|
||||
{
|
||||
public class FillLinearCircleTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public FillLinearCircleTests(ITestOutputHelper output) => _output = output;
|
||||
|
||||
private static Drawing MakeCircleDrawing(double radius)
|
||||
{
|
||||
var pgm = new Program();
|
||||
var startPt = new Vector(radius * 2, radius); // rightmost point
|
||||
pgm.Codes.Add(new RapidMove(startPt));
|
||||
pgm.Codes.Add(new ArcMove(startPt, new Vector(radius, radius), RotationType.CCW));
|
||||
return new Drawing("circle", pgm);
|
||||
}
|
||||
|
||||
private static Drawing MakeRingDrawing(double outerRadius, double innerRadius)
|
||||
{
|
||||
var pgm = new Program();
|
||||
// Outer circle (CCW)
|
||||
var outerStart = new Vector(outerRadius * 2, outerRadius);
|
||||
pgm.Codes.Add(new RapidMove(outerStart));
|
||||
pgm.Codes.Add(new ArcMove(outerStart, new Vector(outerRadius, outerRadius), RotationType.CCW));
|
||||
// Inner circle (CW = hole)
|
||||
var innerStart = new Vector(outerRadius + innerRadius, outerRadius);
|
||||
pgm.Codes.Add(new RapidMove(innerStart));
|
||||
pgm.Codes.Add(new ArcMove(innerStart, new Vector(outerRadius, outerRadius), RotationType.CW));
|
||||
return new Drawing("ring", pgm);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2.0, 0.125)] // 4" diameter circle, 1/8" spacing
|
||||
[InlineData(1.0, 0.125)] // 2" diameter circle
|
||||
[InlineData(3.0, 0.0625)] // 6" diameter circle, 1/16" spacing
|
||||
[InlineData(0.5, 0.25)] // 1" diameter circle, 1/4" spacing
|
||||
public void CircleFill_OffsetBoundaries_DoNotOverlap(double radius, double spacing)
|
||||
{
|
||||
var drawing = MakeCircleDrawing(radius);
|
||||
var workArea = new Box(0, 0, 48, 48);
|
||||
var engine = new FillLinear(workArea, spacing);
|
||||
var parts = engine.Fill(drawing, 0, NestDirection.Horizontal);
|
||||
|
||||
_output.WriteLine($"Circle R={radius}, spacing={spacing}: {parts.Count} parts");
|
||||
|
||||
AssertNoOffsetOverlap(parts, spacing, radius * 2);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2.0, 1.5, 0.125)] // Ring: outer R=2, inner R=1.5
|
||||
[InlineData(1.5, 1.0, 0.125)] // Ring: outer R=1.5, inner R=1.0
|
||||
public void RingFill_OffsetBoundaries_DoNotOverlap(double outerR, double innerR, double spacing)
|
||||
{
|
||||
var drawing = MakeRingDrawing(outerR, innerR);
|
||||
var workArea = new Box(0, 0, 48, 48);
|
||||
var engine = new FillLinear(workArea, spacing);
|
||||
var parts = engine.Fill(drawing, 0, NestDirection.Horizontal);
|
||||
|
||||
_output.WriteLine($"Ring outerR={outerR}, innerR={innerR}, spacing={spacing}: {parts.Count} parts");
|
||||
|
||||
AssertNoOffsetOverlap(parts, spacing, outerR * 2);
|
||||
}
|
||||
|
||||
private void AssertNoOffsetOverlap(List<Part> parts, double spacing, double expectedDiameter)
|
||||
{
|
||||
if (parts.Count < 2)
|
||||
{
|
||||
_output.WriteLine(" Only 1 part placed, skipping overlap check");
|
||||
return;
|
||||
}
|
||||
|
||||
var halfSpacing = spacing / 2;
|
||||
var radius = expectedDiameter / 2;
|
||||
var minGap = double.MaxValue;
|
||||
var violationCount = 0;
|
||||
|
||||
// For circular parts, the center is at Location + (radius, radius).
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
var ci = parts[i].Location + new Vector(radius, radius);
|
||||
|
||||
for (var j = i + 1; j < parts.Count; j++)
|
||||
{
|
||||
var cj = parts[j].Location + new Vector(radius, radius);
|
||||
var centerDist = ci.DistanceTo(cj);
|
||||
|
||||
// Gap between raw circle perimeters
|
||||
var rawGap = centerDist - expectedDiameter;
|
||||
|
||||
// Gap between offset circle perimeters (halfSpacing each side)
|
||||
var offsetGap = centerDist - expectedDiameter - spacing;
|
||||
|
||||
if (rawGap < minGap)
|
||||
minGap = rawGap;
|
||||
|
||||
if (rawGap < spacing - Tolerance.Epsilon)
|
||||
{
|
||||
violationCount++;
|
||||
if (violationCount <= 5)
|
||||
{
|
||||
_output.WriteLine($" SPACING VIOLATION parts[{i}] vs parts[{j}]: " +
|
||||
$"centerDist={centerDist:F6}, rawGap={rawGap:F6}, offsetGap={offsetGap:F6}, " +
|
||||
$"expected>={spacing:F4}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine($" Min gap={minGap:F6}, expected>={spacing:F4}, violations={violationCount}");
|
||||
|
||||
if (violationCount > 0)
|
||||
{
|
||||
var maxDeficit = spacing - minGap;
|
||||
_output.WriteLine($" Max deficit={maxDeficit:F6}");
|
||||
Assert.Fail($"{violationCount} pairs violate spacing: min gap={minGap:F6}, expected>={spacing}, deficit={maxDeficit:F6}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Tests.Geometry;
|
||||
|
||||
public class SpatialQueryTests
|
||||
{
|
||||
#region Helpers
|
||||
|
||||
private static List<Entity> MakeSquare(double size)
|
||||
{
|
||||
return new List<Entity>
|
||||
{
|
||||
new Line(0, 0, size, 0),
|
||||
new Line(size, 0, size, size),
|
||||
new Line(size, size, 0, size),
|
||||
new Line(0, size, 0, 0),
|
||||
};
|
||||
}
|
||||
|
||||
private static List<Entity> MakeRoundedRect(double length, double width, double r)
|
||||
{
|
||||
return new List<Entity>
|
||||
{
|
||||
new Line(r, 0, length - r, 0),
|
||||
new Arc(length - r, r, r, Angle.ToRadians(270), Angle.ToRadians(360)),
|
||||
new Line(length, r, length, width - r),
|
||||
new Arc(length - r, width - r, r, Angle.ToRadians(0), Angle.ToRadians(90)),
|
||||
new Line(length - r, width, r, width),
|
||||
new Arc(r, width - r, r, Angle.ToRadians(90), Angle.ToRadians(180)),
|
||||
new Line(0, width - r, 0, r),
|
||||
new Arc(r, r, r, Angle.ToRadians(180), Angle.ToRadians(270)),
|
||||
};
|
||||
}
|
||||
|
||||
private static List<Entity> MakeCircle(double cx, double cy, double radius)
|
||||
{
|
||||
return new List<Entity> { new Circle(cx, cy, radius) };
|
||||
}
|
||||
|
||||
private static List<Entity> Translate(List<Entity> entities, double dx, double dy)
|
||||
{
|
||||
var result = new List<Entity>();
|
||||
foreach (var e in entities)
|
||||
{
|
||||
if (e is Line line)
|
||||
result.Add(new Line(line.pt1.X + dx, line.pt1.Y + dy, line.pt2.X + dx, line.pt2.Y + dy));
|
||||
else if (e is Arc arc)
|
||||
result.Add(new Arc(arc.Center.X + dx, arc.Center.Y + dy, arc.Radius, arc.StartAngle, arc.EndAngle));
|
||||
else if (e is Circle circle)
|
||||
result.Add(new Circle(circle.Center.X + dx, circle.Center.Y + dy, circle.Radius));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Circle vs Circle
|
||||
|
||||
[Fact]
|
||||
public void CircleToCircle_Right_ReturnsGap()
|
||||
{
|
||||
var a = MakeCircle(0, 0, 5);
|
||||
var b = MakeCircle(20, 0, 5);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, 9.9, 10.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CircleToCircle_Left_ReturnsGap()
|
||||
{
|
||||
var a = MakeCircle(20, 0, 5);
|
||||
var b = MakeCircle(0, 0, 5);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(-1, 0));
|
||||
|
||||
Assert.InRange(dist, 9.9, 10.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CircleToCircle_Up_ReturnsGap()
|
||||
{
|
||||
var a = MakeCircle(0, 0, 5);
|
||||
var b = MakeCircle(0, 20, 5);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(0, 1));
|
||||
|
||||
Assert.InRange(dist, 9.9, 10.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CircleToCircle_Touching_ReturnsZero()
|
||||
{
|
||||
var a = MakeCircle(0, 0, 5);
|
||||
var b = MakeCircle(10, 0, 5);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, -0.01, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CircleToCircle_NoPath_ReturnsMaxValue()
|
||||
{
|
||||
var a = MakeCircle(0, 0, 3);
|
||||
var b = MakeCircle(0, 20, 3);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
|
||||
Assert.Equal(double.MaxValue, dist);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CircleToCircle_PushDirection_Right()
|
||||
{
|
||||
var a = MakeCircle(0, 0, 5);
|
||||
var b = MakeCircle(20, 0, 5);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, PushDirection.Right);
|
||||
|
||||
Assert.InRange(dist, 9.9, 10.1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Square vs Square
|
||||
|
||||
[Fact]
|
||||
public void SquareToSquare_Right_ReturnsGap()
|
||||
{
|
||||
var a = MakeSquare(10);
|
||||
var b = Translate(MakeSquare(10), 25, 0);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, 14.9, 15.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareToSquare_Left_ReturnsGap()
|
||||
{
|
||||
var a = Translate(MakeSquare(10), 25, 0);
|
||||
var b = MakeSquare(10);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(-1, 0));
|
||||
|
||||
Assert.InRange(dist, 14.9, 15.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareToSquare_Down_ReturnsGap()
|
||||
{
|
||||
var a = Translate(MakeSquare(10), 0, 25);
|
||||
var b = MakeSquare(10);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(0, -1));
|
||||
|
||||
Assert.InRange(dist, 14.9, 15.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareToSquare_Touching_ReturnsZero()
|
||||
{
|
||||
var a = MakeSquare(10);
|
||||
var b = Translate(MakeSquare(10), 10, 0);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, -0.01, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareToSquare_NoOverlap_ReturnsMaxValue()
|
||||
{
|
||||
var a = MakeSquare(10);
|
||||
var b = Translate(MakeSquare(10), 0, 20);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
|
||||
Assert.Equal(double.MaxValue, dist);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareToSquare_PartialOverlap_Right()
|
||||
{
|
||||
var a = MakeSquare(10);
|
||||
var b = Translate(MakeSquare(10), 20, 5);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, 9.9, 10.1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rounded Rectangle
|
||||
|
||||
[Fact]
|
||||
public void RoundedRect_Right_ReturnsGap()
|
||||
{
|
||||
var a = MakeRoundedRect(20, 10, 2);
|
||||
var b = Translate(MakeRoundedRect(20, 10, 2), 30, 0);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, 9.9, 10.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundedRect_Up_ReturnsGap()
|
||||
{
|
||||
var a = MakeRoundedRect(20, 10, 2);
|
||||
var b = Translate(MakeRoundedRect(20, 10, 2), 0, 25);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(0, 1));
|
||||
|
||||
Assert.InRange(dist, 14.9, 15.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundedRect_Touching_ReturnsZero()
|
||||
{
|
||||
var a = MakeRoundedRect(20, 10, 2);
|
||||
var b = Translate(MakeRoundedRect(20, 10, 2), 20, 0);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, -0.01, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundedRect_Diagonal_ReturnsDistance()
|
||||
{
|
||||
var dir = new Vector(1 / System.Math.Sqrt(2), 1 / System.Math.Sqrt(2));
|
||||
var a = MakeRoundedRect(10, 10, 2);
|
||||
var b = Translate(MakeRoundedRect(10, 10, 2), 20, 20);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, dir);
|
||||
|
||||
Assert.True(dist > 0 && dist < double.MaxValue);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Circle vs Square
|
||||
|
||||
[Fact]
|
||||
public void CircleToSquare_Right_ReturnsGap()
|
||||
{
|
||||
var circle = MakeCircle(0, 5, 5);
|
||||
var square = Translate(MakeSquare(10), 15, 0);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(circle, square, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, 9.9, 10.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareToCircle_Right_ReturnsGap()
|
||||
{
|
||||
var square = MakeSquare(10);
|
||||
var circle = MakeCircle(25, 5, 5);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(square, circle, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, 9.9, 10.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CircleToSquare_Touching_ReturnsZero()
|
||||
{
|
||||
var circle = MakeCircle(0, 5, 5);
|
||||
var square = Translate(MakeSquare(10), 5, 0);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(circle, square, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, -0.01, 0.01);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Circle vs Rounded Rectangle
|
||||
|
||||
[Fact]
|
||||
public void CircleToRoundedRect_Right_ReturnsGap()
|
||||
{
|
||||
var circle = MakeCircle(0, 5, 5);
|
||||
var rect = Translate(MakeRoundedRect(20, 10, 2), 15, 0);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(circle, rect, new Vector(1, 0));
|
||||
|
||||
Assert.InRange(dist, 9.9, 10.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundedRectToCircle_Left_ReturnsGap()
|
||||
{
|
||||
var rect = Translate(MakeRoundedRect(20, 10, 2), 15, 0);
|
||||
var circle = MakeCircle(0, 5, 5);
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(rect, circle, new Vector(-1, 0));
|
||||
|
||||
Assert.InRange(dist, 9.9, 10.1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge cases
|
||||
|
||||
[Fact]
|
||||
public void EmptyLists_ReturnsMaxValue()
|
||||
{
|
||||
var a = new List<Entity>();
|
||||
var b = new List<Entity>();
|
||||
|
||||
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
|
||||
Assert.Equal(double.MaxValue, dist);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Symmetry_LeftRightReturnSameDistance()
|
||||
{
|
||||
var a = MakeSquare(10);
|
||||
var b = Translate(MakeSquare(10), 25, 0);
|
||||
|
||||
var right = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
var left = SpatialQuery.DirectionalDistance(b, a, new Vector(-1, 0));
|
||||
|
||||
Assert.InRange(System.Math.Abs(right - left), 0, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Symmetry_CirclesLeftRightSame()
|
||||
{
|
||||
var a = MakeCircle(0, 0, 5);
|
||||
var b = MakeCircle(20, 0, 5);
|
||||
|
||||
var right = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
|
||||
var left = SpatialQuery.DirectionalDistance(b, a, new Vector(-1, 0));
|
||||
|
||||
Assert.InRange(System.Math.Abs(right - left), 0, 0.01);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using OpenNest.IO.Bom;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
public static class ArchUnits
|
||||
{
|
||||
private static readonly Regex UnitRegex =
|
||||
new Regex("^(?<Feet>\\d+\\.?\\d*\\s*')?\\s*(?<Inches>\\d+\\.?\\d*\\s*\")?$");
|
||||
|
||||
public static double ParseToInches(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return 0;
|
||||
|
||||
var sb = new StringBuilder(input.Trim().ToLower());
|
||||
|
||||
sb.Replace("ft", "'");
|
||||
sb.Replace("feet", "'");
|
||||
sb.Replace("foot", "'");
|
||||
sb.Replace("inches", "\"");
|
||||
sb.Replace("inch", "\"");
|
||||
sb.Replace("in", "\"");
|
||||
|
||||
input = Fraction.ReplaceFractionsWithDecimals(sb.ToString());
|
||||
|
||||
var match = UnitRegex.Match(input);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
if (!input.Contains("'") && !input.Contains("\""))
|
||||
{
|
||||
if (double.TryParse(input.Trim(), out var plainInches))
|
||||
return System.Math.Round(plainInches, 8);
|
||||
}
|
||||
|
||||
throw new FormatException("Input is not in a valid format.");
|
||||
}
|
||||
|
||||
var feet = match.Groups["Feet"];
|
||||
var inches = match.Groups["Inches"];
|
||||
var totalInches = 0.0;
|
||||
|
||||
if (feet.Success)
|
||||
{
|
||||
var x = double.Parse(feet.Value.Remove(feet.Length - 1));
|
||||
totalInches += x * 12;
|
||||
}
|
||||
|
||||
if (inches.Success)
|
||||
{
|
||||
var x = double.Parse(inches.Value.Remove(inches.Length - 1));
|
||||
totalInches += x;
|
||||
}
|
||||
|
||||
return System.Math.Round(totalInches, 8);
|
||||
}
|
||||
|
||||
public static double GetLengthInches(TextBox tb)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (double.TryParse(tb.Text, out var d))
|
||||
{
|
||||
tb.ForeColor = SystemColors.WindowText;
|
||||
return d;
|
||||
}
|
||||
|
||||
var x = ParseToInches(tb.Text);
|
||||
tb.ForeColor = SystemColors.WindowText;
|
||||
return x;
|
||||
}
|
||||
catch
|
||||
{
|
||||
tb.ForeColor = Color.Red;
|
||||
return double.NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Action = OpenNest.Actions.Action;
|
||||
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
internal class ActionManager
|
||||
{
|
||||
private readonly PlateView view;
|
||||
private Action currentAction;
|
||||
private Action previousAction;
|
||||
|
||||
public ActionManager(PlateView view)
|
||||
{
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
public Action CurrentAction => currentAction;
|
||||
|
||||
public void SetAction(Type type)
|
||||
{
|
||||
var action = Activator.CreateInstance(type, view) as Action;
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
if (currentAction != null)
|
||||
{
|
||||
if (type == typeof(Actions.ActionSelect) && !(currentAction is Actions.ActionSelect))
|
||||
previousAction = currentAction;
|
||||
else
|
||||
previousAction = null;
|
||||
|
||||
currentAction.CancelAction();
|
||||
currentAction.DisconnectEvents();
|
||||
currentAction = null;
|
||||
}
|
||||
|
||||
currentAction = action;
|
||||
view.Status = GetDisplayName(type);
|
||||
}
|
||||
|
||||
public void SetAction(Type type, params object[] args)
|
||||
{
|
||||
if (currentAction != null)
|
||||
{
|
||||
previousAction = null;
|
||||
currentAction.CancelAction();
|
||||
currentAction.DisconnectEvents();
|
||||
currentAction = null;
|
||||
}
|
||||
|
||||
Array.Resize(ref args, args.Length + 1);
|
||||
for (var i = args.Length - 2; i >= 0; i--)
|
||||
args[i + 1] = args[i];
|
||||
args[0] = view;
|
||||
|
||||
var action = Activator.CreateInstance(type, args) as Action;
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
currentAction = action;
|
||||
view.Status = GetDisplayName(type);
|
||||
}
|
||||
|
||||
public void ProcessEscapeKey()
|
||||
{
|
||||
if (currentAction.IsBusy())
|
||||
currentAction.CancelAction();
|
||||
else if (currentAction is Actions.ActionSelect && previousAction != null)
|
||||
RestorePreviousAction();
|
||||
else
|
||||
view.SetAction(typeof(Actions.ActionSelect));
|
||||
}
|
||||
|
||||
public void RestorePreviousAction()
|
||||
{
|
||||
var action = previousAction;
|
||||
previousAction = null;
|
||||
|
||||
currentAction.CancelAction();
|
||||
currentAction.DisconnectEvents();
|
||||
|
||||
action.ConnectEvents();
|
||||
currentAction = action;
|
||||
|
||||
view.Status = GetDisplayName(action.GetType());
|
||||
}
|
||||
|
||||
public void OnPlateChanged()
|
||||
{
|
||||
if (currentAction == null || !currentAction.SurvivesPlateChange)
|
||||
view.SetAction(typeof(Actions.ActionSelect));
|
||||
else
|
||||
currentAction.OnPlateChanged();
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
if (currentAction != null)
|
||||
{
|
||||
currentAction.CancelAction();
|
||||
currentAction.DisconnectEvents();
|
||||
currentAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetDisplayName(Type type)
|
||||
{
|
||||
var attributes = type.GetCustomAttributes(true);
|
||||
foreach (var attr in attributes)
|
||||
{
|
||||
if (attr is DisplayNameAttribute displayNameAttr)
|
||||
return displayNameAttr.DisplayName;
|
||||
}
|
||||
return type.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
internal class CutOffHandler
|
||||
{
|
||||
private readonly PlateView view;
|
||||
private Dictionary<Part, Geometry.Entity> dragPerimeterCache;
|
||||
|
||||
public CutOffHandler(PlateView view)
|
||||
{
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
public bool IsDragging { get; private set; }
|
||||
|
||||
public CutOff TryStartDrag(Vector point, double tolerance)
|
||||
{
|
||||
var hitCutOff = GetCutOffAtPoint(point, tolerance);
|
||||
if (hitCutOff == null)
|
||||
return null;
|
||||
|
||||
IsDragging = true;
|
||||
dragPerimeterCache = Plate.BuildPerimeterCache(view.Plate);
|
||||
return hitCutOff;
|
||||
}
|
||||
|
||||
public void UpdateDrag(Vector currentPoint, CutOff cutOff)
|
||||
{
|
||||
if (!IsDragging || cutOff == null)
|
||||
return;
|
||||
|
||||
if (cutOff.Axis == CutOffAxis.Vertical)
|
||||
cutOff.Position = new Vector(currentPoint.X, cutOff.Position.Y);
|
||||
else
|
||||
cutOff.Position = new Vector(cutOff.Position.X, currentPoint.Y);
|
||||
|
||||
cutOff.Regenerate(view.Plate, view.CutOffSettings, dragPerimeterCache);
|
||||
view.Invalidate();
|
||||
}
|
||||
|
||||
public void EndDrag()
|
||||
{
|
||||
if (!IsDragging)
|
||||
return;
|
||||
|
||||
IsDragging = false;
|
||||
dragPerimeterCache = null;
|
||||
view.Plate.RegenerateCutOffs(view.CutOffSettings);
|
||||
view.Invalidate();
|
||||
}
|
||||
|
||||
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
|
||||
{
|
||||
if (view.Plate?.CutOffs == null)
|
||||
return null;
|
||||
|
||||
foreach (var cutoff in view.Plate.CutOffs)
|
||||
{
|
||||
var program = cutoff.Drawing?.Program;
|
||||
if (program == null)
|
||||
continue;
|
||||
|
||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||
{
|
||||
if (program.Codes[i] is RapidMove rapid &&
|
||||
program.Codes[i + 1] is LinearMove linear)
|
||||
{
|
||||
var line = new Line(rapid.EndPoint, linear.EndPoint);
|
||||
if (line.ClosestPointTo(point).DistanceTo(point) <= tolerance)
|
||||
return cutoff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace OpenNest.Controls
|
||||
{ "None", "Line", "Arc", "Line + Arc", "Clean Hole", "Line + Line" };
|
||||
|
||||
private static readonly string[] LeadOutTypes =
|
||||
{ "None", "Line", "Arc", "Microtab" };
|
||||
{ "None", "Line", "Arc" };
|
||||
|
||||
private readonly TabControl tabControl;
|
||||
private readonly ComboBox cboExternalLeadIn, cboExternalLeadOut;
|
||||
@@ -424,9 +424,6 @@ namespace OpenNest.Controls
|
||||
case 2:
|
||||
AddNumericField(panel, "Radius:", 0.25, ref y, "Radius");
|
||||
break;
|
||||
case 3:
|
||||
AddNumericField(panel, "Gap Size:", 0.06, ref y, "GapSize");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,10 +510,6 @@ namespace OpenNest.Controls
|
||||
combo.SelectedIndex = 2;
|
||||
SetParam(panel, "Radius", arc.Radius);
|
||||
break;
|
||||
case MicrotabLeadOut microtab:
|
||||
combo.SelectedIndex = 3;
|
||||
SetParam(panel, "GapSize", microtab.GapSize);
|
||||
break;
|
||||
default:
|
||||
combo.SelectedIndex = 0;
|
||||
break;
|
||||
@@ -572,10 +565,6 @@ namespace OpenNest.Controls
|
||||
{
|
||||
Radius = GetParam(panel, "Radius", 0.25)
|
||||
},
|
||||
3 => new MicrotabLeadOut
|
||||
{
|
||||
GapSize = GetParam(panel, "GapSize", 0.06)
|
||||
},
|
||||
_ => new NoLeadOut()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace OpenNest.Controls
|
||||
{
|
||||
ViewScale = 1.0f;
|
||||
ViewScaleMin = 0.3f;
|
||||
ViewScaleMax = 3000;
|
||||
ViewScaleMax = 10000;
|
||||
origin = new PointF(100, 100);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,16 +8,14 @@ namespace OpenNest.Controls
|
||||
|
||||
public class DrawingListBox : ListBox
|
||||
{
|
||||
private const int WM_ERASEBKGND = 0x0014;
|
||||
|
||||
private readonly Size imageSize;
|
||||
private readonly Font nameFont;
|
||||
private Point lastClickLocation;
|
||||
|
||||
public DrawingListBox()
|
||||
{
|
||||
SetStyle(
|
||||
ControlStyles.AllPaintingInWmPaint |
|
||||
ControlStyles.OptimizedDoubleBuffer, true);
|
||||
|
||||
DrawMode = DrawMode.OwnerDrawFixed;
|
||||
ItemHeight = 85;
|
||||
|
||||
@@ -149,6 +147,30 @@ namespace OpenNest.Controls
|
||||
base.OnMouseDown(e);
|
||||
lastClickLocation = e.Location;
|
||||
}
|
||||
|
||||
protected override void WndProc(ref Message m)
|
||||
{
|
||||
if (m.Msg == WM_ERASEBKGND)
|
||||
{
|
||||
var itemBottom = 0;
|
||||
|
||||
if (Items.Count > 0)
|
||||
{
|
||||
var lastVisible = System.Math.Min(TopIndex + (ClientSize.Height / ItemHeight), Items.Count - 1);
|
||||
itemBottom = GetItemRectangle(lastVisible).Bottom;
|
||||
}
|
||||
|
||||
if (itemBottom < ClientSize.Height)
|
||||
{
|
||||
using var g = Graphics.FromHdc(m.WParam);
|
||||
g.FillRectangle(Brushes.White, 0, itemBottom, ClientSize.Width, ClientSize.Height - itemBottom);
|
||||
}
|
||||
|
||||
m.Result = (IntPtr)1;
|
||||
return;
|
||||
}
|
||||
base.WndProc(ref m);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PointExtensions
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace OpenNest.Controls
|
||||
public List<Entity> Entities { get; set; } = new();
|
||||
public List<Entity> OriginalEntities { get; set; }
|
||||
public List<Bend> Bends { get; set; } = new();
|
||||
public HashSet<Guid> SuppressedEntityIds { get; set; }
|
||||
public Box Bounds { get; set; }
|
||||
public int EntityCount { get; set; }
|
||||
}
|
||||
|
||||
@@ -154,7 +154,10 @@ namespace OpenNest.Controls
|
||||
Font = new Font("Segoe UI", 9f)
|
||||
};
|
||||
list.ItemCheck += (s, e) =>
|
||||
{
|
||||
if (IsHandleCreated)
|
||||
BeginInvoke((Action)(() => FilterChanged?.Invoke(this, EventArgs.Empty)));
|
||||
};
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -167,10 +170,11 @@ namespace OpenNest.Controls
|
||||
layersList.Items.Clear();
|
||||
var layers = entities
|
||||
.Where(e => e.Layer != null)
|
||||
.Select(e => e.Layer.Name)
|
||||
.Distinct();
|
||||
.Select(e => e.Layer)
|
||||
.GroupBy(l => l.Name)
|
||||
.Select(g => g.First());
|
||||
foreach (var layer in layers)
|
||||
layersList.Items.Add(layer, true); // checked = visible
|
||||
layersList.Items.Add(layer.Name, layer.IsVisible);
|
||||
|
||||
layersPanel.HeaderText = $"Layers ({layersList.Items.Count})";
|
||||
|
||||
@@ -188,10 +192,10 @@ namespace OpenNest.Controls
|
||||
// Line Types
|
||||
lineTypesList.Items.Clear();
|
||||
var lineTypes = entities
|
||||
.Select(e => e.LineTypeName ?? "Continuous")
|
||||
.Distinct();
|
||||
.GroupBy(e => e.LineTypeName ?? "Continuous")
|
||||
.Select(g => new { Name = g.Key, Visible = g.Any(e => e.IsVisible) });
|
||||
foreach (var lt in lineTypes)
|
||||
lineTypesList.Items.Add(lt, true); // checked = visible
|
||||
lineTypesList.Items.Add(lt.Name, lt.Visible);
|
||||
|
||||
lineTypesPanel.HeaderText = $"Line Types ({lineTypesList.Items.Count})";
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
+145
-412
@@ -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
|
||||
@@ -233,7 +225,6 @@ namespace OpenNest.Controls
|
||||
protected override void OnMouseEnter(EventArgs e)
|
||||
{
|
||||
base.OnMouseEnter(e);
|
||||
if (!Focused) Focus();
|
||||
}
|
||||
|
||||
protected override void OnDragEnter(DragEventArgs drgevent)
|
||||
@@ -257,22 +248,25 @@ namespace OpenNest.Controls
|
||||
|
||||
protected override void OnMouseDown(MouseEventArgs e)
|
||||
{
|
||||
if (!Focused) Focus();
|
||||
|
||||
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 +282,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 +310,7 @@ namespace OpenNest.Controls
|
||||
|
||||
var angle = Angle.ToRadians((e.Delta > 0 ? -increment : increment) * multiplier);
|
||||
|
||||
RotateSelectedParts(angle);
|
||||
selection.RotateSelectedParts(angle);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -358,18 +349,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 +389,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 +405,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 +425,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 +481,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 +540,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 +616,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 +624,35 @@ namespace OpenNest.Controls
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void hoverTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
var graphPt = PointControlToGraph(hoverPoint);
|
||||
LayoutPart hitPart = null;
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// GraphicsPath in use by paint thread — skip this hover tick
|
||||
return;
|
||||
}
|
||||
|
||||
hoveredPart = hitPart;
|
||||
showTooltip = hitPart != null;
|
||||
|
||||
if (showTooltip)
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void plate_PartAdded(object sender, ItemAddedEventArgs<Part> e)
|
||||
{
|
||||
if (PartAdded != null)
|
||||
@@ -880,24 +670,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 +705,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,79 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
public class ShapePreviewControl : PlateView
|
||||
{
|
||||
private string[] infoLines;
|
||||
|
||||
public ShapePreviewControl()
|
||||
{
|
||||
DrawOrigin = false;
|
||||
DrawBounds = false;
|
||||
AllowPan = false;
|
||||
AllowSelect = false;
|
||||
AllowZoom = false;
|
||||
AllowDrop = false;
|
||||
BackColor = Color.White;
|
||||
}
|
||||
|
||||
public void SetInfo(params string[] lines)
|
||||
{
|
||||
infoLines = lines;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public void ShowDrawing(Drawing drawing)
|
||||
{
|
||||
Plate.Parts.Clear();
|
||||
Plate.Size = new Geometry.Size(0, 0);
|
||||
|
||||
if (drawing?.Program != null)
|
||||
{
|
||||
AddPartFromDrawing(drawing, Geometry.Vector.Zero);
|
||||
ZoomToFit();
|
||||
}
|
||||
else
|
||||
{
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnResize(System.EventArgs e)
|
||||
{
|
||||
base.OnResize(e);
|
||||
|
||||
if (Plate.Parts.Count > 0)
|
||||
ZoomToFit(false);
|
||||
}
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||
|
||||
e.Graphics.TranslateTransform(origin.X, origin.Y);
|
||||
Renderer.DrawPlate(e.Graphics);
|
||||
Renderer.DrawParts(e.Graphics);
|
||||
e.Graphics.ResetTransform();
|
||||
|
||||
PaintInfo(e.Graphics);
|
||||
}
|
||||
|
||||
private void PaintInfo(Graphics g)
|
||||
{
|
||||
if (infoLines == null) return;
|
||||
|
||||
var lineHeight = Font.GetHeight(g) + 1;
|
||||
var y = 4f;
|
||||
|
||||
foreach (var line in infoLines)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
g.DrawString(line, Font, Brushes.Black, 4, y);
|
||||
y += lineHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+149
-61
@@ -17,13 +17,20 @@ namespace OpenNest.Forms
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.engineLabel = new System.Windows.Forms.Label();
|
||||
this.engineComboBox = new System.Windows.Forms.ComboBox();
|
||||
this.partsGroup = new System.Windows.Forms.GroupBox();
|
||||
this.tabControl = new System.Windows.Forms.TabControl();
|
||||
this.partsTab = new System.Windows.Forms.TabPage();
|
||||
this.platesTab = new System.Windows.Forms.TabPage();
|
||||
this.partsGrid = new System.Windows.Forms.DataGridView();
|
||||
this.summaryLabel = new System.Windows.Forms.Label();
|
||||
this.optionsGroup = new System.Windows.Forms.GroupBox();
|
||||
this.engineLabel = new System.Windows.Forms.Label();
|
||||
this.engineComboBox = new System.Windows.Forms.ComboBox();
|
||||
this.createNewPlatesAsNeededBox = new System.Windows.Forms.CheckBox();
|
||||
this.partFirstGroup = new System.Windows.Forms.GroupBox();
|
||||
this.partFirstCheckBox = new System.Windows.Forms.CheckBox();
|
||||
this.sortOrderLabel = new System.Windows.Forms.Label();
|
||||
this.sortOrderComboBox = new System.Windows.Forms.ComboBox();
|
||||
this.minRemnantLabel = new System.Windows.Forms.Label();
|
||||
this.minRemnantBox = new System.Windows.Forms.TextBox();
|
||||
this.plateOptimizerGroup = new System.Windows.Forms.GroupBox();
|
||||
this.optimizePlateSizeBox = new System.Windows.Forms.CheckBox();
|
||||
this.plateGrid = new System.Windows.Forms.DataGridView();
|
||||
@@ -33,42 +40,53 @@ namespace OpenNest.Forms
|
||||
this.buttonPanel = new System.Windows.Forms.Panel();
|
||||
this.acceptButton = new System.Windows.Forms.Button();
|
||||
this.cancelButton = new System.Windows.Forms.Button();
|
||||
this.tabControl.SuspendLayout();
|
||||
this.partsTab.SuspendLayout();
|
||||
this.platesTab.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).BeginInit();
|
||||
this.partsGroup.SuspendLayout();
|
||||
this.optionsGroup.SuspendLayout();
|
||||
this.partFirstGroup.SuspendLayout();
|
||||
this.plateOptimizerGroup.SuspendLayout();
|
||||
this.buttonPanel.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// engineLabel
|
||||
// tabControl
|
||||
//
|
||||
this.engineLabel.AutoSize = true;
|
||||
this.engineLabel.Location = new System.Drawing.Point(12, 15);
|
||||
this.engineLabel.Name = "engineLabel";
|
||||
this.engineLabel.Size = new System.Drawing.Size(82, 16);
|
||||
this.engineLabel.TabIndex = 0;
|
||||
this.engineLabel.Text = "Nest Engine:";
|
||||
this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.tabControl.Controls.Add(this.partsTab);
|
||||
this.tabControl.Controls.Add(this.platesTab);
|
||||
this.tabControl.Location = new System.Drawing.Point(12, 12);
|
||||
this.tabControl.Name = "tabControl";
|
||||
this.tabControl.SelectedIndex = 0;
|
||||
this.tabControl.Size = new System.Drawing.Size(556, 490);
|
||||
this.tabControl.TabIndex = 0;
|
||||
//
|
||||
// engineComboBox
|
||||
// partsTab
|
||||
//
|
||||
this.engineComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.engineComboBox.Location = new System.Drawing.Point(100, 12);
|
||||
this.engineComboBox.Name = "engineComboBox";
|
||||
this.engineComboBox.Size = new System.Drawing.Size(200, 24);
|
||||
this.engineComboBox.TabIndex = 1;
|
||||
this.partsTab.Controls.Add(this.partsGrid);
|
||||
this.partsTab.Controls.Add(this.summaryLabel);
|
||||
this.partsTab.Location = new System.Drawing.Point(4, 25);
|
||||
this.partsTab.Name = "partsTab";
|
||||
this.partsTab.Padding = new System.Windows.Forms.Padding(6);
|
||||
this.partsTab.Size = new System.Drawing.Size(548, 461);
|
||||
this.partsTab.TabIndex = 0;
|
||||
this.partsTab.Text = "Parts";
|
||||
this.partsTab.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// partsGroup
|
||||
// platesTab
|
||||
//
|
||||
this.partsGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.partsGroup.Controls.Add(this.partsGrid);
|
||||
this.partsGroup.Controls.Add(this.summaryLabel);
|
||||
this.partsGroup.Location = new System.Drawing.Point(12, 42);
|
||||
this.partsGroup.Name = "partsGroup";
|
||||
this.partsGroup.Size = new System.Drawing.Size(556, 210);
|
||||
this.partsGroup.TabIndex = 2;
|
||||
this.partsGroup.TabStop = false;
|
||||
this.partsGroup.Text = "Parts";
|
||||
this.platesTab.Controls.Add(this.engineLabel);
|
||||
this.platesTab.Controls.Add(this.engineComboBox);
|
||||
this.platesTab.Controls.Add(this.createNewPlatesAsNeededBox);
|
||||
this.platesTab.Controls.Add(this.partFirstGroup);
|
||||
this.platesTab.Controls.Add(this.plateOptimizerGroup);
|
||||
this.platesTab.Location = new System.Drawing.Point(4, 25);
|
||||
this.platesTab.Name = "platesTab";
|
||||
this.platesTab.Padding = new System.Windows.Forms.Padding(6);
|
||||
this.platesTab.Size = new System.Drawing.Size(548, 461);
|
||||
this.platesTab.TabIndex = 1;
|
||||
this.platesTab.Text = "Plates";
|
||||
this.platesTab.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// partsGrid
|
||||
//
|
||||
@@ -78,43 +96,108 @@ namespace OpenNest.Forms
|
||||
this.partsGrid.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.partsGrid.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this.partsGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
this.partsGrid.Location = new System.Drawing.Point(10, 22);
|
||||
this.partsGrid.Location = new System.Drawing.Point(10, 10);
|
||||
this.partsGrid.Name = "partsGrid";
|
||||
this.partsGrid.RowHeadersVisible = false;
|
||||
this.partsGrid.AutoGenerateColumns = false;
|
||||
this.partsGrid.Size = new System.Drawing.Size(536, 160);
|
||||
this.partsGrid.Size = new System.Drawing.Size(528, 420);
|
||||
this.partsGrid.TabIndex = 0;
|
||||
//
|
||||
// summaryLabel
|
||||
//
|
||||
this.summaryLabel.AutoSize = true;
|
||||
this.summaryLabel.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||
this.summaryLabel.Location = new System.Drawing.Point(10, 188);
|
||||
this.summaryLabel.Location = new System.Drawing.Point(10, 436);
|
||||
this.summaryLabel.Name = "summaryLabel";
|
||||
this.summaryLabel.Size = new System.Drawing.Size(0, 16);
|
||||
this.summaryLabel.TabIndex = 1;
|
||||
//
|
||||
// optionsGroup
|
||||
// engineLabel
|
||||
//
|
||||
this.optionsGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.optionsGroup.Controls.Add(this.createNewPlatesAsNeededBox);
|
||||
this.optionsGroup.Location = new System.Drawing.Point(12, 258);
|
||||
this.optionsGroup.Name = "optionsGroup";
|
||||
this.optionsGroup.Size = new System.Drawing.Size(556, 48);
|
||||
this.optionsGroup.TabIndex = 3;
|
||||
this.optionsGroup.TabStop = false;
|
||||
this.optionsGroup.Text = "Options";
|
||||
this.engineLabel.AutoSize = true;
|
||||
this.engineLabel.Location = new System.Drawing.Point(10, 15);
|
||||
this.engineLabel.Name = "engineLabel";
|
||||
this.engineLabel.Size = new System.Drawing.Size(82, 16);
|
||||
this.engineLabel.TabIndex = 0;
|
||||
this.engineLabel.Text = "Nest Engine:";
|
||||
//
|
||||
// engineComboBox
|
||||
//
|
||||
this.engineComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.engineComboBox.Location = new System.Drawing.Point(98, 12);
|
||||
this.engineComboBox.Name = "engineComboBox";
|
||||
this.engineComboBox.Size = new System.Drawing.Size(200, 24);
|
||||
this.engineComboBox.TabIndex = 1;
|
||||
//
|
||||
// createNewPlatesAsNeededBox
|
||||
//
|
||||
this.createNewPlatesAsNeededBox.AutoSize = true;
|
||||
this.createNewPlatesAsNeededBox.Location = new System.Drawing.Point(10, 22);
|
||||
this.createNewPlatesAsNeededBox.Location = new System.Drawing.Point(10, 44);
|
||||
this.createNewPlatesAsNeededBox.Name = "createNewPlatesAsNeededBox";
|
||||
this.createNewPlatesAsNeededBox.Size = new System.Drawing.Size(202, 20);
|
||||
this.createNewPlatesAsNeededBox.TabIndex = 0;
|
||||
this.createNewPlatesAsNeededBox.TabIndex = 2;
|
||||
this.createNewPlatesAsNeededBox.Text = "Create new plates as needed";
|
||||
this.createNewPlatesAsNeededBox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// partFirstGroup
|
||||
//
|
||||
this.partFirstGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.partFirstGroup.Controls.Add(this.partFirstCheckBox);
|
||||
this.partFirstGroup.Controls.Add(this.sortOrderLabel);
|
||||
this.partFirstGroup.Controls.Add(this.sortOrderComboBox);
|
||||
this.partFirstGroup.Controls.Add(this.minRemnantLabel);
|
||||
this.partFirstGroup.Controls.Add(this.minRemnantBox);
|
||||
this.partFirstGroup.Location = new System.Drawing.Point(10, 72);
|
||||
this.partFirstGroup.Name = "partFirstGroup";
|
||||
this.partFirstGroup.Size = new System.Drawing.Size(528, 80);
|
||||
this.partFirstGroup.TabIndex = 3;
|
||||
this.partFirstGroup.TabStop = false;
|
||||
this.partFirstGroup.Text = " Part-First Mode";
|
||||
//
|
||||
// partFirstCheckBox
|
||||
//
|
||||
this.partFirstCheckBox.AutoSize = true;
|
||||
this.partFirstCheckBox.Location = new System.Drawing.Point(10, 0);
|
||||
this.partFirstCheckBox.Name = "partFirstCheckBox";
|
||||
this.partFirstCheckBox.Size = new System.Drawing.Size(15, 14);
|
||||
this.partFirstCheckBox.TabIndex = 0;
|
||||
this.partFirstCheckBox.UseVisualStyleBackColor = true;
|
||||
this.partFirstCheckBox.CheckedChanged += new System.EventHandler(this.partFirstCheckBox_CheckedChanged);
|
||||
//
|
||||
// sortOrderLabel
|
||||
//
|
||||
this.sortOrderLabel.AutoSize = true;
|
||||
this.sortOrderLabel.Location = new System.Drawing.Point(10, 26);
|
||||
this.sortOrderLabel.Name = "sortOrderLabel";
|
||||
this.sortOrderLabel.Size = new System.Drawing.Size(75, 16);
|
||||
this.sortOrderLabel.TabIndex = 1;
|
||||
this.sortOrderLabel.Text = "Sort Order:";
|
||||
//
|
||||
// sortOrderComboBox
|
||||
//
|
||||
this.sortOrderComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.sortOrderComboBox.Location = new System.Drawing.Point(100, 23);
|
||||
this.sortOrderComboBox.Name = "sortOrderComboBox";
|
||||
this.sortOrderComboBox.Size = new System.Drawing.Size(180, 24);
|
||||
this.sortOrderComboBox.TabIndex = 2;
|
||||
//
|
||||
// minRemnantLabel
|
||||
//
|
||||
this.minRemnantLabel.AutoSize = true;
|
||||
this.minRemnantLabel.Location = new System.Drawing.Point(10, 54);
|
||||
this.minRemnantLabel.Name = "minRemnantLabel";
|
||||
this.minRemnantLabel.Size = new System.Drawing.Size(117, 16);
|
||||
this.minRemnantLabel.TabIndex = 3;
|
||||
this.minRemnantLabel.Text = "Min Remnant Size:";
|
||||
//
|
||||
// minRemnantBox
|
||||
//
|
||||
this.minRemnantBox.Location = new System.Drawing.Point(133, 51);
|
||||
this.minRemnantBox.Name = "minRemnantBox";
|
||||
this.minRemnantBox.Size = new System.Drawing.Size(60, 22);
|
||||
this.minRemnantBox.TabIndex = 4;
|
||||
this.minRemnantBox.Text = "12";
|
||||
//
|
||||
// plateOptimizerGroup
|
||||
//
|
||||
this.plateOptimizerGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
@@ -123,9 +206,9 @@ namespace OpenNest.Forms
|
||||
this.plateOptimizerGroup.Controls.Add(this.salvageRateLabel);
|
||||
this.plateOptimizerGroup.Controls.Add(this.salvageRateBox);
|
||||
this.plateOptimizerGroup.Controls.Add(this.salvageRatePercentLabel);
|
||||
this.plateOptimizerGroup.Location = new System.Drawing.Point(12, 312);
|
||||
this.plateOptimizerGroup.Location = new System.Drawing.Point(10, 158);
|
||||
this.plateOptimizerGroup.Name = "plateOptimizerGroup";
|
||||
this.plateOptimizerGroup.Size = new System.Drawing.Size(556, 188);
|
||||
this.plateOptimizerGroup.Size = new System.Drawing.Size(528, 188);
|
||||
this.plateOptimizerGroup.TabIndex = 4;
|
||||
this.plateOptimizerGroup.TabStop = false;
|
||||
this.plateOptimizerGroup.Text = " Plate Optimizer";
|
||||
@@ -150,7 +233,7 @@ namespace OpenNest.Forms
|
||||
this.plateGrid.Name = "plateGrid";
|
||||
this.plateGrid.RowHeadersVisible = false;
|
||||
this.plateGrid.AutoGenerateColumns = false;
|
||||
this.plateGrid.Size = new System.Drawing.Size(536, 130);
|
||||
this.plateGrid.Size = new System.Drawing.Size(508, 130);
|
||||
this.plateGrid.TabIndex = 1;
|
||||
//
|
||||
// salvageRateLabel
|
||||
@@ -187,7 +270,7 @@ namespace OpenNest.Forms
|
||||
this.buttonPanel.Location = new System.Drawing.Point(0, 506);
|
||||
this.buttonPanel.Name = "buttonPanel";
|
||||
this.buttonPanel.Size = new System.Drawing.Size(580, 50);
|
||||
this.buttonPanel.TabIndex = 5;
|
||||
this.buttonPanel.TabIndex = 1;
|
||||
//
|
||||
// acceptButton
|
||||
//
|
||||
@@ -217,11 +300,7 @@ namespace OpenNest.Forms
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
this.CancelButton = this.cancelButton;
|
||||
this.ClientSize = new System.Drawing.Size(580, 556);
|
||||
this.Controls.Add(this.engineLabel);
|
||||
this.Controls.Add(this.engineComboBox);
|
||||
this.Controls.Add(this.partsGroup);
|
||||
this.Controls.Add(this.optionsGroup);
|
||||
this.Controls.Add(this.plateOptimizerGroup);
|
||||
this.Controls.Add(this.tabControl);
|
||||
this.Controls.Add(this.buttonPanel);
|
||||
this.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
@@ -232,28 +311,37 @@ namespace OpenNest.Forms
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "AutoNest";
|
||||
this.tabControl.ResumeLayout(false);
|
||||
this.partsTab.ResumeLayout(false);
|
||||
this.partsTab.PerformLayout();
|
||||
this.platesTab.ResumeLayout(false);
|
||||
this.platesTab.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).EndInit();
|
||||
this.partsGroup.ResumeLayout(false);
|
||||
this.partsGroup.PerformLayout();
|
||||
this.optionsGroup.ResumeLayout(false);
|
||||
this.optionsGroup.PerformLayout();
|
||||
this.partFirstGroup.ResumeLayout(false);
|
||||
this.partFirstGroup.PerformLayout();
|
||||
this.plateOptimizerGroup.ResumeLayout(false);
|
||||
this.plateOptimizerGroup.PerformLayout();
|
||||
this.buttonPanel.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Label engineLabel;
|
||||
private System.Windows.Forms.ComboBox engineComboBox;
|
||||
private System.Windows.Forms.GroupBox partsGroup;
|
||||
private System.Windows.Forms.TabControl tabControl;
|
||||
private System.Windows.Forms.TabPage partsTab;
|
||||
private System.Windows.Forms.TabPage platesTab;
|
||||
private System.Windows.Forms.DataGridView partsGrid;
|
||||
private System.Windows.Forms.Label summaryLabel;
|
||||
private System.Windows.Forms.GroupBox optionsGroup;
|
||||
private System.Windows.Forms.Label engineLabel;
|
||||
private System.Windows.Forms.ComboBox engineComboBox;
|
||||
private System.Windows.Forms.CheckBox createNewPlatesAsNeededBox;
|
||||
private System.Windows.Forms.GroupBox partFirstGroup;
|
||||
private System.Windows.Forms.CheckBox partFirstCheckBox;
|
||||
private System.Windows.Forms.Label sortOrderLabel;
|
||||
private System.Windows.Forms.ComboBox sortOrderComboBox;
|
||||
private System.Windows.Forms.Label minRemnantLabel;
|
||||
private System.Windows.Forms.TextBox minRemnantBox;
|
||||
private System.Windows.Forms.GroupBox plateOptimizerGroup;
|
||||
private System.Windows.Forms.CheckBox optimizePlateSizeBox;
|
||||
private System.Windows.Forms.DataGridView plateGrid;
|
||||
|
||||
@@ -22,6 +22,11 @@ namespace OpenNest.Forms
|
||||
LoadDefaultPlateOptions();
|
||||
SetPlateOptimizerVisible(false);
|
||||
|
||||
sortOrderComboBox.Items.Add("Bounding Box Area");
|
||||
sortOrderComboBox.Items.Add("Size");
|
||||
sortOrderComboBox.SelectedIndex = 0;
|
||||
SetPartFirstVisible(false);
|
||||
|
||||
partsGrid.DataError += PartsGrid_DataError;
|
||||
}
|
||||
|
||||
@@ -54,6 +59,32 @@ namespace OpenNest.Forms
|
||||
set { salvageRateBox.Text = (value * 100).ToString("F0"); }
|
||||
}
|
||||
|
||||
public bool PartFirstMode
|
||||
{
|
||||
get { return partFirstCheckBox.Checked; }
|
||||
set { partFirstCheckBox.Checked = value; }
|
||||
}
|
||||
|
||||
public PartSortOrder SortOrder
|
||||
{
|
||||
get
|
||||
{
|
||||
if (sortOrderComboBox.SelectedItem is string s && s == "Size")
|
||||
return PartSortOrder.Size;
|
||||
return PartSortOrder.BoundingBoxArea;
|
||||
}
|
||||
}
|
||||
|
||||
public double MinRemnantSize
|
||||
{
|
||||
get
|
||||
{
|
||||
if (double.TryParse(minRemnantBox.Text, out var val) && val > 0)
|
||||
return val;
|
||||
return 12.0;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadEngines()
|
||||
{
|
||||
foreach (var engine in NestEngineRegistry.AvailableEngines)
|
||||
@@ -242,6 +273,19 @@ namespace OpenNest.Forms
|
||||
salvageRatePercentLabel.Visible = visible;
|
||||
}
|
||||
|
||||
private void partFirstCheckBox_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
SetPartFirstVisible(partFirstCheckBox.Checked);
|
||||
}
|
||||
|
||||
private void SetPartFirstVisible(bool visible)
|
||||
{
|
||||
sortOrderLabel.Visible = visible;
|
||||
sortOrderComboBox.Visible = visible;
|
||||
minRemnantLabel.Visible = visible;
|
||||
minRemnantBox.Visible = visible;
|
||||
}
|
||||
|
||||
private void UpdateSummary()
|
||||
{
|
||||
var gridItems = partsGrid.DataSource as List<DataGridViewItem>;
|
||||
|
||||
@@ -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>
|
||||
+100
-27
@@ -1,7 +1,9 @@
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.IO.Bending;
|
||||
using OpenNest.IO.Bom;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -16,8 +18,9 @@ namespace OpenNest.Forms
|
||||
public partial class BomImportForm : Form
|
||||
{
|
||||
private List<BomPartRow> _parts;
|
||||
private Dictionary<string, (double Width, double Length)> _plateSizes;
|
||||
private Dictionary<string, GroupSettings> _groupSettings;
|
||||
private bool _suppressRegroup;
|
||||
private Nest.PlateSettings _templateDefaults;
|
||||
|
||||
public Form MdiParentForm { get; set; }
|
||||
|
||||
@@ -25,7 +28,38 @@ namespace OpenNest.Forms
|
||||
{
|
||||
InitializeComponent();
|
||||
_parts = new List<BomPartRow>();
|
||||
_plateSizes = new Dictionary<string, (double, double)>();
|
||||
_groupSettings = new Dictionary<string, GroupSettings>();
|
||||
_templateDefaults = LoadTemplateDefaults();
|
||||
ApplyTemplateDefaults();
|
||||
}
|
||||
|
||||
private Nest.PlateSettings LoadTemplateDefaults()
|
||||
{
|
||||
var templatePath = Properties.Settings.Default.NestTemplatePath;
|
||||
if (File.Exists(templatePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var nest = new NestReader(templatePath).Read();
|
||||
return nest.PlateDefaults;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Fallback defaults matching CreateDefaultNest
|
||||
return new Nest.PlateSettings
|
||||
{
|
||||
Size = new Geometry.Size(100, 100),
|
||||
Quadrant = 1,
|
||||
PartSpacing = 1,
|
||||
EdgeSpacing = new Spacing(1, 1, 1, 1),
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyTemplateDefaults()
|
||||
{
|
||||
txtPlateWidth.Text = _templateDefaults.Size.Width.ToString("0.####");
|
||||
txtPlateLength.Text = _templateDefaults.Size.Length.ToString("0.####");
|
||||
}
|
||||
|
||||
#region File Browsing
|
||||
@@ -154,7 +188,7 @@ namespace OpenNest.Forms
|
||||
_parts.Add(row);
|
||||
}
|
||||
|
||||
_plateSizes.Clear();
|
||||
_groupSettings.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -244,11 +278,11 @@ namespace OpenNest.Forms
|
||||
|
||||
private void RebuildGroups()
|
||||
{
|
||||
// Save existing plate sizes before rebuilding
|
||||
SavePlateSizes();
|
||||
// Save existing settings before rebuilding
|
||||
SaveGroupSettings();
|
||||
|
||||
var defaultWidth = double.TryParse(txtPlateWidth.Text, out var w) ? w : 60;
|
||||
var defaultLength = double.TryParse(txtPlateLength.Text, out var l) ? l : 120;
|
||||
var defaultWidth = double.TryParse(txtPlateWidth.Text, out var w) ? w : _templateDefaults.Size.Width;
|
||||
var defaultLength = double.TryParse(txtPlateLength.Text, out var l) ? l : _templateDefaults.Size.Length;
|
||||
|
||||
var groups = _parts
|
||||
.Where(p => p.IsEditable
|
||||
@@ -270,6 +304,11 @@ namespace OpenNest.Forms
|
||||
table.Columns.Add("Total Qty", typeof(int));
|
||||
table.Columns.Add("Plate Width", typeof(double));
|
||||
table.Columns.Add("Plate Length", typeof(double));
|
||||
table.Columns.Add("Part Spacing", typeof(double));
|
||||
table.Columns.Add("Edge Left", typeof(double));
|
||||
table.Columns.Add("Edge Bottom", typeof(double));
|
||||
table.Columns.Add("Edge Right", typeof(double));
|
||||
table.Columns.Add("Edge Top", typeof(double));
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
@@ -277,23 +316,27 @@ namespace OpenNest.Forms
|
||||
var thickness = group.Key.Thickness;
|
||||
var key = GroupKey(material, thickness);
|
||||
|
||||
var plateWidth = _plateSizes.TryGetValue(key, out var size) ? size.Width : defaultWidth;
|
||||
var plateLength = _plateSizes.TryGetValue(key, out _) ? size.Length : defaultLength;
|
||||
var existing = _groupSettings.TryGetValue(key, out var gs);
|
||||
|
||||
table.Rows.Add(
|
||||
material,
|
||||
thickness,
|
||||
group.Count(),
|
||||
group.Sum(p => p.Qty ?? 0),
|
||||
plateWidth,
|
||||
plateLength
|
||||
existing ? gs.PlateWidth : defaultWidth,
|
||||
existing ? gs.PlateLength : defaultLength,
|
||||
existing ? gs.PartSpacing : _templateDefaults.PartSpacing,
|
||||
existing ? gs.EdgeLeft : _templateDefaults.EdgeSpacing.Left,
|
||||
existing ? gs.EdgeBottom : _templateDefaults.EdgeSpacing.Bottom,
|
||||
existing ? gs.EdgeRight : _templateDefaults.EdgeSpacing.Right,
|
||||
existing ? gs.EdgeTop : _templateDefaults.EdgeSpacing.Top
|
||||
);
|
||||
}
|
||||
|
||||
dgvGroups.DataSource = table;
|
||||
|
||||
// Material, Thickness, Parts, Total Qty are read-only
|
||||
if (dgvGroups.Columns.Count >= 6)
|
||||
if (dgvGroups.Columns.Count > 0)
|
||||
{
|
||||
dgvGroups.Columns["Material"].ReadOnly = true;
|
||||
dgvGroups.Columns["Thickness"].ReadOnly = true;
|
||||
@@ -304,22 +347,28 @@ namespace OpenNest.Forms
|
||||
btnCreateNests.Enabled = table.Rows.Count > 0;
|
||||
}
|
||||
|
||||
private void SavePlateSizes()
|
||||
private void SaveGroupSettings()
|
||||
{
|
||||
if (dgvGroups.DataSource is not DataTable table)
|
||||
return;
|
||||
|
||||
_plateSizes.Clear();
|
||||
_groupSettings.Clear();
|
||||
foreach (DataRow row in table.Rows)
|
||||
{
|
||||
var material = row["Material"]?.ToString() ?? "";
|
||||
var thickness = row["Thickness"] is double t ? t : 0;
|
||||
var key = GroupKey(material, thickness);
|
||||
|
||||
var width = row["Plate Width"] is double pw ? pw : 60;
|
||||
var length = row["Plate Length"] is double pl ? pl : 120;
|
||||
|
||||
_plateSizes[key] = (width, length);
|
||||
_groupSettings[key] = new GroupSettings
|
||||
{
|
||||
PlateWidth = row["Plate Width"] is double pw ? pw : _templateDefaults.Size.Width,
|
||||
PlateLength = row["Plate Length"] is double pl ? pl : _templateDefaults.Size.Length,
|
||||
PartSpacing = row["Part Spacing"] is double ps ? ps : _templateDefaults.PartSpacing,
|
||||
EdgeLeft = row["Edge Left"] is double el ? el : _templateDefaults.EdgeSpacing.Left,
|
||||
EdgeBottom = row["Edge Bottom"] is double eb ? eb : _templateDefaults.EdgeSpacing.Bottom,
|
||||
EdgeRight = row["Edge Right"] is double er ? er : _templateDefaults.EdgeSpacing.Right,
|
||||
EdgeTop = row["Edge Top"] is double et ? et : _templateDefaults.EdgeSpacing.Top,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,11 +405,11 @@ namespace OpenNest.Forms
|
||||
if (_parts == null || _parts.Count == 0)
|
||||
return;
|
||||
|
||||
// Save latest plate size edits
|
||||
SavePlateSizes();
|
||||
// Save latest group edits
|
||||
SaveGroupSettings();
|
||||
|
||||
var defaultWidth = double.TryParse(txtPlateWidth.Text, out var dw) ? dw : 60;
|
||||
var defaultLength = double.TryParse(txtPlateLength.Text, out var dl) ? dl : 120;
|
||||
var defaultWidth = double.TryParse(txtPlateWidth.Text, out var dw) ? dw : _templateDefaults.Size.Width;
|
||||
var defaultLength = double.TryParse(txtPlateLength.Text, out var dl) ? dl : _templateDefaults.Size.Length;
|
||||
|
||||
var groups = _parts
|
||||
.Where(p => p.IsEditable
|
||||
@@ -391,8 +440,14 @@ namespace OpenNest.Forms
|
||||
var thickness = group.Key.Thickness;
|
||||
var key = GroupKey(material, thickness);
|
||||
|
||||
var plateWidth = _plateSizes.TryGetValue(key, out var size) ? size.Width : defaultWidth;
|
||||
var plateLength = _plateSizes.TryGetValue(key, out _) ? size.Length : defaultLength;
|
||||
var hasSettings = _groupSettings.TryGetValue(key, out var gs);
|
||||
var plateWidth = hasSettings ? gs.PlateWidth : defaultWidth;
|
||||
var plateLength = hasSettings ? gs.PlateLength : defaultLength;
|
||||
var partSpacing = hasSettings ? gs.PartSpacing : _templateDefaults.PartSpacing;
|
||||
var edgeLeft = hasSettings ? gs.EdgeLeft : _templateDefaults.EdgeSpacing.Left;
|
||||
var edgeBottom = hasSettings ? gs.EdgeBottom : _templateDefaults.EdgeSpacing.Bottom;
|
||||
var edgeRight = hasSettings ? gs.EdgeRight : _templateDefaults.EdgeSpacing.Right;
|
||||
var edgeTop = hasSettings ? gs.EdgeTop : _templateDefaults.EdgeSpacing.Top;
|
||||
|
||||
var nestName = $"{jobName} - {thickness:0.###} {material}";
|
||||
var nest = new Nest(nestName);
|
||||
@@ -401,9 +456,9 @@ namespace OpenNest.Forms
|
||||
nest.PlateDefaults.Size = new Geometry.Size(plateWidth, plateLength);
|
||||
nest.Thickness = thickness;
|
||||
nest.Material = new Material(material);
|
||||
nest.PlateDefaults.Quadrant = 1;
|
||||
nest.PlateDefaults.PartSpacing = 1;
|
||||
nest.PlateDefaults.EdgeSpacing = new Spacing(1, 1, 1, 1);
|
||||
nest.PlateDefaults.Quadrant = _templateDefaults.Quadrant;
|
||||
nest.PlateDefaults.PartSpacing = partSpacing;
|
||||
nest.PlateDefaults.EdgeSpacing = new Spacing(edgeLeft, edgeBottom, edgeRight, edgeTop);
|
||||
|
||||
foreach (var part in group)
|
||||
{
|
||||
@@ -417,12 +472,19 @@ namespace OpenNest.Forms
|
||||
{
|
||||
var result = Dxf.Import(part.DxfPath);
|
||||
|
||||
var bends = new List<Bend>();
|
||||
if (result.Document != null)
|
||||
bends = BendDetectorRegistry.AutoDetect(result.Document);
|
||||
Bend.UpdateEtchEntities(result.Entities, bends);
|
||||
|
||||
var drawingName = Path.GetFileNameWithoutExtension(part.DxfPath);
|
||||
var drawing = new Drawing(drawingName);
|
||||
drawing.Color = Drawing.GetNextColor();
|
||||
drawing.Source.Path = part.DxfPath;
|
||||
drawing.Quantity.Required = part.Qty ?? 1;
|
||||
drawing.Material = new Material(material);
|
||||
if (bends.Count > 0)
|
||||
drawing.Bends.AddRange(bends);
|
||||
|
||||
var normalized = ShapeProfile.NormalizeEntities(result.Entities);
|
||||
var pgm = ConvertGeometry.ToProgram(normalized);
|
||||
@@ -486,4 +548,15 @@ namespace OpenNest.Forms
|
||||
public string Status { get; set; }
|
||||
public bool IsEditable { get; set; }
|
||||
}
|
||||
|
||||
internal class GroupSettings
|
||||
{
|
||||
public double PlateWidth { get; set; }
|
||||
public double PlateLength { get; set; }
|
||||
public double PartSpacing { get; set; }
|
||||
public double EdgeLeft { get; set; }
|
||||
public double EdgeBottom { get; set; }
|
||||
public double EdgeRight { get; set; }
|
||||
public double EdgeTop { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ namespace OpenNest.Forms
|
||||
if (item.Entities.Any(e => e.Layer != null))
|
||||
item.Entities.ForEach(e => e.Layer.IsVisible = true);
|
||||
ReHidePromotedEntities(item.Bends);
|
||||
ReHideSuppressedEntities(item);
|
||||
|
||||
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||
|
||||
@@ -245,6 +246,7 @@ namespace OpenNest.Forms
|
||||
|
||||
filterPanel.ApplyFilters(item.Entities);
|
||||
ReHidePromotedEntities(item.Bends);
|
||||
SyncSuppressedState(item);
|
||||
entityView1.Invalidate();
|
||||
staleProgram = true;
|
||||
}
|
||||
@@ -604,6 +606,61 @@ namespace OpenNest.Forms
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load Existing Drawings
|
||||
|
||||
public void LoadDrawings(IEnumerable<Drawing> drawings)
|
||||
{
|
||||
foreach (var drawing in drawings)
|
||||
{
|
||||
List<Entity> entities;
|
||||
|
||||
if (drawing.SourceEntities != null)
|
||||
{
|
||||
// Use stored entities with stable GUIDs; apply suppression state
|
||||
entities = new List<Entity>(drawing.SourceEntities);
|
||||
|
||||
foreach (var entity in entities)
|
||||
entity.IsVisible = !drawing.SuppressedEntityIds.Contains(entity.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: derive entities from Program (older drawings without source entities)
|
||||
entities = ConvertProgram.ToGeometry(drawing.Program);
|
||||
|
||||
// Re-apply source offset so entities appear in their natural position
|
||||
if (drawing.Source?.Offset != null && drawing.Source.Offset != Vector.Zero)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
entity.Offset(drawing.Source.Offset);
|
||||
}
|
||||
|
||||
// Remove rapid traversals — they aren't part of the cut geometry
|
||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||
}
|
||||
|
||||
var bounds = entities.GetBoundingBox();
|
||||
|
||||
var item = new FileListItem
|
||||
{
|
||||
Name = drawing.Name,
|
||||
Entities = entities,
|
||||
Path = drawing.Source?.Path,
|
||||
Quantity = drawing.Quantity.Required,
|
||||
Customer = drawing.Customer ?? string.Empty,
|
||||
Bends = drawing.Bends?.ToList() ?? new List<Bend>(),
|
||||
SuppressedEntityIds = drawing.SuppressedEntityIds.Count > 0
|
||||
? new HashSet<Guid>(drawing.SuppressedEntityIds)
|
||||
: null,
|
||||
Bounds = bounds,
|
||||
EntityCount = entities.Count
|
||||
};
|
||||
|
||||
fileList.AddItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output
|
||||
|
||||
public List<Drawing> GetDrawings()
|
||||
@@ -644,6 +701,22 @@ namespace OpenNest.Forms
|
||||
drawing.Program = programEditor.Program;
|
||||
else
|
||||
drawing.Program = pgm;
|
||||
|
||||
// Store all entities with stable GUIDs; track suppressed by ID
|
||||
var bendSources = new HashSet<Entity>(
|
||||
(item.Bends ?? new List<Bend>())
|
||||
.Where(b => b.SourceEntity != null)
|
||||
.Select(b => b.SourceEntity));
|
||||
|
||||
drawing.SourceEntities = item.Entities
|
||||
.Where(e => !bendSources.Contains(e))
|
||||
.ToList();
|
||||
|
||||
drawing.SuppressedEntityIds = new HashSet<Guid>(
|
||||
drawing.SourceEntities
|
||||
.Where(e => !(e.Layer.IsVisible && e.IsVisible))
|
||||
.Select(e => e.Id));
|
||||
|
||||
drawings.Add(drawing);
|
||||
|
||||
Thread.Sleep(20);
|
||||
@@ -666,6 +739,47 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReHideSuppressedEntities(FileListItem item)
|
||||
{
|
||||
if (item.SuppressedEntityIds == null || item.SuppressedEntityIds.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var entity in item.Entities)
|
||||
{
|
||||
if (item.SuppressedEntityIds.Contains(entity.Id))
|
||||
entity.IsVisible = false;
|
||||
}
|
||||
|
||||
// If all entities on a layer are suppressed, uncheck the layer too
|
||||
var layerGroups = item.Entities
|
||||
.Where(e => e.Layer != null)
|
||||
.GroupBy(e => e.Layer);
|
||||
|
||||
foreach (var group in layerGroups)
|
||||
{
|
||||
if (group.All(e => !e.IsVisible))
|
||||
group.Key.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SyncSuppressedState(FileListItem item)
|
||||
{
|
||||
var bendSources = new HashSet<Entity>(
|
||||
(item.Bends ?? new List<Bend>())
|
||||
.Where(b => b.SourceEntity != null)
|
||||
.Select(b => b.SourceEntity));
|
||||
|
||||
var suppressed = item.Entities
|
||||
.Where(e => !(e.Layer.IsVisible && e.IsVisible))
|
||||
.Where(e => !bendSources.Contains(e))
|
||||
.Select(e => e.Id);
|
||||
|
||||
item.SuppressedEntityIds = new HashSet<Guid>(suppressed);
|
||||
|
||||
if (item.SuppressedEntityIds.Count == 0)
|
||||
item.SuppressedEntityIds = null;
|
||||
}
|
||||
|
||||
|
||||
private static Color GetNextColor() => Drawing.GetNextColor();
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Controls;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
public class CuttingParametersDialog : Form
|
||||
{
|
||||
private readonly CuttingPanel cuttingPanel;
|
||||
|
||||
public CuttingParametersDialog()
|
||||
{
|
||||
Text = "Cutting Parameters";
|
||||
Size = new Size(400, 560);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
|
||||
cuttingPanel = new CuttingPanel
|
||||
{
|
||||
Dock = DockStyle.Fill
|
||||
};
|
||||
|
||||
var buttonPanel = new Panel
|
||||
{
|
||||
Dock = DockStyle.Bottom,
|
||||
Height = 40
|
||||
};
|
||||
|
||||
var btnOk = new Button
|
||||
{
|
||||
Text = "OK",
|
||||
DialogResult = DialogResult.OK,
|
||||
Size = new Size(80, 28),
|
||||
Location = new Point(220, 6)
|
||||
};
|
||||
|
||||
var btnCancel = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Size = new Size(80, 28),
|
||||
Location = new Point(305, 6)
|
||||
};
|
||||
|
||||
buttonPanel.Controls.Add(btnOk);
|
||||
buttonPanel.Controls.Add(btnCancel);
|
||||
|
||||
Controls.Add(cuttingPanel);
|
||||
Controls.Add(buttonPanel);
|
||||
|
||||
AcceptButton = btnOk;
|
||||
CancelButton = btnCancel;
|
||||
}
|
||||
|
||||
public void LoadParameters(CuttingParameters parameters)
|
||||
{
|
||||
cuttingPanel.LoadFromParameters(parameters);
|
||||
}
|
||||
|
||||
public CuttingParameters GetParameters()
|
||||
{
|
||||
return cuttingPanel.BuildParameters();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,6 @@ namespace OpenNest.Forms
|
||||
{
|
||||
LineLeadOut line => new LeadOutDto { Type = "Line", Length = line.Length, ApproachAngle = line.ApproachAngle },
|
||||
ArcLeadOut arc => new LeadOutDto { Type = "Arc", Radius = arc.Radius },
|
||||
MicrotabLeadOut mt => new LeadOutDto { Type = "Microtab", GapSize = mt.GapSize },
|
||||
_ => new LeadOutDto { Type = "None" }
|
||||
};
|
||||
}
|
||||
@@ -97,7 +96,6 @@ namespace OpenNest.Forms
|
||||
{
|
||||
"Line" => new LineLeadOut { Length = dto.Length, ApproachAngle = dto.ApproachAngle },
|
||||
"Arc" => new ArcLeadOut { Radius = dto.Radius },
|
||||
"Microtab" => new MicrotabLeadOut { GapSize = dto.GapSize },
|
||||
_ => new NoLeadOut()
|
||||
};
|
||||
}
|
||||
|
||||
Generated
+21
-2
@@ -47,6 +47,8 @@
|
||||
drawingListBox1 = new OpenNest.Controls.DrawingListBox();
|
||||
toolStrip2 = new System.Windows.Forms.ToolStrip();
|
||||
toolStripButton2 = new System.Windows.Forms.ToolStripButton();
|
||||
toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
|
||||
editDrawingsButton = new System.Windows.Forms.ToolStripButton();
|
||||
toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
toolStripButton3 = new System.Windows.Forms.ToolStripButton();
|
||||
toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
||||
@@ -175,7 +177,7 @@
|
||||
// toolStripLabel2
|
||||
//
|
||||
toolStripLabel2.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
|
||||
toolStripLabel2.Image = Properties.Resources.delete;
|
||||
toolStripLabel2.Image = (System.Drawing.Image)resources.GetObject("toolStripLabel2.Image");
|
||||
toolStripLabel2.Name = "toolStripLabel2";
|
||||
toolStripLabel2.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
|
||||
toolStripLabel2.Size = new System.Drawing.Size(34, 24);
|
||||
@@ -217,7 +219,7 @@
|
||||
//
|
||||
toolStrip2.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
|
||||
toolStrip2.ImageScalingSize = new System.Drawing.Size(20, 20);
|
||||
toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripButton2, toolStripSeparator1, toolStripButton3, toolStripSeparator2, hideNestedButton });
|
||||
toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripButton2, toolStripSeparator4, editDrawingsButton, toolStripSeparator1, toolStripButton3, toolStripSeparator2, hideNestedButton });
|
||||
toolStrip2.Location = new System.Drawing.Point(4, 3);
|
||||
toolStrip2.Name = "toolStrip2";
|
||||
toolStrip2.Size = new System.Drawing.Size(265, 27);
|
||||
@@ -236,6 +238,21 @@
|
||||
toolStripButton2.Text = "Import Drawings";
|
||||
toolStripButton2.Click += ImportDrawings_Click;
|
||||
//
|
||||
// toolStripSeparator4
|
||||
//
|
||||
toolStripSeparator4.Name = "toolStripSeparator4";
|
||||
toolStripSeparator4.Size = new System.Drawing.Size(6, 27);
|
||||
//
|
||||
// editDrawingsButton
|
||||
//
|
||||
editDrawingsButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
|
||||
editDrawingsButton.Image = (System.Drawing.Image)resources.GetObject("editDrawingsButton.Image");
|
||||
editDrawingsButton.Name = "editDrawingsButton";
|
||||
editDrawingsButton.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
|
||||
editDrawingsButton.Size = new System.Drawing.Size(34, 24);
|
||||
editDrawingsButton.Text = "Edit Drawings in Converter";
|
||||
editDrawingsButton.Click += EditDrawingsInConverter_Click;
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
@@ -312,6 +329,8 @@
|
||||
private System.Windows.Forms.ColumnHeader utilColumn;
|
||||
private System.Windows.Forms.ToolStrip toolStrip2;
|
||||
private System.Windows.Forms.ToolStripButton toolStripButton2;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
|
||||
private System.Windows.Forms.ToolStripButton editDrawingsButton;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
||||
private System.Windows.Forms.ToolStripButton toolStripButton3;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
|
||||
|
||||
+106
-33
@@ -52,6 +52,7 @@ namespace OpenNest.Forms
|
||||
private EditNestForm()
|
||||
{
|
||||
PlateView = new PlateView();
|
||||
PlateView.MouseEnter += PlateView_MouseEnter;
|
||||
PlateView.Enter += PlateView_Enter;
|
||||
PlateView.PartAdded += PlateView_PartAdded;
|
||||
PlateView.PartRemoved += PlateView_PartRemoved;
|
||||
@@ -718,19 +719,17 @@ namespace OpenNest.Forms
|
||||
|
||||
var plate = PlateView.Plate;
|
||||
|
||||
if (plate.CuttingParameters == null)
|
||||
{
|
||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
try { plate.CuttingParameters = CuttingParametersSerializer.Deserialize(json); }
|
||||
catch { plate.CuttingParameters = new CuttingParameters(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
plate.CuttingParameters = new CuttingParameters();
|
||||
}
|
||||
}
|
||||
var parameters = LoadOrDefaultParameters(plate.CuttingParameters);
|
||||
|
||||
using var dlg = new CuttingParametersDialog();
|
||||
dlg.LoadParameters(parameters);
|
||||
|
||||
if (dlg.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
parameters = dlg.GetParameters();
|
||||
plate.CuttingParameters = parameters;
|
||||
SaveCuttingParameters(parameters);
|
||||
|
||||
var assigner = new LeadInAssigner
|
||||
{
|
||||
@@ -781,17 +780,16 @@ namespace OpenNest.Forms
|
||||
if (Nest == null)
|
||||
return;
|
||||
|
||||
CuttingParameters parameters;
|
||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
try { parameters = CuttingParametersSerializer.Deserialize(json); }
|
||||
catch { parameters = new CuttingParameters(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
parameters = new CuttingParameters();
|
||||
}
|
||||
var parameters = LoadOrDefaultParameters(PlateView?.Plate?.CuttingParameters);
|
||||
|
||||
using var dlg = new CuttingParametersDialog();
|
||||
dlg.LoadParameters(parameters);
|
||||
|
||||
if (dlg.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
parameters = dlg.GetParameters();
|
||||
SaveCuttingParameters(parameters);
|
||||
|
||||
var assigner = new LeadInAssigner
|
||||
{
|
||||
@@ -839,22 +837,32 @@ namespace OpenNest.Forms
|
||||
|
||||
var plate = PlateView.Plate;
|
||||
|
||||
// If no cutting parameters exist, initialize from saved settings or defaults
|
||||
if (plate.CuttingParameters == null)
|
||||
plate.CuttingParameters = LoadOrDefaultParameters(null);
|
||||
|
||||
PlateView.SetAction(typeof(Actions.ActionLeadIn));
|
||||
}
|
||||
|
||||
private static CuttingParameters LoadOrDefaultParameters(CuttingParameters existing)
|
||||
{
|
||||
if (existing != null)
|
||||
return existing;
|
||||
|
||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
try { plate.CuttingParameters = CuttingParametersSerializer.Deserialize(json); }
|
||||
catch { plate.CuttingParameters = new CuttingParameters(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
plate.CuttingParameters = new CuttingParameters();
|
||||
}
|
||||
try { return CuttingParametersSerializer.Deserialize(json); }
|
||||
catch { /* fall through */ }
|
||||
}
|
||||
|
||||
PlateView.SetAction(typeof(Actions.ActionLeadIn));
|
||||
return new CuttingParameters();
|
||||
}
|
||||
|
||||
private static void SaveCuttingParameters(CuttingParameters parameters)
|
||||
{
|
||||
var json = CuttingParametersSerializer.Serialize(parameters);
|
||||
Properties.Settings.Default.CuttingParametersJson = json;
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
|
||||
private void ImportDrawings_Click(object sender, EventArgs e)
|
||||
@@ -862,6 +870,55 @@ namespace OpenNest.Forms
|
||||
Import();
|
||||
}
|
||||
|
||||
private void EditDrawingsInConverter_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (Nest.Drawings.Count == 0)
|
||||
return;
|
||||
|
||||
var converter = new CadConverterForm();
|
||||
converter.LoadDrawings(Nest.Drawings);
|
||||
|
||||
if (converter.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var newDrawings = converter.GetDrawings();
|
||||
var newByName = newDrawings.ToDictionary(d => d.Name);
|
||||
|
||||
// Update existing drawings in-place so parts keep their BaseDrawing references
|
||||
foreach (var existing in Nest.Drawings.ToList())
|
||||
{
|
||||
if (newByName.TryGetValue(existing.Name, out var updated))
|
||||
{
|
||||
existing.Program = updated.Program;
|
||||
existing.SourceEntities = updated.SourceEntities;
|
||||
existing.SuppressedEntityIds = updated.SuppressedEntityIds;
|
||||
existing.Source = updated.Source;
|
||||
existing.Customer = updated.Customer;
|
||||
existing.Quantity.Required = updated.Quantity.Required;
|
||||
existing.Bends.Clear();
|
||||
existing.Bends.AddRange(updated.Bends);
|
||||
newByName.Remove(existing.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
Nest.Drawings.Remove(existing);
|
||||
}
|
||||
}
|
||||
|
||||
// Add any new drawings that weren't in the original set
|
||||
foreach (var d in newByName.Values)
|
||||
Nest.Drawings.Add(d);
|
||||
|
||||
// Refresh all parts to use the updated programs
|
||||
foreach (var plate in Nest.Plates)
|
||||
foreach (var part in plate.Parts)
|
||||
if (!part.BaseDrawing.IsCutOff)
|
||||
part.Update();
|
||||
|
||||
UpdateDrawingList();
|
||||
PlateView.Invalidate();
|
||||
}
|
||||
|
||||
private void CleanUnusedDrawings_Click(object sender, EventArgs e)
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
@@ -892,6 +949,7 @@ namespace OpenNest.Forms
|
||||
PlateView.Plate = PlateManager.CurrentPlate;
|
||||
PlateView.ZoomToFit();
|
||||
UpdatePlateHeader();
|
||||
UpdateRemovePlateButton();
|
||||
PlateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -955,6 +1013,15 @@ namespace OpenNest.Forms
|
||||
drawingListBox1.Items.RemoveAt(i);
|
||||
}
|
||||
|
||||
foreach (var dwg in Nest.Drawings.OrderBy(d => d.Name))
|
||||
{
|
||||
if (dwg.Quantity.Required > 0 && dwg.Quantity.Remaining == 0)
|
||||
continue;
|
||||
|
||||
if (!drawingListBox1.Items.Contains(dwg))
|
||||
drawingListBox1.Items.Add(dwg);
|
||||
}
|
||||
|
||||
drawingListBox1.EndUpdate();
|
||||
}
|
||||
|
||||
@@ -1016,6 +1083,12 @@ namespace OpenNest.Forms
|
||||
addPart = true;
|
||||
}
|
||||
|
||||
private void PlateView_MouseEnter(object sender, EventArgs e)
|
||||
{
|
||||
if (!PlateView.Focused)
|
||||
PlateView.Focus();
|
||||
}
|
||||
|
||||
private void PlateView_Enter(object sender, EventArgs e)
|
||||
{
|
||||
if (!addPart)
|
||||
|
||||
+937
-589
File diff suppressed because it is too large
Load Diff
Generated
+10
-1
@@ -85,6 +85,7 @@
|
||||
mnuNest = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestImportDrawing = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestShapeLibrary = new System.Windows.Forms.ToolStripMenuItem();
|
||||
toolStripMenuItem7 = new System.Windows.Forms.ToolStripSeparator();
|
||||
mnuNestFirstPlate = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestLastPlate = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -559,7 +560,7 @@
|
||||
//
|
||||
// mnuNest
|
||||
//
|
||||
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
|
||||
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, mnuNestShapeLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
|
||||
mnuNest.Name = "mnuNest";
|
||||
mnuNest.Size = new System.Drawing.Size(43, 20);
|
||||
mnuNest.Text = "&Nest";
|
||||
@@ -579,6 +580,13 @@
|
||||
mnuNestImportDrawing.Text = "Import Drawing";
|
||||
mnuNestImportDrawing.Click += Import_Click;
|
||||
//
|
||||
// mnuNestShapeLibrary
|
||||
//
|
||||
mnuNestShapeLibrary.Name = "mnuNestShapeLibrary";
|
||||
mnuNestShapeLibrary.Size = new System.Drawing.Size(205, 22);
|
||||
mnuNestShapeLibrary.Text = "Shape Library";
|
||||
mnuNestShapeLibrary.Click += ShapeLibrary_Click;
|
||||
//
|
||||
// toolStripMenuItem7
|
||||
//
|
||||
toolStripMenuItem7.Name = "toolStripMenuItem7";
|
||||
@@ -1213,6 +1221,7 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNest;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestEdit;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestImportDrawing;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestShapeLibrary;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem7;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestFirstPlate;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestLastPlate;
|
||||
|
||||
@@ -64,8 +64,8 @@ namespace OpenNest.Forms
|
||||
//if (GpuEvaluatorFactory.GpuAvailable)
|
||||
// BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
|
||||
|
||||
if (GpuEvaluatorFactory.GpuAvailable)
|
||||
BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
||||
//if (GpuEvaluatorFactory.GpuAvailable)
|
||||
// BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
||||
|
||||
var enginesDir = Path.Combine(Application.StartupPath, "Engines");
|
||||
NestEngineRegistry.LoadPlugins(enginesDir);
|
||||
@@ -829,6 +829,20 @@ namespace OpenNest.Forms
|
||||
activeForm.Import();
|
||||
}
|
||||
|
||||
private void ShapeLibrary_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (activeForm == null) return;
|
||||
|
||||
var form = new ShapeLibraryForm();
|
||||
form.ShowDialog();
|
||||
|
||||
var drawings = form.GetDrawings();
|
||||
if (drawings.Count == 0) return;
|
||||
|
||||
drawings.ForEach(d => activeForm.Nest.Drawings.Add(d));
|
||||
activeForm.UpdateDrawingList();
|
||||
}
|
||||
|
||||
private void EditNest_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (activeForm == null) return;
|
||||
@@ -932,6 +946,10 @@ namespace OpenNest.Forms
|
||||
var optimizePlateSize = form.OptimizePlateSize;
|
||||
var plateOptions = optimizePlateSize ? form.GetPlateOptions() : null;
|
||||
var salvageRate = form.SalvageRate;
|
||||
var partFirstMode = form.PartFirstMode;
|
||||
var sortOrder = form.SortOrder;
|
||||
var minRemnantSize = form.MinRemnantSize;
|
||||
var allowPlateCreation = form.AllowPlateCreation;
|
||||
|
||||
if (optimizePlateSize)
|
||||
{
|
||||
@@ -960,7 +978,7 @@ namespace OpenNest.Forms
|
||||
try
|
||||
{
|
||||
await RunAutoNestAsync(items, progressForm, progress, nestingCts.Token,
|
||||
plateOptions, salvageRate);
|
||||
plateOptions, salvageRate, partFirstMode, sortOrder, minRemnantSize, allowPlateCreation);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -984,8 +1002,52 @@ namespace OpenNest.Forms
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token,
|
||||
List<PlateOption> plateOptions = null,
|
||||
double salvageRate = 0.5)
|
||||
double salvageRate = 0.5,
|
||||
bool partFirstMode = false,
|
||||
PartSortOrder sortOrder = PartSortOrder.BoundingBoxArea,
|
||||
double minRemnantSize = 12.0,
|
||||
bool allowPlateCreation = true)
|
||||
{
|
||||
if (partFirstMode)
|
||||
{
|
||||
var existingPlates = new List<Plate>();
|
||||
for (var i = 0; i < activeForm.Nest.Plates.Count; i++)
|
||||
{
|
||||
var p = activeForm.Nest.Plates[i];
|
||||
if (p.Parts.Count > 0)
|
||||
existingPlates.Add(p);
|
||||
}
|
||||
|
||||
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, nestOptions, existingPlates, progress, token));
|
||||
|
||||
foreach (var pr in result.Plates)
|
||||
{
|
||||
if (pr.IsNew)
|
||||
{
|
||||
var plate = GetOrCreatePlate(progressForm);
|
||||
plate.Size = pr.Plate.Size;
|
||||
plate.Parts.AddRange(pr.Parts);
|
||||
}
|
||||
}
|
||||
|
||||
activeForm.Nest.UpdateDrawingQuantities();
|
||||
progressForm.ShowCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
const int maxPlates = 100;
|
||||
|
||||
for (var plateIndex = 0; plateIndex < maxPlates; plateIndex++)
|
||||
|
||||
+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";
|
||||
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
partial class ShapeLibraryForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
ColorScheme colorScheme1 = new ColorScheme();
|
||||
CutOffSettings cutOffSettings1 = new CutOffSettings();
|
||||
Plate plate1 = new Plate();
|
||||
Collections.ObservableList<CutOff> observableList_11 = new Collections.ObservableList<CutOff>();
|
||||
Collections.ObservableList<Part> observableList_12 = new Collections.ObservableList<Part>();
|
||||
splitContainer = new System.Windows.Forms.SplitContainer();
|
||||
shapeListBox = new System.Windows.Forms.ListBox();
|
||||
layoutTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
fieldsTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
nameLabel = new System.Windows.Forms.Label();
|
||||
nameTextBox = new System.Windows.Forms.TextBox();
|
||||
qtyLabel = new System.Windows.Forms.Label();
|
||||
quantityUpDown = new OpenNest.Controls.NumericUpDown();
|
||||
configLabel = new System.Windows.Forms.Label();
|
||||
configComboBox = new System.Windows.Forms.ComboBox();
|
||||
contentPanel = new System.Windows.Forms.Panel();
|
||||
previewBox = new OpenNest.Controls.ShapePreviewControl();
|
||||
parametersPanel = new System.Windows.Forms.Panel();
|
||||
buttonPanel = new System.Windows.Forms.Panel();
|
||||
addButton = new System.Windows.Forms.Button();
|
||||
closeButton = new System.Windows.Forms.Button();
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
|
||||
splitContainer.Panel1.SuspendLayout();
|
||||
splitContainer.Panel2.SuspendLayout();
|
||||
splitContainer.SuspendLayout();
|
||||
layoutTable.SuspendLayout();
|
||||
fieldsTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)quantityUpDown).BeginInit();
|
||||
contentPanel.SuspendLayout();
|
||||
buttonPanel.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// splitContainer
|
||||
//
|
||||
splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1;
|
||||
splitContainer.Location = new System.Drawing.Point(0, 0);
|
||||
splitContainer.Name = "splitContainer";
|
||||
//
|
||||
// splitContainer.Panel1
|
||||
//
|
||||
splitContainer.Panel1.Controls.Add(shapeListBox);
|
||||
//
|
||||
// splitContainer.Panel2
|
||||
//
|
||||
splitContainer.Panel2.Controls.Add(layoutTable);
|
||||
splitContainer.Size = new System.Drawing.Size(750, 520);
|
||||
splitContainer.SplitterDistance = 150;
|
||||
splitContainer.TabIndex = 0;
|
||||
//
|
||||
// shapeListBox
|
||||
//
|
||||
shapeListBox.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||
shapeListBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
shapeListBox.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawFixed;
|
||||
shapeListBox.Font = new System.Drawing.Font("Segoe UI", 10F);
|
||||
shapeListBox.IntegralHeight = false;
|
||||
shapeListBox.ItemHeight = 32;
|
||||
shapeListBox.Location = new System.Drawing.Point(0, 0);
|
||||
shapeListBox.Name = "shapeListBox";
|
||||
shapeListBox.Size = new System.Drawing.Size(150, 520);
|
||||
shapeListBox.TabIndex = 0;
|
||||
//
|
||||
// layoutTable
|
||||
//
|
||||
layoutTable.ColumnCount = 1;
|
||||
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.Controls.Add(fieldsTable, 0, 0);
|
||||
layoutTable.Controls.Add(contentPanel, 0, 1);
|
||||
layoutTable.Controls.Add(buttonPanel, 0, 2);
|
||||
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
layoutTable.Location = new System.Drawing.Point(0, 0);
|
||||
layoutTable.Name = "layoutTable";
|
||||
layoutTable.Padding = new System.Windows.Forms.Padding(6, 4, 6, 0);
|
||||
layoutTable.RowCount = 3;
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 44F));
|
||||
layoutTable.Size = new System.Drawing.Size(596, 520);
|
||||
layoutTable.TabIndex = 0;
|
||||
//
|
||||
// fieldsTable
|
||||
//
|
||||
fieldsTable.AutoSize = true;
|
||||
fieldsTable.ColumnCount = 2;
|
||||
fieldsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
fieldsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
fieldsTable.Controls.Add(nameLabel, 0, 0);
|
||||
fieldsTable.Controls.Add(nameTextBox, 1, 0);
|
||||
fieldsTable.Controls.Add(qtyLabel, 0, 1);
|
||||
fieldsTable.Controls.Add(quantityUpDown, 1, 1);
|
||||
fieldsTable.Controls.Add(configLabel, 0, 2);
|
||||
fieldsTable.Controls.Add(configComboBox, 1, 2);
|
||||
fieldsTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
fieldsTable.Location = new System.Drawing.Point(6, 4);
|
||||
fieldsTable.Margin = new System.Windows.Forms.Padding(0, 0, 0, 4);
|
||||
fieldsTable.Name = "fieldsTable";
|
||||
fieldsTable.RowCount = 3;
|
||||
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
fieldsTable.Size = new System.Drawing.Size(584, 99);
|
||||
fieldsTable.TabIndex = 0;
|
||||
//
|
||||
// nameLabel
|
||||
//
|
||||
nameLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
nameLabel.AutoSize = true;
|
||||
nameLabel.Location = new System.Drawing.Point(4, 8);
|
||||
nameLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
|
||||
nameLabel.Name = "nameLabel";
|
||||
nameLabel.Size = new System.Drawing.Size(46, 17);
|
||||
nameLabel.TabIndex = 0;
|
||||
nameLabel.Text = "Name:";
|
||||
//
|
||||
// nameTextBox
|
||||
//
|
||||
nameTextBox.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
nameTextBox.Location = new System.Drawing.Point(106, 4);
|
||||
nameTextBox.Margin = new System.Windows.Forms.Padding(4);
|
||||
nameTextBox.Name = "nameTextBox";
|
||||
nameTextBox.Size = new System.Drawing.Size(474, 25);
|
||||
nameTextBox.TabIndex = 1;
|
||||
//
|
||||
// qtyLabel
|
||||
//
|
||||
qtyLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
qtyLabel.AutoSize = true;
|
||||
qtyLabel.Location = new System.Drawing.Point(4, 41);
|
||||
qtyLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
|
||||
qtyLabel.Name = "qtyLabel";
|
||||
qtyLabel.Size = new System.Drawing.Size(59, 17);
|
||||
qtyLabel.TabIndex = 2;
|
||||
qtyLabel.Text = "Quantity:";
|
||||
//
|
||||
// quantityUpDown
|
||||
//
|
||||
quantityUpDown.Location = new System.Drawing.Point(106, 37);
|
||||
quantityUpDown.Margin = new System.Windows.Forms.Padding(4);
|
||||
quantityUpDown.Maximum = new decimal(new int[] { 999999, 0, 0, 0 });
|
||||
quantityUpDown.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
quantityUpDown.Name = "quantityUpDown";
|
||||
quantityUpDown.Size = new System.Drawing.Size(100, 25);
|
||||
quantityUpDown.Suffix = "";
|
||||
quantityUpDown.TabIndex = 2;
|
||||
quantityUpDown.Value = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
//
|
||||
// configLabel
|
||||
//
|
||||
configLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
configLabel.AutoSize = true;
|
||||
configLabel.Location = new System.Drawing.Point(4, 74);
|
||||
configLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
|
||||
configLabel.Name = "configLabel";
|
||||
configLabel.Size = new System.Drawing.Size(90, 17);
|
||||
configLabel.TabIndex = 3;
|
||||
configLabel.Text = "Configuration:";
|
||||
configLabel.Visible = false;
|
||||
//
|
||||
// configComboBox
|
||||
//
|
||||
configComboBox.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
configComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
configComboBox.Location = new System.Drawing.Point(106, 70);
|
||||
configComboBox.Margin = new System.Windows.Forms.Padding(4);
|
||||
configComboBox.Name = "configComboBox";
|
||||
configComboBox.Size = new System.Drawing.Size(474, 25);
|
||||
configComboBox.TabIndex = 3;
|
||||
configComboBox.Visible = false;
|
||||
//
|
||||
// contentPanel
|
||||
//
|
||||
contentPanel.Controls.Add(previewBox);
|
||||
contentPanel.Controls.Add(parametersPanel);
|
||||
contentPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
contentPanel.Location = new System.Drawing.Point(9, 110);
|
||||
contentPanel.Name = "contentPanel";
|
||||
contentPanel.Size = new System.Drawing.Size(578, 363);
|
||||
contentPanel.TabIndex = 1;
|
||||
//
|
||||
// previewBox
|
||||
//
|
||||
previewBox.ActiveWorkArea = null;
|
||||
previewBox.AllowPan = false;
|
||||
previewBox.AllowSelect = false;
|
||||
previewBox.AllowZoom = false;
|
||||
previewBox.BackColor = System.Drawing.Color.White;
|
||||
colorScheme1.BackgroundColor = System.Drawing.Color.DarkGray;
|
||||
colorScheme1.BoundingBoxColor = System.Drawing.Color.FromArgb(128, 128, 255);
|
||||
colorScheme1.EdgeSpacingColor = System.Drawing.Color.FromArgb(180, 180, 180);
|
||||
colorScheme1.LayoutFillColor = System.Drawing.Color.WhiteSmoke;
|
||||
colorScheme1.LayoutOutlineColor = System.Drawing.Color.Gray;
|
||||
colorScheme1.OriginColor = System.Drawing.Color.Gray;
|
||||
colorScheme1.PreviewPartColor = System.Drawing.Color.FromArgb(255, 140, 0);
|
||||
colorScheme1.RapidColor = System.Drawing.Color.DodgerBlue;
|
||||
previewBox.ColorScheme = colorScheme1;
|
||||
cutOffSettings1.CutDirection = CutDirection.AwayFromOrigin;
|
||||
cutOffSettings1.MinSegmentLength = 0.05D;
|
||||
cutOffSettings1.Overtravel = 0D;
|
||||
cutOffSettings1.PartClearance = 0.02D;
|
||||
previewBox.CutOffSettings = cutOffSettings1;
|
||||
previewBox.DebugRemnantPriorities = null;
|
||||
previewBox.DebugRemnants = null;
|
||||
previewBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
previewBox.DrawBounds = false;
|
||||
previewBox.DrawCutDirection = false;
|
||||
previewBox.DrawOffset = false;
|
||||
previewBox.DrawOrigin = false;
|
||||
previewBox.DrawPiercePoints = false;
|
||||
previewBox.DrawRapid = false;
|
||||
previewBox.FillParts = true;
|
||||
previewBox.Location = new System.Drawing.Point(0, 0);
|
||||
previewBox.Name = "previewBox";
|
||||
previewBox.OffsetIncrementDistance = 10D;
|
||||
previewBox.OffsetTolerance = 0.001D;
|
||||
plate1.CutOffs = observableList_11;
|
||||
plate1.CuttingParameters = null;
|
||||
plate1.GrainAngle = 0D;
|
||||
plate1.Parts = observableList_12;
|
||||
plate1.PartSpacing = 0D;
|
||||
plate1.Quadrant = 1;
|
||||
plate1.Quantity = 0;
|
||||
previewBox.Plate = plate1;
|
||||
previewBox.RotateIncrementAngle = 10D;
|
||||
previewBox.ShowBendLines = false;
|
||||
previewBox.Size = new System.Drawing.Size(318, 363);
|
||||
previewBox.Status = "Select";
|
||||
previewBox.TabIndex = 4;
|
||||
previewBox.TabStop = false;
|
||||
//
|
||||
// parametersPanel
|
||||
//
|
||||
parametersPanel.AutoScroll = true;
|
||||
parametersPanel.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
parametersPanel.Location = new System.Drawing.Point(318, 0);
|
||||
parametersPanel.Name = "parametersPanel";
|
||||
parametersPanel.Padding = new System.Windows.Forms.Padding(8, 0, 0, 0);
|
||||
parametersPanel.Size = new System.Drawing.Size(260, 363);
|
||||
parametersPanel.TabIndex = 5;
|
||||
//
|
||||
// buttonPanel
|
||||
//
|
||||
buttonPanel.Controls.Add(addButton);
|
||||
buttonPanel.Controls.Add(closeButton);
|
||||
buttonPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
buttonPanel.Location = new System.Drawing.Point(9, 479);
|
||||
buttonPanel.Name = "buttonPanel";
|
||||
buttonPanel.Size = new System.Drawing.Size(578, 38);
|
||||
buttonPanel.TabIndex = 2;
|
||||
//
|
||||
// addButton
|
||||
//
|
||||
addButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
addButton.Location = new System.Drawing.Point(379, 5);
|
||||
addButton.Name = "addButton";
|
||||
addButton.Size = new System.Drawing.Size(100, 30);
|
||||
addButton.TabIndex = 0;
|
||||
addButton.Text = "Add to Nest";
|
||||
addButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// closeButton
|
||||
//
|
||||
closeButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
closeButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
closeButton.Location = new System.Drawing.Point(485, 5);
|
||||
closeButton.Name = "closeButton";
|
||||
closeButton.Size = new System.Drawing.Size(90, 30);
|
||||
closeButton.TabIndex = 1;
|
||||
closeButton.Text = "Close";
|
||||
closeButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// ShapeLibraryForm
|
||||
//
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
CancelButton = closeButton;
|
||||
ClientSize = new System.Drawing.Size(750, 520);
|
||||
Controls.Add(splitContainer);
|
||||
Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
||||
MinimizeBox = false;
|
||||
MinimumSize = new System.Drawing.Size(600, 400);
|
||||
Name = "ShapeLibraryForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Shape Library";
|
||||
splitContainer.Panel1.ResumeLayout(false);
|
||||
splitContainer.Panel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer).EndInit();
|
||||
splitContainer.ResumeLayout(false);
|
||||
layoutTable.ResumeLayout(false);
|
||||
layoutTable.PerformLayout();
|
||||
fieldsTable.ResumeLayout(false);
|
||||
fieldsTable.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)quantityUpDown).EndInit();
|
||||
contentPanel.ResumeLayout(false);
|
||||
buttonPanel.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.SplitContainer splitContainer;
|
||||
private System.Windows.Forms.ListBox shapeListBox;
|
||||
private System.Windows.Forms.TableLayoutPanel layoutTable;
|
||||
private System.Windows.Forms.TableLayoutPanel fieldsTable;
|
||||
private System.Windows.Forms.Label nameLabel;
|
||||
private System.Windows.Forms.TextBox nameTextBox;
|
||||
private System.Windows.Forms.Label qtyLabel;
|
||||
private Controls.NumericUpDown quantityUpDown;
|
||||
private System.Windows.Forms.Label configLabel;
|
||||
private System.Windows.Forms.ComboBox configComboBox;
|
||||
private System.Windows.Forms.Panel contentPanel;
|
||||
private Controls.ShapePreviewControl previewBox;
|
||||
private System.Windows.Forms.Panel parametersPanel;
|
||||
private System.Windows.Forms.Panel buttonPanel;
|
||||
private System.Windows.Forms.Button addButton;
|
||||
private System.Windows.Forms.Button closeButton;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
using OpenNest.Shapes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
public partial class ShapeLibraryForm : Form
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private readonly List<Drawing> addedDrawings = new List<Drawing>();
|
||||
private readonly List<ShapeEntry> shapeEntries = new List<ShapeEntry>();
|
||||
private readonly List<ParameterBinding> parameterBindings = new List<ParameterBinding>();
|
||||
|
||||
private ShapeEntry selectedEntry;
|
||||
private bool suppressPreview;
|
||||
|
||||
public ShapeLibraryForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
DiscoverShapes();
|
||||
PopulateShapeList();
|
||||
|
||||
shapeListBox.DrawItem += ShapeListBox_DrawItem;
|
||||
shapeListBox.SelectedIndexChanged += ShapeListBox_SelectedIndexChanged;
|
||||
configComboBox.SelectedIndexChanged += ConfigComboBox_SelectedIndexChanged;
|
||||
addButton.Click += AddButton_Click;
|
||||
closeButton.Click += (s, e) => Close();
|
||||
|
||||
if (shapeListBox.Items.Count > 0)
|
||||
shapeListBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
public List<Drawing> GetDrawings() => addedDrawings;
|
||||
|
||||
private void DiscoverShapes()
|
||||
{
|
||||
var baseType = typeof(ShapeDefinition);
|
||||
var shapeTypes = baseType.Assembly.GetTypes()
|
||||
.Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t))
|
||||
.OrderBy(t => t.Name)
|
||||
.ToList();
|
||||
|
||||
var configDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configurations");
|
||||
|
||||
foreach (var type in shapeTypes)
|
||||
{
|
||||
var entry = new ShapeEntry { ShapeType = type };
|
||||
entry.DisplayName = FriendlyName(type.Name);
|
||||
|
||||
var configPath = Path.Combine(configDir, type.Name + ".json");
|
||||
if (File.Exists(configPath))
|
||||
entry.Configurations = LoadConfigurations(type, configPath);
|
||||
|
||||
shapeEntries.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ShapeDefinition> LoadConfigurations(Type shapeType, string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
var listType = typeof(List<>).MakeGenericType(shapeType);
|
||||
var list = JsonSerializer.Deserialize(json, listType, JsonOptions);
|
||||
return ((System.Collections.IEnumerable)list).Cast<ShapeDefinition>().ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateShapeList()
|
||||
{
|
||||
foreach (var entry in shapeEntries)
|
||||
shapeListBox.Items.Add(entry);
|
||||
}
|
||||
|
||||
private void ShapeListBox_DrawItem(object sender, DrawItemEventArgs e)
|
||||
{
|
||||
if (e.Index < 0) return;
|
||||
|
||||
e.DrawBackground();
|
||||
|
||||
var entry = (ShapeEntry)shapeListBox.Items[e.Index];
|
||||
var textColor = (e.State & DrawItemState.Selected) != 0
|
||||
? SystemColors.HighlightText
|
||||
: SystemColors.ControlText;
|
||||
|
||||
var text = entry.DisplayName;
|
||||
if (entry.HasConfigurations)
|
||||
text += $" ({entry.Configurations.Count})";
|
||||
|
||||
using (var brush = new SolidBrush(textColor))
|
||||
{
|
||||
var format = new StringFormat { LineAlignment = StringAlignment.Center };
|
||||
var rect = new RectangleF(8, e.Bounds.Y, e.Bounds.Width - 8, e.Bounds.Height);
|
||||
e.Graphics.DrawString(text, e.Font, brush, rect, format);
|
||||
}
|
||||
|
||||
e.DrawFocusRectangle();
|
||||
}
|
||||
|
||||
private void ShapeListBox_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (shapeListBox.SelectedIndex < 0) return;
|
||||
|
||||
selectedEntry = (ShapeEntry)shapeListBox.SelectedItem;
|
||||
suppressPreview = true;
|
||||
|
||||
var hasConfigs = selectedEntry.HasConfigurations;
|
||||
configLabel.Visible = hasConfigs;
|
||||
configComboBox.Visible = hasConfigs;
|
||||
|
||||
if (hasConfigs)
|
||||
{
|
||||
configComboBox.Items.Clear();
|
||||
foreach (var cfg in selectedEntry.Configurations)
|
||||
configComboBox.Items.Add(cfg.Name);
|
||||
|
||||
configComboBox.SelectedIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
nameTextBox.Text = selectedEntry.DisplayName;
|
||||
var defaults = (ShapeDefinition)Activator.CreateInstance(selectedEntry.ShapeType);
|
||||
defaults.SetPreviewDefaults();
|
||||
BuildParameterControls(selectedEntry.ShapeType, defaults);
|
||||
}
|
||||
|
||||
suppressPreview = false;
|
||||
UpdatePreview();
|
||||
}
|
||||
|
||||
private void ConfigComboBox_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (configComboBox.SelectedIndex < 0 || selectedEntry == null) return;
|
||||
|
||||
var config = selectedEntry.Configurations[configComboBox.SelectedIndex];
|
||||
nameTextBox.Text = config.Name;
|
||||
|
||||
suppressPreview = true;
|
||||
BuildParameterControls(selectedEntry.ShapeType, config);
|
||||
suppressPreview = false;
|
||||
UpdatePreview();
|
||||
}
|
||||
|
||||
private void BuildParameterControls(Type shapeType, ShapeDefinition sourceValues)
|
||||
{
|
||||
parametersPanel.SuspendLayout();
|
||||
parametersPanel.Controls.Clear();
|
||||
parameterBindings.Clear();
|
||||
|
||||
var props = shapeType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||
.Where(p => p.CanRead && p.CanWrite && p.Name != "Name")
|
||||
.ToArray();
|
||||
|
||||
var panelWidth = parametersPanel.ClientSize.Width - parametersPanel.Padding.Horizontal;
|
||||
var y = 4;
|
||||
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var label = new Label
|
||||
{
|
||||
Text = FriendlyName(prop.Name),
|
||||
Location = new Point(parametersPanel.Padding.Left, y),
|
||||
AutoSize = true
|
||||
};
|
||||
|
||||
y += 18;
|
||||
|
||||
var tb = new TextBox
|
||||
{
|
||||
Location = new Point(parametersPanel.Padding.Left, y),
|
||||
Width = panelWidth,
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
|
||||
};
|
||||
|
||||
if (sourceValues != null)
|
||||
{
|
||||
if (prop.PropertyType == typeof(int))
|
||||
tb.Text = ((int)prop.GetValue(sourceValues)).ToString();
|
||||
else
|
||||
tb.Text = ((double)prop.GetValue(sourceValues)).ToString("G");
|
||||
}
|
||||
|
||||
tb.TextChanged += (s, ev) => UpdatePreview();
|
||||
|
||||
parameterBindings.Add(new ParameterBinding { Property = prop, Control = tb });
|
||||
|
||||
parametersPanel.Controls.Add(label);
|
||||
parametersPanel.Controls.Add(tb);
|
||||
|
||||
y += 30;
|
||||
}
|
||||
|
||||
parametersPanel.ResumeLayout(true);
|
||||
}
|
||||
|
||||
private void UpdatePreview()
|
||||
{
|
||||
if (suppressPreview || selectedEntry == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var shape = CreateShapeFromInputs();
|
||||
if (shape == null) return;
|
||||
|
||||
var drawing = shape.GetDrawing();
|
||||
previewBox.ShowDrawing(drawing);
|
||||
|
||||
if (drawing?.Program != null)
|
||||
{
|
||||
var bb = drawing.Program.BoundingBox();
|
||||
previewBox.SetInfo(
|
||||
nameTextBox.Text,
|
||||
string.Format("{0:F3} x {1:F3}", bb.Size.Length, bb.Size.Width));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
previewBox.ShowDrawing(null);
|
||||
}
|
||||
}
|
||||
|
||||
private ShapeDefinition CreateShapeFromInputs()
|
||||
{
|
||||
var shape = (ShapeDefinition)Activator.CreateInstance(selectedEntry.ShapeType);
|
||||
shape.Name = nameTextBox.Text;
|
||||
|
||||
foreach (var binding in parameterBindings)
|
||||
{
|
||||
var tb = (TextBox)binding.Control;
|
||||
|
||||
if (binding.Property.PropertyType == typeof(int))
|
||||
{
|
||||
if (int.TryParse(tb.Text, out var intVal))
|
||||
{
|
||||
binding.Property.SetValue(shape, intVal);
|
||||
tb.ForeColor = SystemColors.WindowText;
|
||||
}
|
||||
else
|
||||
{
|
||||
tb.ForeColor = Color.Red;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var val = ArchUnits.GetLengthInches(tb);
|
||||
if (double.IsNaN(val))
|
||||
return null;
|
||||
|
||||
binding.Property.SetValue(shape, val);
|
||||
}
|
||||
}
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
private void AddButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var shape = CreateShapeFromInputs();
|
||||
if (shape == null) return;
|
||||
|
||||
var drawing = shape.GetDrawing();
|
||||
drawing.Color = Drawing.GetNextColor();
|
||||
drawing.Quantity.Required = (int)quantityUpDown.Value;
|
||||
|
||||
addedDrawings.Add(drawing);
|
||||
DialogResult = DialogResult.OK;
|
||||
|
||||
addButton.Text = $"Added ({addedDrawings.Count})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"Failed to create shape: {ex.Message}",
|
||||
"Error",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FriendlyName(string name)
|
||||
{
|
||||
if (name.EndsWith("Shape"))
|
||||
name = name.Substring(0, name.Length - 5);
|
||||
|
||||
return Regex.Replace(name, @"(?<=[a-z0-9])([A-Z])", " $1");
|
||||
}
|
||||
|
||||
private class ShapeEntry
|
||||
{
|
||||
public Type ShapeType { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public List<ShapeDefinition> Configurations { get; set; }
|
||||
public bool HasConfigurations => Configurations != null && Configurations.Count > 0;
|
||||
|
||||
public override string ToString() => DisplayName;
|
||||
}
|
||||
|
||||
private class ParameterBinding
|
||||
{
|
||||
public PropertyInfo Property { get; set; }
|
||||
public Control Control { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -10,6 +10,11 @@
|
||||
<ItemGroup>
|
||||
<Compile Remove="Controls\LayoutViewGL.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Configurations\**\*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user