Compare commits
14 Commits
5d6e018b81
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 786b6e2e88 | |||
| ba89967448 | |||
| b566d984b0 | |||
| c1e6092e83 | |||
| df86d4367b | |||
| 40026ab4dc | |||
| b18a82df7a | |||
| f090a2e299 | |||
| 55192a4888 | |||
| 7c28a35ad8 | |||
| b2a723ca60 | |||
| 3dca25c601 | |||
| ebc1a5f980 | |||
| b729f92cd6 |
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -104,6 +104,39 @@ 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
|
||||
@@ -117,25 +150,9 @@ namespace OpenNest.Geometry
|
||||
double startAngle, double endAngle, bool reversed,
|
||||
double dirX, double dirY)
|
||||
{
|
||||
// Ray: P = (vx,vy) + t*(dirX,dirY)
|
||||
// Circle: (x-cx)^2 + (y-cy)^2 = r^2
|
||||
var ox = vx - cx;
|
||||
var oy = vy - cy;
|
||||
|
||||
// a = dirX^2 + dirY^2 = 1 for unit direction, but handle general case
|
||||
var a = dirX * dirX + dirY * dirY;
|
||||
var b = 2.0 * (ox * dirX + oy * dirY);
|
||||
var c = ox * ox + oy * oy - r * r;
|
||||
|
||||
var discriminant = b * b - 4.0 * a * c;
|
||||
if (discriminant < 0)
|
||||
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||
return double.MaxValue;
|
||||
|
||||
var sqrtD = System.Math.Sqrt(discriminant);
|
||||
var inv2a = 1.0 / (2.0 * a);
|
||||
var t1 = (-b - sqrtD) * inv2a;
|
||||
var t2 = (-b + sqrtD) * inv2a;
|
||||
|
||||
var best = double.MaxValue;
|
||||
|
||||
if (t1 > -Tolerance.Epsilon)
|
||||
@@ -168,27 +185,13 @@ namespace OpenNest.Geometry
|
||||
double cx, double cy, double r,
|
||||
double dirX, double dirY)
|
||||
{
|
||||
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)
|
||||
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||
return double.MaxValue;
|
||||
|
||||
var sqrtD = System.Math.Sqrt(discriminant);
|
||||
var t = (-b - sqrtD) / (2.0 * a);
|
||||
|
||||
if (t > Tolerance.Epsilon) return t;
|
||||
if (t >= -Tolerance.Epsilon) return 0;
|
||||
|
||||
// First root is behind us, try the second
|
||||
t = (-b + sqrtD) / (2.0 * a);
|
||||
if (t > Tolerance.Epsilon) return t;
|
||||
if (t >= -Tolerance.Epsilon) return 0;
|
||||
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;
|
||||
}
|
||||
@@ -200,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>
|
||||
@@ -265,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)
|
||||
{
|
||||
@@ -289,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)
|
||||
{
|
||||
@@ -342,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);
|
||||
@@ -359,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)
|
||||
{
|
||||
@@ -556,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)
|
||||
{
|
||||
@@ -576,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)
|
||||
{
|
||||
@@ -596,6 +510,17 @@ 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
|
||||
@@ -648,6 +573,91 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -728,6 +738,56 @@ namespace OpenNest.Geometry
|
||||
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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -104,6 +104,9 @@ namespace OpenNest.Engine.BestFit
|
||||
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)
|
||||
@@ -165,12 +168,84 @@ namespace OpenNest.Engine.BestFit
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Curve-to-curve direct distance.
|
||||
// Vertex sampling misses the true contact between two curved entities
|
||||
// when the approach angle doesn't align with a sampled vertex.
|
||||
for (var m = 0; m < movingCurves.Length; m++)
|
||||
{
|
||||
var mc = movingCurves[m];
|
||||
var mcx = mc.Cx + offset.Dx;
|
||||
var mcy = mc.Cy + offset.Dy;
|
||||
|
||||
for (var s = 0; s < stationaryCurves.Length; s++)
|
||||
{
|
||||
var sc = stationaryCurves[s];
|
||||
var d = SpatialQuery.RayCircleDistance(
|
||||
mcx, mcy, sc.Cx, sc.Cy, mc.Radius + sc.Radius, dirX, dirY);
|
||||
|
||||
if (d >= minDist || d == double.MaxValue)
|
||||
continue;
|
||||
|
||||
if (mc.Entity is Arc || sc.Entity is Arc)
|
||||
{
|
||||
var mx = mcx + d * dirX;
|
||||
var my = mcy + d * dirY;
|
||||
var toCx = sc.Cx - mx;
|
||||
var toCy = sc.Cy - my;
|
||||
|
||||
if (mc.Entity is Arc mArc)
|
||||
{
|
||||
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
||||
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sc.Entity is Arc sArc)
|
||||
{
|
||||
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
|
||||
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
}
|
||||
}
|
||||
|
||||
results[i] = minDist;
|
||||
});
|
||||
|
||||
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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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
@@ -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) =>
|
||||
BeginInvoke((Action)(() => FilterChanged?.Invoke(this, EventArgs.Empty)));
|
||||
{
|
||||
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})";
|
||||
|
||||
|
||||
@@ -225,7 +225,6 @@ namespace OpenNest.Controls
|
||||
protected override void OnMouseEnter(EventArgs e)
|
||||
{
|
||||
base.OnMouseEnter(e);
|
||||
if (!Focused) Focus();
|
||||
}
|
||||
|
||||
protected override void OnDragEnter(DragEventArgs drgevent)
|
||||
@@ -249,6 +248,8 @@ namespace OpenNest.Controls
|
||||
|
||||
protected override void OnMouseDown(MouseEventArgs e)
|
||||
{
|
||||
if (!Focused) Focus();
|
||||
|
||||
if (e.Button == MouseButtons.Middle)
|
||||
middleMouseDownPoint = e.Location;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+20
-1
@@ -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();
|
||||
@@ -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;
|
||||
|
||||
+100
-37
@@ -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,29 +837,88 @@ namespace OpenNest.Forms
|
||||
|
||||
var plate = PlateView.Plate;
|
||||
|
||||
// If no cutting parameters exist, initialize from saved settings or defaults
|
||||
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();
|
||||
}
|
||||
}
|
||||
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 { return CuttingParametersSerializer.Deserialize(json); }
|
||||
catch { /* fall through */ }
|
||||
}
|
||||
|
||||
return new CuttingParameters();
|
||||
}
|
||||
|
||||
private static void SaveCuttingParameters(CuttingParameters parameters)
|
||||
{
|
||||
var json = CuttingParametersSerializer.Serialize(parameters);
|
||||
Properties.Settings.Default.CuttingParametersJson = json;
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
|
||||
private void ImportDrawings_Click(object sender, EventArgs e)
|
||||
{
|
||||
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(
|
||||
@@ -1026,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)
|
||||
|
||||
+938
-810
File diff suppressed because it is too large
Load Diff
Generated
+11
-2
@@ -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";
|
||||
@@ -578,7 +579,14 @@
|
||||
mnuNestImportDrawing.Size = new System.Drawing.Size(205, 22);
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+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