From 24b89689c5fed69f30aece3ecef425804ff339ac Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 31 Mar 2026 21:37:25 -0400 Subject: [PATCH] feat: add direction arrows and reverse direction to program editor Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Tests/ContourClassificationTests.cs | 14 ++ OpenNest/Controls/ProgramEditorControl.cs | 137 +++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/OpenNest.Tests/ContourClassificationTests.cs b/OpenNest.Tests/ContourClassificationTests.cs index 67281e8..56c86a9 100644 --- a/OpenNest.Tests/ContourClassificationTests.cs +++ b/OpenNest.Tests/ContourClassificationTests.cs @@ -140,4 +140,18 @@ public class ContourClassificationTests Assert.Equal(ContourClassification.Perimeter, contours[0].Type); Assert.Equal("Perimeter", contours[0].Label); } + + [Fact] + public void Reverse_changes_direction_label() + { + var shape = MakeRectShape(0, 0, 100, 50); + var contours = ContourInfo.Classify(new List { shape }); + var contour = contours[0]; + + var originalDirection = contour.DirectionLabel; + contour.Reverse(); + var newDirection = contour.DirectionLabel; + + Assert.NotEqual(originalDirection, newDirection); + } } diff --git a/OpenNest/Controls/ProgramEditorControl.cs b/OpenNest/Controls/ProgramEditorControl.cs index a5a2d73..1ac9fe0 100644 --- a/OpenNest/Controls/ProgramEditorControl.cs +++ b/OpenNest/Controls/ProgramEditorControl.cs @@ -22,6 +22,8 @@ namespace OpenNest.Controls contourList.DrawItem += OnDrawContourItem; contourList.MeasureItem += OnMeasureContourItem; contourList.SelectedIndexChanged += OnContourSelectionChanged; + reverseButton.Click += OnReverseClicked; + menuReverse.Click += OnReverseClicked; } public Program Program { get; private set; } @@ -101,6 +103,9 @@ namespace OpenNest.Controls entity.Color = color; preview.Entities.Add(entity); } + + if (selected) + AddDirectionArrows(contour.Shape, color); } preview.ZoomToFit(); @@ -178,5 +183,137 @@ namespace OpenNest.Controls { RefreshPreview(); } + + private void OnReverseClicked(object sender, EventArgs e) + { + if (contourList.SelectedIndices.Count == 0) return; + + foreach (int index in contourList.SelectedIndices) + { + if (index >= 0 && index < contours.Count) + contours[index].Reverse(); + } + + Program = BuildProgram(contours); + isDirty = true; + + contourList.Invalidate(); + UpdateGcodeText(); + RefreshPreview(); + ProgramChanged?.Invoke(this, EventArgs.Empty); + } + + private void AddDirectionArrows(Shape shape, Color color) + { + var entities = shape.Entities; + if (entities.Count == 0) return; + + var totalLength = shape.Length; + if (totalLength < 0.001) return; + + var arrowSize = totalLength * 0.02; + if (arrowSize < 0.5) arrowSize = 0.5; + if (arrowSize > 5) arrowSize = 5; + + foreach (var fraction in new[] { 0.25, 0.75 }) + { + var targetDist = totalLength * fraction; + var accumulated = 0.0; + var found = false; + + foreach (var entity in entities) + { + var entityLen = entity.Length; + if (accumulated + entityLen >= targetDist) + { + var localFraction = (targetDist - accumulated) / entityLen; + var (point, angle) = GetPointAndAngle(entity, localFraction); + AddArrowHead(point, angle, arrowSize, color); + found = true; + break; + } + accumulated += entityLen; + } + + if (!found && entities.Count > 0) + { + var last = entities[^1]; + var (point, angle) = GetPointAndAngle(last, 0.5); + AddArrowHead(point, angle, arrowSize, color); + } + } + } + + private static (Vector point, double angle) GetPointAndAngle(Entity entity, double fraction) + { + switch (entity) + { + case Line line: + { + var dx = line.EndPoint.X - line.StartPoint.X; + var dy = line.EndPoint.Y - line.StartPoint.Y; + var pt = new Vector( + line.StartPoint.X + dx * fraction, + line.StartPoint.Y + dy * fraction); + var angle = System.Math.Atan2(dy, dx); + return (pt, angle); + } + case Arc arc: + { + var startAngle = arc.StartAngle; + var endAngle = arc.EndAngle; + if (arc.IsReversed) + { + var span = startAngle - endAngle; + if (span < 0) span += 2 * System.Math.PI; + var a = startAngle - span * fraction; + var pt = new Vector( + arc.Center.X + arc.Radius * System.Math.Cos(a), + arc.Center.Y + arc.Radius * System.Math.Sin(a)); + var tangent = a - System.Math.PI / 2; + return (pt, tangent); + } + else + { + var span = endAngle - startAngle; + if (span < 0) span += 2 * System.Math.PI; + var a = startAngle + span * fraction; + var pt = new Vector( + arc.Center.X + arc.Radius * System.Math.Cos(a), + arc.Center.Y + arc.Radius * System.Math.Sin(a)); + var tangent = a + System.Math.PI / 2; + return (pt, tangent); + } + } + case Circle circle: + { + var a = 2 * System.Math.PI * fraction; + var pt = new Vector( + circle.Center.X + circle.Radius * System.Math.Cos(a), + circle.Center.Y + circle.Radius * System.Math.Sin(a)); + var tangent = a + System.Math.PI / 2; + return (pt, tangent); + } + default: + return (new Vector(), 0); + } + } + + private void AddArrowHead(Vector tip, double angle, double size, Color color) + { + var leftAngle = angle + System.Math.PI + 0.4; + var rightAngle = angle + System.Math.PI - 0.4; + + var left = new Vector( + tip.X + size * System.Math.Cos(leftAngle), + tip.Y + size * System.Math.Sin(leftAngle)); + var right = new Vector( + tip.X + size * System.Math.Cos(rightAngle), + tip.Y + size * System.Math.Sin(rightAngle)); + + var arrowColor = Color.FromArgb(255, 140, 50); + preview.Entities.Add(new Line(left, tip) { Color = arrowColor }); + preview.Entities.Add(new Line(right, tip) { Color = arrowColor }); + } } }