diff --git a/OpenNest.Core/Drawing.cs b/OpenNest.Core/Drawing.cs index a76ef63..3989e1e 100644 --- a/OpenNest.Core/Drawing.cs +++ b/OpenNest.Core/Drawing.cs @@ -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 Bends { get; set; } = new List(); + /// + /// Complete set of source entities with stable GUIDs. + /// Null when the drawing was created from G-code or an older nest file. + /// + public List SourceEntities { get; set; } + + /// + /// IDs of entities in that are suppressed (hidden). + /// Suppressed entities are excluded from the active Program but preserved for re-enabling. + /// + public HashSet SuppressedEntityIds { get; set; } = new HashSet(); + public double Area { get; protected set; } public void UpdateArea() diff --git a/OpenNest.Core/Geometry/Entity.cs b/OpenNest.Core/Geometry/Entity.cs index 44aa743..bd2e7fe 100644 --- a/OpenNest.Core/Geometry/Entity.cs +++ b/OpenNest.Core/Geometry/Entity.cs @@ -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(); } + /// + /// Unique identifier for this entity, stable across edit sessions. + /// + public Guid Id { get; set; } + /// /// Entity color (resolved from DXF ByLayer/ByBlock to actual color). /// diff --git a/OpenNest.IO/EntitySerializer.cs b/OpenNest.IO/EntitySerializer.cs new file mode 100644 index 0000000..0f71646 --- /dev/null +++ b/OpenNest.IO/EntitySerializer.cs @@ -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 entities, HashSet suppressed) + { + return new EntitySetDto + { + Entities = entities.Select(ToEntityDto).ToList(), + Suppressed = suppressed.Select(id => id.ToString()).ToList() + }; + } + + public static (List entities, HashSet suppressed) FromDto(EntitySetDto dto) + { + var entities = dto.Entities.Select(FromEntityDto).ToList(); + var suppressed = new HashSet(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); + } + } +} diff --git a/OpenNest.IO/NestFormat.cs b/OpenNest.IO/NestFormat.cs index b4e4bfb..08852f9 100644 --- a/OpenNest.IO/NestFormat.cs +++ b/OpenNest.IO/NestFormat.cs @@ -162,6 +162,35 @@ namespace OpenNest.IO public double Cost { get; init; } } + public record EntitySetDto + { + public List Entities { get; init; } = new(); + public List 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; } diff --git a/OpenNest.IO/NestReader.cs b/OpenNest.IO/NestReader.cs index ceee341..8458561 100644 --- a/OpenNest.IO/NestReader.cs +++ b/OpenNest.IO/NestReader.cs @@ -36,7 +36,8 @@ namespace OpenNest.IO var dto = JsonSerializer.Deserialize(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 BuildDrawings(NestDto dto, Dictionary programs) + private Dictionary entities, HashSet suppressed)> ReadEntitySets(int count) + { + var result = new Dictionary, HashSet)>(); + 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(json, JsonOptions); + result[i] = EntitySerializer.FromDto(dto); + } + return result; + } + + private Dictionary BuildDrawings(NestDto dto, Dictionary programs, + Dictionary entities, HashSet suppressed)> entitySets) { var map = new Dictionary(); 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; diff --git a/OpenNest.IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs index 6446474..fffe57d 100644 --- a/OpenNest.IO/NestWriter.cs +++ b/OpenNest.IO/NestWriter.cs @@ -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; diff --git a/OpenNest/Forms/CadConverterForm.cs b/OpenNest/Forms/CadConverterForm.cs index 502a0a7..a5e9c74 100644 --- a/OpenNest/Forms/CadConverterForm.cs +++ b/OpenNest/Forms/CadConverterForm.cs @@ -610,17 +610,31 @@ namespace OpenNest.Forms { foreach (var drawing in drawings) { - var entities = ConvertProgram.ToGeometry(drawing.Program); + List 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(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(); @@ -682,6 +696,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( + (item.Bends ?? new List()) + .Where(b => b.SourceEntity != null) + .Select(b => b.SourceEntity)); + + drawing.SourceEntities = item.Entities + .Where(e => !bendSources.Contains(e)) + .ToList(); + + drawing.SuppressedEntityIds = new HashSet( + drawing.SourceEntities + .Where(e => !(e.Layer.IsVisible && e.IsVisible)) + .Select(e => e.Id)); + drawings.Add(drawing); Thread.Sleep(20); diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index 54ff52d..4fa4b99 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -883,6 +883,8 @@ namespace OpenNest.Forms 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;