Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 786b6e2e88 | |||
| ba89967448 | |||
| b566d984b0 | |||
| c1e6092e83 | |||
| df86d4367b | |||
| 40026ab4dc | |||
| b18a82df7a | |||
| f090a2e299 |
@@ -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>
|
||||
|
||||
@@ -510,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
|
||||
@@ -562,7 +573,16 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Curve-to-curve direct distance.
|
||||
// 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
|
||||
@@ -582,7 +602,7 @@ namespace OpenNest.Geometry
|
||||
|
||||
var d = RayCircleDistance(mcx, mcy, scx, scy, mr + sr, dirX, dirY);
|
||||
|
||||
if (d >= minDist || d == double.MaxValue)
|
||||
if (d >= minDist)
|
||||
continue;
|
||||
|
||||
// For arcs, verify the contact point falls within both arcs' angular ranges.
|
||||
@@ -616,6 +636,31 @@ namespace OpenNest.Geometry
|
||||
return minDist;
|
||||
}
|
||||
|
||||
private static double ArcToLineClosestDistance(
|
||||
List<Entity> arcEntities, List<Entity> lineEntities,
|
||||
double dirX, double dirY, double minDist)
|
||||
{
|
||||
for (var i = 0; i < arcEntities.Count; i++)
|
||||
{
|
||||
if (arcEntities[i] is Arc arc)
|
||||
{
|
||||
for (var j = 0; j < lineEntities.Count; j++)
|
||||
{
|
||||
if (lineEntities[j] is Line line)
|
||||
{
|
||||
var linePt = line.ClosestPointTo(arc.Center);
|
||||
var arcPt = arc.ClosestPointTo(linePt);
|
||||
var d = RayEdgeDistance(arcPt.X, arcPt.Y,
|
||||
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
|
||||
dirX, dirY);
|
||||
if (d < minDist) { minDist = d; if (d <= 0) return 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return minDist;
|
||||
}
|
||||
|
||||
private static double RayEntityDistance(
|
||||
double vx, double vy, Entity entity, double dirX, double dirY)
|
||||
{
|
||||
@@ -695,13 +740,7 @@ namespace OpenNest.Geometry
|
||||
|
||||
private static HashSet<Vector> CollectVertices(List<Line> lines, Vector offset)
|
||||
{
|
||||
var vertices = new HashSet<Vector>();
|
||||
for (var i = 0; i < lines.Count; i++)
|
||||
{
|
||||
vertices.Add(lines[i].pt1 + offset);
|
||||
vertices.Add(lines[i].pt2 + offset);
|
||||
}
|
||||
return vertices;
|
||||
return CollectVertices(ToEdgeArray(lines), offset);
|
||||
}
|
||||
|
||||
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -170,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})";
|
||||
|
||||
@@ -191,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})";
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -610,17 +612,31 @@ namespace OpenNest.Forms
|
||||
{
|
||||
foreach (var drawing in drawings)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(drawing.Program);
|
||||
List<Entity> entities;
|
||||
|
||||
// Re-apply source offset so entities appear in their natural position
|
||||
if (drawing.Source?.Offset != null && drawing.Source.Offset != Vector.Zero)
|
||||
if (drawing.SourceEntities != null)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
entity.Offset(drawing.Source.Offset);
|
||||
}
|
||||
// Use stored entities with stable GUIDs; apply suppression state
|
||||
entities = new List<Entity>(drawing.SourceEntities);
|
||||
|
||||
// Remove rapid traversals — they aren't part of the cut geometry
|
||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||
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();
|
||||
|
||||
@@ -632,6 +648,9 @@ namespace OpenNest.Forms
|
||||
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
|
||||
};
|
||||
@@ -682,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);
|
||||
@@ -704,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -719,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
|
||||
{
|
||||
@@ -782,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
|
||||
{
|
||||
@@ -840,24 +837,34 @@ 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();
|
||||
@@ -874,11 +881,42 @@ namespace OpenNest.Forms
|
||||
if (converter.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var drawings = converter.GetDrawings();
|
||||
var newDrawings = converter.GetDrawings();
|
||||
var newByName = newDrawings.ToDictionary(d => d.Name);
|
||||
|
||||
// Replace all drawings — clear existing and add new ones
|
||||
Nest.Drawings.Clear();
|
||||
drawings.ForEach(d => Nest.Drawings.Add(d));
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user