using System.IO.Compression; using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.IO; using static OpenNest.IO.NestFormat; namespace OpenNest.Tests.IO; /// /// Per-entity role overrides from the CAD converter: an entity can be cut, etched or kept for /// display only regardless of its DXF layer, and the choice survives a save. /// public class EntityRoleTests { private readonly Layer zero = new("0") { IsVisible = true }; private readonly Layer etchLayer = new("ETCH") { IsVisible = true }; private readonly List outline; private readonly Circle hole; private readonly Line markOnEtchLayer; private readonly Line reference; public EntityRoleTests() { outline = new List { new Line(10, 5, 30, 5) { Layer = zero }, new Line(30, 5, 30, 25) { Layer = zero }, new Line(30, 25, 10, 25) { Layer = zero }, new Line(10, 25, 10, 5) { Layer = zero }, }; hole = new Circle(20, 15, 2) { Layer = zero }; markOnEtchLayer = new Line(12, 22, 18, 22) { Layer = etchLayer }; reference = new Line(12, 8, 18, 8) { Layer = zero }; } private List All => outline.Concat(new Entity[] { hole, markOnEtchLayer, reference }).ToList(); private Dictionary Overrides => new() { [markOnEtchLayer.Id] = EntityRole.Cut, [hole.Id] = EntityRole.Etch, [reference.Id] = EntityRole.Display, }; [Fact] public void WithoutOverridesLayersDecideTheRole() { var drawing = Build(null); Assert.Equal(LayerType.Scribe, LayerAt(drawing, new Vector(15, 22))); Assert.Equal(LayerType.Cut, LayerAt(drawing, new Vector(22, 15))); Assert.Equal(LayerType.Cut, LayerAt(drawing, new Vector(15, 8))); Assert.Empty(drawing.EntityRoles); Assert.Empty(EntityRoles.DisplayGeometry(drawing)); } [Fact] public void OverridesDecideTheProgramLayerAndDisplayOnlyStaysOut() { var drawing = Build(Overrides); Assert.Equal(LayerType.Cut, LayerAt(drawing, new Vector(15, 22))); Assert.Equal(LayerType.Scribe, LayerAt(drawing, new Vector(22, 15))); Assert.Null(LayerAt(drawing, new Vector(15, 8))); Assert.Equal(3, drawing.EntityRoles.Count); // The source entities are untouched: the overrides live beside them. Assert.Same(etchLayer, markOnEtchLayer.Layer); Assert.Same(zero, hole.Layer); } [Fact] public void DisplayGeometryIsInTheProgramFrame() { var drawing = Build(Overrides); var display = Assert.IsType(Assert.Single(EntityRoles.DisplayGeometry(drawing))); var offset = drawing.Source.Offset; Assert.Equal(reference.Id, display.Id); Assert.True(display.StartPoint.DistanceTo(reference.StartPoint - offset) < 1e-9); Assert.True(display.EndPoint.DistanceTo(reference.EndPoint - offset) < 1e-9); } [Fact] public void ExcludedDisplayGeometryIsNotShown() { reference.IsVisible = false; var drawing = Build(Overrides); Assert.Contains(reference.Id, drawing.SuppressedEntityIds); Assert.Empty(EntityRoles.DisplayGeometry(drawing)); } [Fact] public void OverridesSurviveASaveAndAreNotUndoneByMarkRepair() { var drawing = Build(Overrides); var text = NestWriter.GetProgramText(drawing.Program); var loaded = Read(Save(drawing)).Drawings.Single(); Assert.Equal(Overrides.OrderBy(r => r.Key), loaded.EntityRoles.OrderBy(r => r.Key)); Assert.Equal(text, NestWriter.GetProgramText(loaded.Program)); Assert.Equal(LayerType.Cut, LayerAt(loaded, new Vector(15, 22))); Assert.Equal(reference.Id, Assert.Single(EntityRoles.DisplayGeometry(loaded)).Id); } [Fact] public void EntitySetWithoutOverridesHasNoRolesKey() { var bytes = Save(Build(null)); using var archive = new ZipArchive(new MemoryStream(bytes)); using var reader = new StreamReader(archive.GetEntry("entities/entities-1")!.Open()); var json = reader.ReadToEnd(); Assert.DoesNotContain("\"roles\"", json); } [Fact] public void UnknownRolesAndIdsAreSkipped() { var id = Guid.NewGuid(); var dto = new EntitySetDto { Roles = new Dictionary { [id.ToString()] = "Display", [Guid.NewGuid().ToString()] = "bevel", ["not-a-guid"] = "etch", }, }; var roles = EntitySerializer.RolesFromDto(dto); Assert.Equal(EntityRole.Display, Assert.Single(roles).Value); Assert.Equal(id, roles.Single().Key); } private Drawing Build(Dictionary? roles) { var entities = All; var result = new CadImportResult { Entities = entities, Bounds = entities.GetBoundingBox(), Name = "roles", }; return CadImporter.BuildDrawing( result, entities.Where(e => e.IsVisible).ToList(), result.Bends, 1, null, editedProgram: null, entityRoles: roles); } // The program layer of the move through a source-frame point, or null when there is none. private static LayerType? LayerAt(Drawing drawing, Vector sourcePoint) { var point = sourcePoint - drawing.Source.Offset; var geometry = ConvertProgram.ToGeometry(drawing.Program) .Where(e => e.Layer != SpecialLayers.Rapid) .FirstOrDefault(e => e.ClosestPointTo(point).DistanceTo(point) < 1e-6); if (geometry == null) return null; if (geometry.Layer == SpecialLayers.Scribe) return LayerType.Scribe; return geometry.Layer == SpecialLayers.Cut ? LayerType.Cut : (LayerType?)LayerType.Display; } private static byte[] Save(Drawing drawing) { var nest = new Nest { Name = "roles" }; nest.Drawings.Add(drawing); using var stream = new MemoryStream(); Assert.True(new NestWriter(nest).Write(stream)); return stream.ToArray(); } private static Nest Read(byte[] bytes) => new NestReader(new MemoryStream(bytes)).Read(); }