feat: add entity-based suppression with stable GUIDs
Entities now have a Guid Id for stable identity across edit sessions. Drawing stores the full source entity set (SourceEntities) and a set of suppressed entity IDs (SuppressedEntityIds), replacing the previous SuppressedProgram approach. Unchecked entities in the converter are suppressed rather than permanently removed, so they can be re-enabled when editing drawings again. Entities are serialized as JSON in the nest file (entities/entities-N) alongside the existing G-code programs. Backward compatible with older nest files that lack entity data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
139
OpenNest.IO/EntitySerializer.cs
Normal file
139
OpenNest.IO/EntitySerializer.cs
Normal file
@@ -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;
|
||||||
|
|||||||
@@ -610,17 +610,31 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
foreach (var drawing in drawings)
|
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.SourceEntities != null)
|
||||||
if (drawing.Source?.Offset != null && drawing.Source.Offset != Vector.Zero)
|
|
||||||
{
|
{
|
||||||
foreach (var entity in entities)
|
// Use stored entities with stable GUIDs; apply suppression state
|
||||||
entity.Offset(drawing.Source.Offset);
|
entities = new List<Entity>(drawing.SourceEntities);
|
||||||
}
|
|
||||||
|
|
||||||
// Remove rapid traversals — they aren't part of the cut geometry
|
foreach (var entity in entities)
|
||||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
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 bounds = entities.GetBoundingBox();
|
||||||
|
|
||||||
@@ -682,6 +696,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);
|
||||||
|
|||||||
@@ -883,6 +883,8 @@ namespace OpenNest.Forms
|
|||||||
if (newByName.TryGetValue(existing.Name, out var updated))
|
if (newByName.TryGetValue(existing.Name, out var updated))
|
||||||
{
|
{
|
||||||
existing.Program = updated.Program;
|
existing.Program = updated.Program;
|
||||||
|
existing.SourceEntities = updated.SourceEntities;
|
||||||
|
existing.SuppressedEntityIds = updated.SuppressedEntityIds;
|
||||||
existing.Source = updated.Source;
|
existing.Source = updated.Source;
|
||||||
existing.Customer = updated.Customer;
|
existing.Customer = updated.Customer;
|
||||||
existing.Quantity.Required = updated.Quantity.Required;
|
existing.Quantity.Required = updated.Quantity.Required;
|
||||||
|
|||||||
Reference in New Issue
Block a user