From 29c28728192f1671e087cee0f14c780b22a2208e Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 12 Apr 2026 21:35:13 -0400 Subject: [PATCH] fix(geometry): add Entity.Clone() and stop NormalizeEntities from mutating originals ShapeProfile.NormalizeEntities called Shape.Reverse() which flipped arc directions on the original entity objects shared with the CAD view. Switching to the Program tab and back would leave arcs reversed. Clone entities before normalizing so the originals stay untouched. Adds abstract Entity.Clone() with implementations on Line, Arc, Circle, Polygon, and Shape (deep-clones children). Also adds CloneAll() extension and replaces manual duplication in PartGeometry.CopyEntitiesAtLocation and ProgramEditorControl.CloneEntity. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Geometry/Arc.cs | 7 +++++++ OpenNest.Core/Geometry/Circle.cs | 7 +++++++ OpenNest.Core/Geometry/Entity.cs | 25 +++++++++++++++++++++++ OpenNest.Core/Geometry/Line.cs | 7 +++++++ OpenNest.Core/Geometry/Polygon.cs | 7 +++++++ OpenNest.Core/Geometry/Shape.cs | 9 ++++++++ OpenNest.Core/Geometry/ShapeProfile.cs | 3 ++- OpenNest.Core/PartGeometry.cs | 16 +++------------ OpenNest/Controls/ProgramEditorControl.cs | 11 ++-------- 9 files changed, 69 insertions(+), 23 deletions(-) diff --git a/OpenNest.Core/Geometry/Arc.cs b/OpenNest.Core/Geometry/Arc.cs index 0ed1272..d51a4c9 100644 --- a/OpenNest.Core/Geometry/Arc.cs +++ b/OpenNest.Core/Geometry/Arc.cs @@ -267,6 +267,13 @@ namespace OpenNest.Geometry get { return Diameter * System.Math.PI * SweepAngle() / Angle.TwoPI; } } + public override Entity Clone() + { + var copy = new Arc(center, radius, startAngle, endAngle, reversed); + CopyBaseTo(copy); + return copy; + } + /// /// Reverses the rotation direction. /// diff --git a/OpenNest.Core/Geometry/Circle.cs b/OpenNest.Core/Geometry/Circle.cs index 05a03de..ac25197 100644 --- a/OpenNest.Core/Geometry/Circle.cs +++ b/OpenNest.Core/Geometry/Circle.cs @@ -165,6 +165,13 @@ namespace OpenNest.Geometry get { return Circumference(); } } + public override Entity Clone() + { + var copy = new Circle(center, radius) { Rotation = Rotation }; + CopyBaseTo(copy); + return copy; + } + /// /// Reverses the rotation direction. /// diff --git a/OpenNest.Core/Geometry/Entity.cs b/OpenNest.Core/Geometry/Entity.cs index bd2e7fe..723ddc6 100644 --- a/OpenNest.Core/Geometry/Entity.cs +++ b/OpenNest.Core/Geometry/Entity.cs @@ -251,6 +251,23 @@ namespace OpenNest.Geometry /// public abstract bool Intersects(Shape shape, out List pts); + /// + /// Creates a deep copy of the entity with a new Id. + /// + public abstract Entity Clone(); + + /// + /// Copies common Entity properties from this instance to the target. + /// + protected void CopyBaseTo(Entity target) + { + target.Color = Color; + target.Layer = Layer; + target.LineTypeName = LineTypeName; + target.IsVisible = IsVisible; + target.Tag = Tag; + } + /// /// Type of entity. /// @@ -259,6 +276,14 @@ namespace OpenNest.Geometry public static class EntityExtensions { + public static List CloneAll(this IEnumerable entities) + { + var result = new List(); + foreach (var e in entities) + result.Add(e.Clone()); + return result; + } + public static List CollectPoints(this IEnumerable entities) { var points = new List(); diff --git a/OpenNest.Core/Geometry/Line.cs b/OpenNest.Core/Geometry/Line.cs index e0317d8..9b4474b 100644 --- a/OpenNest.Core/Geometry/Line.cs +++ b/OpenNest.Core/Geometry/Line.cs @@ -257,6 +257,13 @@ namespace OpenNest.Geometry } } + public override Entity Clone() + { + var copy = new Line(pt1, pt2); + CopyBaseTo(copy); + return copy; + } + /// /// Reversed the line. /// diff --git a/OpenNest.Core/Geometry/Polygon.cs b/OpenNest.Core/Geometry/Polygon.cs index 22e2131..9ae1cab 100644 --- a/OpenNest.Core/Geometry/Polygon.cs +++ b/OpenNest.Core/Geometry/Polygon.cs @@ -168,6 +168,13 @@ namespace OpenNest.Geometry get { return Perimeter(); } } + public override Entity Clone() + { + var copy = new Polygon { Vertices = new List(Vertices) }; + CopyBaseTo(copy); + return copy; + } + /// /// Reverses the rotation direction of the polygon. /// diff --git a/OpenNest.Core/Geometry/Shape.cs b/OpenNest.Core/Geometry/Shape.cs index 6e35f1c..6cd9358 100644 --- a/OpenNest.Core/Geometry/Shape.cs +++ b/OpenNest.Core/Geometry/Shape.cs @@ -349,6 +349,15 @@ namespace OpenNest.Geometry return polygon; } + public override Entity Clone() + { + var copy = new Shape(); + foreach (var e in Entities) + copy.Entities.Add(e.Clone()); + CopyBaseTo(copy); + return copy; + } + /// /// Reverses the rotation direction of the shape. /// diff --git a/OpenNest.Core/Geometry/ShapeProfile.cs b/OpenNest.Core/Geometry/ShapeProfile.cs index 23928e6..e73632d 100644 --- a/OpenNest.Core/Geometry/ShapeProfile.cs +++ b/OpenNest.Core/Geometry/ShapeProfile.cs @@ -75,7 +75,8 @@ namespace OpenNest.Geometry /// public static List NormalizeEntities(IEnumerable entities) { - var profile = new ShapeProfile(entities.ToList()); + var cloned = entities.CloneAll(); + var profile = new ShapeProfile(cloned); return profile.ToNormalizedEntities(); } diff --git a/OpenNest.Core/PartGeometry.cs b/OpenNest.Core/PartGeometry.cs index c30f6e0..573d641 100644 --- a/OpenNest.Core/PartGeometry.cs +++ b/OpenNest.Core/PartGeometry.cs @@ -126,20 +126,10 @@ namespace OpenNest { var result = new List(source.Count); - for (var i = 0; i < source.Count; i++) + foreach (var entity in source) { - var entity = source[i]; - Entity copy; - - if (entity is Line line) - copy = new Line(line.StartPoint + location, line.EndPoint + location); - else if (entity is Arc arc) - copy = new Arc(arc.Center + location, arc.Radius, arc.StartAngle, arc.EndAngle, arc.IsReversed); - else if (entity is Circle circle) - copy = new Circle(circle.Center + location, circle.Radius); - else - continue; - + var copy = entity.Clone(); + copy.Offset(location); result.Add(copy); } diff --git a/OpenNest/Controls/ProgramEditorControl.cs b/OpenNest/Controls/ProgramEditorControl.cs index 0941463..0b026aa 100644 --- a/OpenNest/Controls/ProgramEditorControl.cs +++ b/OpenNest/Controls/ProgramEditorControl.cs @@ -209,15 +209,8 @@ namespace OpenNest.Controls private static Entity CloneEntity(Entity entity, Color color) { - Entity clone = entity switch - { - Line line => new Line(line.StartPoint, line.EndPoint) { Layer = line.Layer, IsVisible = line.IsVisible }, - Arc arc => new Arc(arc.Center, arc.Radius, arc.StartAngle, arc.EndAngle, arc.IsReversed) { Layer = arc.Layer, IsVisible = arc.IsVisible }, - Circle circle => new Circle(circle.Center, circle.Radius) { Layer = circle.Layer, IsVisible = circle.IsVisible }, - _ => null, - }; - if (clone != null) - clone.Color = color; + var clone = entity.Clone(); + clone.Color = color; return clone; }