Compare commits
8 Commits
55192a4888
...
786b6e2e88
| 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)
|
if (shape.Entities.Count == 1 && shape.Entities[0] is Circle circle)
|
||||||
return circle.Rotation;
|
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)
|
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -90,6 +91,18 @@ namespace OpenNest
|
|||||||
|
|
||||||
public List<Bend> Bends { get; set; } = new List<Bend>();
|
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 double Area { get; protected set; }
|
||||||
|
|
||||||
public void UpdateArea()
|
public void UpdateArea()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
|
||||||
@@ -10,10 +11,16 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
protected Entity()
|
protected Entity()
|
||||||
{
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
Layer = OpenNest.Geometry.Layer.Default;
|
Layer = OpenNest.Geometry.Layer.Default;
|
||||||
boundingBox = new Box();
|
boundingBox = new Box();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier for this entity, stable across edit sessions.
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entity color (resolved from DXF ByLayer/ByBlock to actual color).
|
/// Entity color (resolved from DXF ByLayer/ByBlock to actual color).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -510,6 +510,17 @@ namespace OpenNest.Geometry
|
|||||||
return minDist;
|
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>
|
/// <summary>
|
||||||
/// Computes the minimum translation distance along an arbitrary unit direction
|
/// Computes the minimum translation distance along an arbitrary unit direction
|
||||||
/// before any vertex/edge of movingEntities contacts any vertex/edge of
|
/// 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
|
// The vertex-to-entity approach misses the closest contact between two
|
||||||
// curved entities (circles/arcs) because only a few cardinal vertices are
|
// curved entities (circles/arcs) because only a few cardinal vertices are
|
||||||
// sampled. The true closest contact along the push direction is found by
|
// 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);
|
var d = RayCircleDistance(mcx, mcy, scx, scy, mr + sr, dirX, dirY);
|
||||||
|
|
||||||
if (d >= minDist || d == double.MaxValue)
|
if (d >= minDist)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// For arcs, verify the contact point falls within both arcs' angular ranges.
|
// For arcs, verify the contact point falls within both arcs' angular ranges.
|
||||||
@@ -616,6 +636,31 @@ namespace OpenNest.Geometry
|
|||||||
return minDist;
|
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(
|
private static double RayEntityDistance(
|
||||||
double vx, double vy, Entity entity, double dirX, double dirY)
|
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)
|
private static HashSet<Vector> CollectVertices(List<Line> lines, Vector offset)
|
||||||
{
|
{
|
||||||
var vertices = new HashSet<Vector>();
|
return CollectVertices(ToEdgeArray(lines), offset);
|
||||||
for (var i = 0; i < lines.Count; i++)
|
|
||||||
{
|
|
||||||
vertices.Add(lines[i].pt1 + offset);
|
|
||||||
vertices.Add(lines[i].pt2 + offset);
|
|
||||||
}
|
|
||||||
return vertices;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
|
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
|
||||||
|
|||||||
@@ -190,7 +190,14 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var rotation = Rotation;
|
var rotation = Rotation;
|
||||||
Program = BaseDrawing.Program.Clone() as Program;
|
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>
|
/// <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 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 record BestFitSetDto
|
||||||
{
|
{
|
||||||
public double PlateWidth { get; init; }
|
public double PlateWidth { get; init; }
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ namespace OpenNest.IO
|
|||||||
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
|
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
|
||||||
|
|
||||||
var programs = ReadPrograms(dto.Drawings.Count);
|
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);
|
ReadBestFits(drawingMap);
|
||||||
var nest = BuildNest(dto, drawingMap);
|
var nest = BuildNest(dto, drawingMap);
|
||||||
|
|
||||||
@@ -74,7 +75,25 @@ namespace OpenNest.IO
|
|||||||
return programs;
|
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>();
|
var map = new Dictionary<int, Drawing>();
|
||||||
foreach (var d in dto.Drawings)
|
foreach (var d in dto.Drawings)
|
||||||
@@ -112,6 +131,12 @@ namespace OpenNest.IO
|
|||||||
if (programs.TryGetValue(d.Id, out var pgm))
|
if (programs.TryGetValue(d.Id, out var pgm))
|
||||||
drawing.Program = pgm;
|
drawing.Program = pgm;
|
||||||
|
|
||||||
|
if (entitySets.TryGetValue(d.Id, out var entitySet))
|
||||||
|
{
|
||||||
|
drawing.SourceEntities = entitySet.entities;
|
||||||
|
drawing.SuppressedEntityIds = entitySet.suppressed;
|
||||||
|
}
|
||||||
|
|
||||||
map[d.Id] = drawing;
|
map[d.Id] = drawing;
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
WriteNestJson(zipArchive);
|
WriteNestJson(zipArchive);
|
||||||
WritePrograms(zipArchive);
|
WritePrograms(zipArchive);
|
||||||
|
WriteEntities(zipArchive);
|
||||||
WriteBestFits(zipArchive);
|
WriteBestFits(zipArchive);
|
||||||
|
|
||||||
return true;
|
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)
|
private void WriteDrawing(Stream stream, Drawing drawing)
|
||||||
{
|
{
|
||||||
var program = drawing.Program;
|
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> Entities { get; set; } = new();
|
||||||
public List<Entity> OriginalEntities { get; set; }
|
public List<Entity> OriginalEntities { get; set; }
|
||||||
public List<Bend> Bends { get; set; } = new();
|
public List<Bend> Bends { get; set; } = new();
|
||||||
|
public HashSet<Guid> SuppressedEntityIds { get; set; }
|
||||||
public Box Bounds { get; set; }
|
public Box Bounds { get; set; }
|
||||||
public int EntityCount { get; set; }
|
public int EntityCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,10 +170,11 @@ namespace OpenNest.Controls
|
|||||||
layersList.Items.Clear();
|
layersList.Items.Clear();
|
||||||
var layers = entities
|
var layers = entities
|
||||||
.Where(e => e.Layer != null)
|
.Where(e => e.Layer != null)
|
||||||
.Select(e => e.Layer.Name)
|
.Select(e => e.Layer)
|
||||||
.Distinct();
|
.GroupBy(l => l.Name)
|
||||||
|
.Select(g => g.First());
|
||||||
foreach (var layer in layers)
|
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})";
|
layersPanel.HeaderText = $"Layers ({layersList.Items.Count})";
|
||||||
|
|
||||||
@@ -191,10 +192,10 @@ namespace OpenNest.Controls
|
|||||||
// Line Types
|
// Line Types
|
||||||
lineTypesList.Items.Clear();
|
lineTypesList.Items.Clear();
|
||||||
var lineTypes = entities
|
var lineTypes = entities
|
||||||
.Select(e => e.LineTypeName ?? "Continuous")
|
.GroupBy(e => e.LineTypeName ?? "Continuous")
|
||||||
.Distinct();
|
.Select(g => new { Name = g.Key, Visible = g.Any(e => e.IsVisible) });
|
||||||
foreach (var lt in lineTypes)
|
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})";
|
lineTypesPanel.HeaderText = $"Line Types ({lineTypesList.Items.Count})";
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ namespace OpenNest.Forms
|
|||||||
if (item.Entities.Any(e => e.Layer != null))
|
if (item.Entities.Any(e => e.Layer != null))
|
||||||
item.Entities.ForEach(e => e.Layer.IsVisible = true);
|
item.Entities.ForEach(e => e.Layer.IsVisible = true);
|
||||||
ReHidePromotedEntities(item.Bends);
|
ReHidePromotedEntities(item.Bends);
|
||||||
|
ReHideSuppressedEntities(item);
|
||||||
|
|
||||||
filterPanel.LoadItem(item.Entities, item.Bends);
|
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||||
|
|
||||||
@@ -245,6 +246,7 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
filterPanel.ApplyFilters(item.Entities);
|
filterPanel.ApplyFilters(item.Entities);
|
||||||
ReHidePromotedEntities(item.Bends);
|
ReHidePromotedEntities(item.Bends);
|
||||||
|
SyncSuppressedState(item);
|
||||||
entityView1.Invalidate();
|
entityView1.Invalidate();
|
||||||
staleProgram = true;
|
staleProgram = true;
|
||||||
}
|
}
|
||||||
@@ -610,7 +612,20 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
foreach (var drawing in drawings)
|
foreach (var drawing in drawings)
|
||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(drawing.Program);
|
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
|
// Re-apply source offset so entities appear in their natural position
|
||||||
if (drawing.Source?.Offset != null && drawing.Source.Offset != Vector.Zero)
|
if (drawing.Source?.Offset != null && drawing.Source.Offset != Vector.Zero)
|
||||||
@@ -621,6 +636,7 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
// Remove rapid traversals — they aren't part of the cut geometry
|
// Remove rapid traversals — they aren't part of the cut geometry
|
||||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||||
|
}
|
||||||
|
|
||||||
var bounds = entities.GetBoundingBox();
|
var bounds = entities.GetBoundingBox();
|
||||||
|
|
||||||
@@ -632,6 +648,9 @@ namespace OpenNest.Forms
|
|||||||
Quantity = drawing.Quantity.Required,
|
Quantity = drawing.Quantity.Required,
|
||||||
Customer = drawing.Customer ?? string.Empty,
|
Customer = drawing.Customer ?? string.Empty,
|
||||||
Bends = drawing.Bends?.ToList() ?? new List<Bend>(),
|
Bends = drawing.Bends?.ToList() ?? new List<Bend>(),
|
||||||
|
SuppressedEntityIds = drawing.SuppressedEntityIds.Count > 0
|
||||||
|
? new HashSet<Guid>(drawing.SuppressedEntityIds)
|
||||||
|
: null,
|
||||||
Bounds = bounds,
|
Bounds = bounds,
|
||||||
EntityCount = entities.Count
|
EntityCount = entities.Count
|
||||||
};
|
};
|
||||||
@@ -682,6 +701,22 @@ namespace OpenNest.Forms
|
|||||||
drawing.Program = programEditor.Program;
|
drawing.Program = programEditor.Program;
|
||||||
else
|
else
|
||||||
drawing.Program = pgm;
|
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);
|
drawings.Add(drawing);
|
||||||
|
|
||||||
Thread.Sleep(20);
|
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();
|
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;
|
var plate = PlateView.Plate;
|
||||||
|
|
||||||
if (plate.CuttingParameters == null)
|
var parameters = LoadOrDefaultParameters(plate.CuttingParameters);
|
||||||
{
|
|
||||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
using var dlg = new CuttingParametersDialog();
|
||||||
if (!string.IsNullOrEmpty(json))
|
dlg.LoadParameters(parameters);
|
||||||
{
|
|
||||||
try { plate.CuttingParameters = CuttingParametersSerializer.Deserialize(json); }
|
if (dlg.ShowDialog() != DialogResult.OK)
|
||||||
catch { plate.CuttingParameters = new CuttingParameters(); }
|
return;
|
||||||
}
|
|
||||||
else
|
parameters = dlg.GetParameters();
|
||||||
{
|
plate.CuttingParameters = parameters;
|
||||||
plate.CuttingParameters = new CuttingParameters();
|
SaveCuttingParameters(parameters);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var assigner = new LeadInAssigner
|
var assigner = new LeadInAssigner
|
||||||
{
|
{
|
||||||
@@ -782,17 +780,16 @@ namespace OpenNest.Forms
|
|||||||
if (Nest == null)
|
if (Nest == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
CuttingParameters parameters;
|
var parameters = LoadOrDefaultParameters(PlateView?.Plate?.CuttingParameters);
|
||||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
|
||||||
if (!string.IsNullOrEmpty(json))
|
using var dlg = new CuttingParametersDialog();
|
||||||
{
|
dlg.LoadParameters(parameters);
|
||||||
try { parameters = CuttingParametersSerializer.Deserialize(json); }
|
|
||||||
catch { parameters = new CuttingParameters(); }
|
if (dlg.ShowDialog() != DialogResult.OK)
|
||||||
}
|
return;
|
||||||
else
|
|
||||||
{
|
parameters = dlg.GetParameters();
|
||||||
parameters = new CuttingParameters();
|
SaveCuttingParameters(parameters);
|
||||||
}
|
|
||||||
|
|
||||||
var assigner = new LeadInAssigner
|
var assigner = new LeadInAssigner
|
||||||
{
|
{
|
||||||
@@ -840,22 +837,32 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
var plate = PlateView.Plate;
|
var plate = PlateView.Plate;
|
||||||
|
|
||||||
// If no cutting parameters exist, initialize from saved settings or defaults
|
|
||||||
if (plate.CuttingParameters == null)
|
if (plate.CuttingParameters == null)
|
||||||
|
plate.CuttingParameters = LoadOrDefaultParameters(null);
|
||||||
|
|
||||||
|
PlateView.SetAction(typeof(Actions.ActionLeadIn));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CuttingParameters LoadOrDefaultParameters(CuttingParameters existing)
|
||||||
{
|
{
|
||||||
|
if (existing != null)
|
||||||
|
return existing;
|
||||||
|
|
||||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
var json = Properties.Settings.Default.CuttingParametersJson;
|
||||||
if (!string.IsNullOrEmpty(json))
|
if (!string.IsNullOrEmpty(json))
|
||||||
{
|
{
|
||||||
try { plate.CuttingParameters = CuttingParametersSerializer.Deserialize(json); }
|
try { return CuttingParametersSerializer.Deserialize(json); }
|
||||||
catch { plate.CuttingParameters = new CuttingParameters(); }
|
catch { /* fall through */ }
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
plate.CuttingParameters = new CuttingParameters();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PlateView.SetAction(typeof(Actions.ActionLeadIn));
|
return new CuttingParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveCuttingParameters(CuttingParameters parameters)
|
||||||
|
{
|
||||||
|
var json = CuttingParametersSerializer.Serialize(parameters);
|
||||||
|
Properties.Settings.Default.CuttingParametersJson = json;
|
||||||
|
Properties.Settings.Default.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ImportDrawings_Click(object sender, EventArgs e)
|
private void ImportDrawings_Click(object sender, EventArgs e)
|
||||||
@@ -874,11 +881,42 @@ namespace OpenNest.Forms
|
|||||||
if (converter.ShowDialog() != DialogResult.OK)
|
if (converter.ShowDialog() != DialogResult.OK)
|
||||||
return;
|
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
|
// Update existing drawings in-place so parts keep their BaseDrawing references
|
||||||
Nest.Drawings.Clear();
|
foreach (var existing in Nest.Drawings.ToList())
|
||||||
drawings.ForEach(d => Nest.Drawings.Add(d));
|
{
|
||||||
|
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)
|
private void CleanUnusedDrawings_Click(object sender, EventArgs e)
|
||||||
|
|||||||
Reference in New Issue
Block a user