From 2db8c49838fa5037356544199b9fcffc7a0dbd80 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 28 Mar 2026 00:42:49 -0400 Subject: [PATCH] feat: add etch mark entities from bend lines to CNC program pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Etch marks for up bends are now real geometry entities on an ETCH layer instead of being drawn dynamically. They flow through the full pipeline: entities → FilterPanel layers → ConvertGeometry (tagged as Scribe) → post-processor sequencing before cut geometry. Also includes ShapeProfile normalization (CW perimeter, CCW cutouts) applied consistently across all import paths, and inward offset support for cutout shapes in overlap/offset polygon calculations. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Api/NestRunner.cs | 3 +- OpenNest.Console/Program.cs | 3 +- OpenNest.Core/Bending/Bend.cs | 56 ++++++++++++ OpenNest.Core/Converters/ConvertGeometry.cs | 5 +- OpenNest.Core/Geometry/Shape.cs | 35 ++++++++ OpenNest.Core/Geometry/ShapeProfile.cs | 48 ++++++++++ OpenNest.Core/PartGeometry.cs | 98 +++++++++++++-------- OpenNest.Engine/BestFit/PolygonHelper.cs | 13 +-- OpenNest.Mcp/Tools/InputTools.cs | 4 +- OpenNest.Training/Program.cs | 3 +- OpenNest/Controls/EntityView.cs | 42 --------- OpenNest/Controls/PlateView.cs | 53 +++++++++++ OpenNest/Forms/BomImportForm.cs | 3 +- OpenNest/Forms/CadConverterForm.cs | 43 +++------ OpenNest/LayoutPart.cs | 44 ++++----- 15 files changed, 306 insertions(+), 147 deletions(-) diff --git a/OpenNest.Api/NestRunner.cs b/OpenNest.Api/NestRunner.cs index ccca7bd..f48ba2c 100644 --- a/OpenNest.Api/NestRunner.cs +++ b/OpenNest.Api/NestRunner.cs @@ -35,7 +35,8 @@ public static class NestRunner if (!importer.GetGeometry(part.DxfPath, out var geometry) || geometry.Count == 0) throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}"); - var pgm = ConvertGeometry.ToProgram(geometry); + var normalized = ShapeProfile.NormalizeEntities(geometry); + var pgm = ConvertGeometry.ToProgram(normalized); var name = Path.GetFileNameWithoutExtension(part.DxfPath); var drawing = new Drawing(name); drawing.Program = pgm; diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs index 3d5dfa6..18fb896 100644 --- a/OpenNest.Console/Program.cs +++ b/OpenNest.Console/Program.cs @@ -255,7 +255,8 @@ static class NestConsole return null; } - var pgm = ConvertGeometry.ToProgram(geometry); + var normalized = ShapeProfile.NormalizeEntities(geometry); + var pgm = ConvertGeometry.ToProgram(normalized); if (pgm == null) { diff --git a/OpenNest.Core/Bending/Bend.cs b/OpenNest.Core/Bending/Bend.cs index 98114da..a1ef2d3 100644 --- a/OpenNest.Core/Bending/Bend.cs +++ b/OpenNest.Core/Bending/Bend.cs @@ -1,10 +1,20 @@ using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; +using System.Drawing; namespace OpenNest.Bending { public class Bend { + public static readonly Layer EtchLayer = new Layer("ETCH") + { + Color = Color.Green, + IsVisible = true + }; + + private const double DefaultEtchLength = 1.0; + public Vector StartPoint { get; set; } public Vector EndPoint { get; set; } public BendDirection Direction { get; set; } @@ -29,6 +39,52 @@ namespace OpenNest.Bending /// public double LineAngle => StartPoint.AngleTo(EndPoint); + /// + /// Generates etch mark entities for this bend (up bends only). + /// Returns 1" dashes at each end of the bend line, or the full line if shorter than 3". + /// + public List GetEtchEntities(double etchLength = DefaultEtchLength) + { + var result = new List(); + if (Direction != BendDirection.Up) + return result; + + var length = Length; + + if (length < etchLength * 3.0) + { + result.Add(CreateEtchLine(StartPoint, EndPoint)); + } + else + { + var angle = StartPoint.AngleTo(EndPoint); + var dx = System.Math.Cos(angle) * etchLength; + var dy = System.Math.Sin(angle) * etchLength; + + result.Add(CreateEtchLine(StartPoint, new Vector(StartPoint.X + dx, StartPoint.Y + dy))); + result.Add(CreateEtchLine(new Vector(EndPoint.X - dx, EndPoint.Y - dy), EndPoint)); + } + + return result; + } + + /// + /// Removes existing etch entities from the list and regenerates from the given bends. + /// + public static void UpdateEtchEntities(List entities, List bends) + { + entities.RemoveAll(e => e.Layer == EtchLayer); + if (bends == null) return; + + foreach (var bend in bends) + entities.AddRange(bend.GetEtchEntities()); + } + + private static Line CreateEtchLine(Vector start, Vector end) + { + return new Line(start, end) { Layer = EtchLayer, Color = Color.Green }; + } + public override string ToString() { var dir = Direction.ToString(); diff --git a/OpenNest.Core/Converters/ConvertGeometry.cs b/OpenNest.Core/Converters/ConvertGeometry.cs index 8c89945..25c616a 100644 --- a/OpenNest.Core/Converters/ConvertGeometry.cs +++ b/OpenNest.Core/Converters/ConvertGeometry.cs @@ -108,7 +108,10 @@ namespace OpenNest.Converters if (line.StartPoint != lastpt) pgm.MoveTo(line.StartPoint); - pgm.LineTo(line.EndPoint); + var move = new LinearMove(line.EndPoint); + if (string.Equals(line.Layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase)) + move.Layer = LayerType.Scribe; + pgm.Codes.Add(move); lastpt = line.EndPoint; return lastpt; diff --git a/OpenNest.Core/Geometry/Shape.cs b/OpenNest.Core/Geometry/Shape.cs index 3f984f8..52190c8 100644 --- a/OpenNest.Core/Geometry/Shape.cs +++ b/OpenNest.Core/Geometry/Shape.cs @@ -598,6 +598,41 @@ namespace OpenNest.Geometry return result; } + /// + /// Offsets the shape inward by the given distance. + /// Normalizes to CCW winding before offsetting Left (which is inward for CCW), + /// making the method independent of the original contour winding direction. + /// + public Shape OffsetInward(double distance) + { + var poly = ToPolygon(); + + if (poly == null || poly.Vertices.Count < 3 + || poly.RotationDirection() == RotationType.CCW) + return OffsetEntity(distance, OffsetSide.Left) as Shape; + + // Create a reversed copy to avoid mutating shared entity objects. + var copy = new Shape(); + + for (var i = Entities.Count - 1; i >= 0; i--) + { + switch (Entities[i]) + { + case Line l: + copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer }); + break; + case Arc a: + copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer }); + break; + case Circle c: + copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer }); + break; + } + } + + return copy.OffsetEntity(distance, OffsetSide.Left) as Shape; + } + /// /// Gets the closest point on the shape to the given point. /// diff --git a/OpenNest.Core/Geometry/ShapeProfile.cs b/OpenNest.Core/Geometry/ShapeProfile.cs index 8c1ce33..23928e6 100644 --- a/OpenNest.Core/Geometry/ShapeProfile.cs +++ b/OpenNest.Core/Geometry/ShapeProfile.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; namespace OpenNest.Geometry { @@ -41,5 +42,52 @@ namespace OpenNest.Geometry public Shape Perimeter { get; set; } public List Cutouts { get; set; } + + /// + /// Ensures CNC-standard winding: perimeter CW (kerf left = outward), + /// cutouts CCW (kerf left = inward). Reverses contours in-place as needed. + /// + public void NormalizeWinding() + { + EnsureWinding(Perimeter, RotationType.CW); + + foreach (var cutout in Cutouts) + EnsureWinding(cutout, RotationType.CCW); + } + + /// + /// Returns the entities in normalized winding order (perimeter first, then cutouts). + /// + public List ToNormalizedEntities() + { + NormalizeWinding(); + var result = new List(Perimeter.Entities); + + foreach (var cutout in Cutouts) + result.AddRange(cutout.Entities); + + return result; + } + + /// + /// Convenience method: builds a ShapeProfile from raw entities, + /// normalizes winding, and returns the corrected entity list. + /// + public static List NormalizeEntities(IEnumerable entities) + { + var profile = new ShapeProfile(entities.ToList()); + return profile.ToNormalizedEntities(); + } + + private static void EnsureWinding(Shape shape, RotationType desired) + { + var poly = shape.ToPolygon(); + + if (poly != null && poly.Vertices.Count >= 3 + && poly.RotationDirection() != desired) + { + shape.Reverse(); + } + } } } diff --git a/OpenNest.Core/PartGeometry.cs b/OpenNest.Core/PartGeometry.cs index 7d98d9f..43bdfb4 100644 --- a/OpenNest.Core/PartGeometry.cs +++ b/OpenNest.Core/PartGeometry.cs @@ -42,23 +42,17 @@ namespace OpenNest public static List GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001) { var entities = ConvertProgram.ToGeometry(part.Program); - var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid)); + var profile = new ShapeProfile( + entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); var lines = new List(); + var totalSpacing = spacing + chordTolerance; - foreach (var shape in shapes) - { - // Add chord tolerance to compensate for inscribed polygon chords - // being inside the actual offset arcs. - var offsetEntity = shape.OffsetOutward(spacing + chordTolerance); + AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing), + chordTolerance, part.Location); - if (offsetEntity == null) - continue; - - var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance); - polygon.RemoveSelfIntersections(); - polygon.Offset(part.Location); - lines.AddRange(polygon.ToLines()); - } + foreach (var cutout in profile.Cutouts) + AddOffsetLines(lines, cutout.OffsetInward(totalSpacing), + chordTolerance, part.Location); return lines; } @@ -66,21 +60,17 @@ namespace OpenNest public static List GetOffsetPartLines(Part part, double spacing, PushDirection facingDirection, double chordTolerance = 0.001) { var entities = ConvertProgram.ToGeometry(part.Program); - var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid)); + var profile = new ShapeProfile( + entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); var lines = new List(); + var totalSpacing = spacing + chordTolerance; - foreach (var shape in shapes) - { - var offsetEntity = shape.OffsetOutward(spacing + chordTolerance); + AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing), + chordTolerance, part.Location, facingDirection); - if (offsetEntity == null) - continue; - - var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance); - polygon.RemoveSelfIntersections(); - polygon.Offset(part.Location); - lines.AddRange(GetDirectionalLines(polygon, facingDirection)); - } + foreach (var cutout in profile.Cutouts) + AddOffsetDirectionalLines(lines, cutout.OffsetInward(totalSpacing), + chordTolerance, part.Location, facingDirection); return lines; } @@ -104,21 +94,17 @@ namespace OpenNest public static List GetOffsetPartLines(Part part, double spacing, Vector facingDirection, double chordTolerance = 0.001) { var entities = ConvertProgram.ToGeometry(part.Program); - var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid)); + var profile = new ShapeProfile( + entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); var lines = new List(); + var totalSpacing = spacing + chordTolerance; - foreach (var shape in shapes) - { - var offsetEntity = shape.OffsetOutward(spacing + chordTolerance); + AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing), + chordTolerance, part.Location, facingDirection); - if (offsetEntity == null) - continue; - - var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance); - polygon.RemoveSelfIntersections(); - polygon.Offset(part.Location); - lines.AddRange(GetDirectionalLines(polygon, facingDirection)); - } + foreach (var cutout in profile.Cutouts) + AddOffsetDirectionalLines(lines, cutout.OffsetInward(totalSpacing), + chordTolerance, part.Location, facingDirection); return lines; } @@ -189,5 +175,41 @@ namespace OpenNest return lines; } + + private static void AddOffsetLines(List lines, Shape offsetEntity, + double chordTolerance, Vector location) + { + if (offsetEntity == null) + return; + + var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance); + polygon.RemoveSelfIntersections(); + polygon.Offset(location); + lines.AddRange(polygon.ToLines()); + } + + private static void AddOffsetDirectionalLines(List lines, Shape offsetEntity, + double chordTolerance, Vector location, PushDirection facingDirection) + { + if (offsetEntity == null) + return; + + var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance); + polygon.RemoveSelfIntersections(); + polygon.Offset(location); + lines.AddRange(GetDirectionalLines(polygon, facingDirection)); + } + + private static void AddOffsetDirectionalLines(List lines, Shape offsetEntity, + double chordTolerance, Vector location, Vector facingDirection) + { + if (offsetEntity == null) + return; + + var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance); + polygon.RemoveSelfIntersections(); + polygon.Offset(location); + lines.AddRange(GetDirectionalLines(polygon, facingDirection)); + } } } diff --git a/OpenNest.Engine/BestFit/PolygonHelper.cs b/OpenNest.Engine/BestFit/PolygonHelper.cs index e803912..087cd97 100644 --- a/OpenNest.Engine/BestFit/PolygonHelper.cs +++ b/OpenNest.Engine/BestFit/PolygonHelper.cs @@ -22,18 +22,11 @@ namespace OpenNest.Engine.BestFit if (perimeter == null) return new PolygonExtractionResult(null, Vector.Zero); - // Inflate by half-spacing if spacing is non-zero. - // Detect winding direction to choose the correct outward offset side. - var outwardSide = OffsetSide.Right; - if (halfSpacing > 0) - { - var testPoly = perimeter.ToPolygon(); - if (testPoly.Vertices.Count >= 3 && testPoly.RotationDirection() == RotationType.CW) - outwardSide = OffsetSide.Left; - } + // Ensure CW winding for correct outward offset direction. + definedShape.NormalizeWinding(); var inflated = halfSpacing > 0 - ? (perimeter.OffsetEntity(halfSpacing, outwardSide) as Shape ?? perimeter) + ? (perimeter.OffsetOutward(halfSpacing) ?? perimeter) : perimeter; // Convert to polygon with circumscribed arcs for tight nesting. diff --git a/OpenNest.Mcp/Tools/InputTools.cs b/OpenNest.Mcp/Tools/InputTools.cs index 4bf5000..d06ce18 100644 --- a/OpenNest.Mcp/Tools/InputTools.cs +++ b/OpenNest.Mcp/Tools/InputTools.cs @@ -1,5 +1,6 @@ using ModelContextProtocol.Server; using OpenNest.Converters; +using OpenNest.Geometry; using OpenNest.IO; using OpenNest.Shapes; using System.ComponentModel; @@ -103,7 +104,8 @@ namespace OpenNest.Mcp.Tools if (geometry.Count == 0) return "Error: no geometry found in DXF file"; - var pgm = ConvertGeometry.ToProgram(geometry); + var normalized = ShapeProfile.NormalizeEntities(geometry); + var pgm = ConvertGeometry.ToProgram(normalized); if (pgm == null) return "Error: failed to convert geometry to program"; diff --git a/OpenNest.Training/Program.cs b/OpenNest.Training/Program.cs index a584169..7e4e8fe 100644 --- a/OpenNest.Training/Program.cs +++ b/OpenNest.Training/Program.cs @@ -151,7 +151,8 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin } var drawing = new Drawing(Path.GetFileName(file)); - drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(entities); + var normalized = ShapeProfile.NormalizeEntities(entities); + drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(normalized); drawing.UpdateArea(); drawing.Color = PartColors[colorIndex % PartColors.Length]; colorIndex++; diff --git a/OpenNest/Controls/EntityView.cs b/OpenNest/Controls/EntityView.cs index 9932dc2..d27231d 100644 --- a/OpenNest/Controls/EntityView.cs +++ b/OpenNest/Controls/EntityView.cs @@ -112,8 +112,6 @@ namespace OpenNest.Controls DrawEntity(e.Graphics, entity, pen); } - DrawEtchMarks(e.Graphics); - if (SimplifierPreview != null) { // Draw tolerance zone (offset lines each side of original geometry) @@ -240,46 +238,6 @@ namespace OpenNest.Controls private static bool IsEtchLayer(Layer layer) => string.Equals(layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase); - private void DrawEtchMarks(Graphics g) - { - if (Bends == null || Bends.Count == 0) - return; - - using var etchPen = new Pen(Color.Green, 1.5f); - var etchLength = 1.0; - - foreach (var bend in Bends) - { - if (bend.Direction != BendDirection.Up) - continue; - - var start = bend.StartPoint; - var end = bend.EndPoint; - var length = bend.Length; - - if (length < etchLength * 3.0) - { - var pt1 = PointWorldToGraph(start); - var pt2 = PointWorldToGraph(end); - g.DrawLine(etchPen, pt1, pt2); - } - else - { - var angle = start.AngleTo(end); - var dx = System.Math.Cos(angle) * etchLength; - var dy = System.Math.Sin(angle) * etchLength; - - var s1 = PointWorldToGraph(start); - var e1 = PointWorldToGraph(new Vector(start.X + dx, start.Y + dy)); - g.DrawLine(etchPen, s1, e1); - - var s2 = PointWorldToGraph(end); - var e2 = PointWorldToGraph(new Vector(end.X - dx, end.Y - dy)); - g.DrawLine(etchPen, s2, e2); - } - } - } - private void DrawBendLines(Graphics g) { if (Bends == null || Bends.Count == 0) diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index 0971bd2..797167e 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -584,6 +584,7 @@ namespace OpenNest.Controls part.Draw(g, (i + 1).ToString()); DrawBendLines(g, part.BasePart); + DrawEtchMarks(g, part.BasePart); DrawGrainWarning(g, part.BasePart); } @@ -657,6 +658,58 @@ namespace OpenNest.Controls } } + private void DrawEtchMarks(Graphics g, Part part) + { + if (!ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0) + return; + + using var etchPen = new Pen(Color.Green, 1.5f); + var etchLength = 1.0; + + foreach (var bend in part.BaseDrawing.Bends) + { + if (bend.Direction != BendDirection.Up) + continue; + + var start = bend.StartPoint; + var end = bend.EndPoint; + + // Apply part rotation + if (part.Rotation != 0) + { + start = start.Rotate(part.Rotation); + end = end.Rotate(part.Rotation); + } + + // Apply part offset + start = start + part.Location; + end = end + part.Location; + + var length = bend.Length; + var angle = bend.StartPoint.AngleTo(bend.EndPoint) + part.Rotation; + + if (length < etchLength * 3.0) + { + var pt1 = PointWorldToGraph(start); + var pt2 = PointWorldToGraph(end); + g.DrawLine(etchPen, pt1, pt2); + } + else + { + var dx = System.Math.Cos(angle) * etchLength; + var dy = System.Math.Sin(angle) * etchLength; + + var s1 = PointWorldToGraph(start); + var e1 = PointWorldToGraph(new Vector(start.X + dx, start.Y + dy)); + g.DrawLine(etchPen, s1, e1); + + var s2 = PointWorldToGraph(end); + var e2 = PointWorldToGraph(new Vector(end.X - dx, end.Y - dy)); + g.DrawLine(etchPen, s2, e2); + } + } + } + private void DrawGrainWarning(Graphics g, Part part) { if (!ShowBendLines || Plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0) diff --git a/OpenNest/Forms/BomImportForm.cs b/OpenNest/Forms/BomImportForm.cs index 9d33466..23f5003 100644 --- a/OpenNest/Forms/BomImportForm.cs +++ b/OpenNest/Forms/BomImportForm.cs @@ -424,7 +424,8 @@ namespace OpenNest.Forms drawing.Quantity.Required = part.Qty ?? 1; drawing.Material = new Material(material); - var pgm = ConvertGeometry.ToProgram(result.Entities); + var normalized = ShapeProfile.NormalizeEntities(result.Entities); + var pgm = ConvertGeometry.ToProgram(normalized); if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove) { diff --git a/OpenNest/Forms/CadConverterForm.cs b/OpenNest/Forms/CadConverterForm.cs index c09f33b..77ea09f 100644 --- a/OpenNest/Forms/CadConverterForm.cs +++ b/OpenNest/Forms/CadConverterForm.cs @@ -81,6 +81,8 @@ namespace OpenNest.Forms ?? new List(); } + Bend.UpdateEtchEntities(result.Entities, bends); + var item = new FileListItem { Name = Path.GetFileNameWithoutExtension(file), @@ -245,6 +247,9 @@ namespace OpenNest.Forms bend.SourceEntity.IsVisible = true; item.Bends.RemoveAt(index); + Bend.UpdateEtchEntities(item.Entities, item.Bends); + entityView1.Entities.Clear(); + entityView1.Entities.AddRange(item.Entities); entityView1.Bends = item.Bends; entityView1.SelectedBendIndex = -1; filterPanel.LoadItem(item.Entities, item.Bends); @@ -281,16 +286,8 @@ namespace OpenNest.Forms var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList(); if (entities.Count == 0) return; - var shape = new ShapeProfile(entities); - SetRotation(shape.Perimeter, RotationType.CW); - foreach (var cutout in shape.Cutouts) - SetRotation(cutout, RotationType.CCW); - - var drawEntities = new List(); - drawEntities.AddRange(shape.Perimeter.Entities); - shape.Cutouts.ForEach(c => drawEntities.AddRange(c.Entities)); - - var pgm = ConvertGeometry.ToProgram(drawEntities); + var normalized = ShapeProfile.NormalizeEntities(entities); + var pgm = ConvertGeometry.ToProgram(normalized); var originOffset = Vector.Zero; if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove) { @@ -395,6 +392,9 @@ namespace OpenNest.Forms line.IsVisible = false; item.Bends.Add(bend); + Bend.UpdateEtchEntities(item.Entities, item.Bends); + entityView1.Entities.Clear(); + entityView1.Entities.AddRange(item.Entities); entityView1.Bends = item.Bends; filterPanel.LoadItem(item.Entities, item.Bends); entityView1.Invalidate(); @@ -560,18 +560,8 @@ namespace OpenNest.Forms if (item.Bends != null) drawing.Bends.AddRange(item.Bends); - var shape = new ShapeProfile(entities); - - SetRotation(shape.Perimeter, RotationType.CW); - - foreach (var cutout in shape.Cutouts) - SetRotation(cutout, RotationType.CCW); - - entities = new List(); - entities.AddRange(shape.Perimeter.Entities); - shape.Cutouts.ForEach(cutout => entities.AddRange(cutout.Entities)); - - var pgm = ConvertGeometry.ToProgram(entities); + var normalized = ShapeProfile.NormalizeEntities(entities); + var pgm = ConvertGeometry.ToProgram(normalized); var firstCode = pgm[0]; if (firstCode.Type == CodeType.RapidMove) @@ -605,15 +595,6 @@ namespace OpenNest.Forms } } - private static void SetRotation(Shape shape, RotationType rotation) - { - try - { - var dir = shape.ToPolygon(3).RotationDirection(); - if (dir != rotation) shape.Reverse(); - } - catch { } - } private static Color GetNextColor() { diff --git a/OpenNest/LayoutPart.cs b/OpenNest/LayoutPart.cs index 44106ae..ccefe1a 100644 --- a/OpenNest/LayoutPart.cs +++ b/OpenNest/LayoutPart.cs @@ -178,32 +178,36 @@ namespace OpenNest { var result = new List(); var entities = ConvertProgram.ToGeometry(BasePart.Program); - var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid)); + var profile = new ShapeProfile( + entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); - foreach (var shape in shapes) - { - var offsetEntity = shape.OffsetOutward(spacing); + AddOffsetPolygon(result, profile.Perimeter.OffsetOutward(spacing), tolerance); - if (offsetEntity == null) - continue; - - var polygon = offsetEntity.ToPolygonWithTolerance(tolerance); - polygon.RemoveSelfIntersections(); - - if (polygon.Vertices.Count < 2) - continue; - - var pts = new PointF[polygon.Vertices.Count]; - - for (var j = 0; j < pts.Length; j++) - pts[j] = new PointF((float)polygon.Vertices[j].X, (float)polygon.Vertices[j].Y); - - result.Add(pts); - } + foreach (var cutout in profile.Cutouts) + AddOffsetPolygon(result, cutout.OffsetInward(spacing), tolerance); return result; } + private static void AddOffsetPolygon(List result, Shape offsetEntity, double tolerance) + { + if (offsetEntity == null) + return; + + var polygon = offsetEntity.ToPolygonWithTolerance(tolerance); + polygon.RemoveSelfIntersections(); + + if (polygon.Vertices.Count < 2) + return; + + var pts = new PointF[polygon.Vertices.Count]; + + for (var j = 0; j < pts.Length; j++) + pts[j] = new PointF((float)polygon.Vertices[j].X, (float)polygon.Vertices[j].Y); + + result.Add(pts); + } + private void RebuildOffsetPath(Matrix matrix) { OffsetPath?.Dispose();