diff --git a/OpenNest/Controls/BestFitCell.cs b/OpenNest/Controls/BestFitCell.cs index d531f05..c6ec08a 100644 --- a/OpenNest/Controls/BestFitCell.cs +++ b/OpenNest/Controls/BestFitCell.cs @@ -61,8 +61,8 @@ namespace OpenNest.Controls e.Graphics.SmoothingMode = SmoothingMode.HighSpeed; e.Graphics.TranslateTransform(origin.X, origin.Y); - DrawPlate(e.Graphics); - DrawParts(e.Graphics); + Renderer.DrawPlate(e.Graphics); + Renderer.DrawParts(e.Graphics); e.Graphics.ResetTransform(); PaintMetadata(e.Graphics); diff --git a/OpenNest/Controls/PlateRenderer.cs b/OpenNest/Controls/PlateRenderer.cs new file mode 100644 index 0000000..cbf328f --- /dev/null +++ b/OpenNest/Controls/PlateRenderer.cs @@ -0,0 +1,691 @@ +using OpenNest.Bending; +using OpenNest.CNC; +using OpenNest.Geometry; +using OpenNest.Math; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Linq; + +namespace OpenNest.Controls +{ + internal class PlateRenderer + { + private readonly PlateView view; + + public PlateRenderer(PlateView view) + { + this.view = view; + } + + public void DrawPlate(Graphics g) + { + var plate = view.Plate; + var plateRect = new RectangleF + { + Width = view.LengthWorldToGui(plate.Size.Length), + Height = view.LengthWorldToGui(plate.Size.Width) + }; + + var edgeSpacingRect = new RectangleF + { + Width = view.LengthWorldToGui(plate.Size.Length - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right), + Height = view.LengthWorldToGui(plate.Size.Width - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom) + }; + + switch (plate.Quadrant) + { + case 1: + plateRect.Location = view.PointWorldToGraph(0, 0); + edgeSpacingRect.Location = view.PointWorldToGraph( + plate.EdgeSpacing.Left, + plate.EdgeSpacing.Bottom); + break; + + case 2: + plateRect.Location = view.PointWorldToGraph(-plate.Size.Length, 0); + edgeSpacingRect.Location = view.PointWorldToGraph( + plate.EdgeSpacing.Left - plate.Size.Length, + plate.EdgeSpacing.Bottom); + break; + + case 3: + plateRect.Location = view.PointWorldToGraph(-plate.Size.Length, -plate.Size.Width); + edgeSpacingRect.Location = view.PointWorldToGraph( + plate.EdgeSpacing.Left - plate.Size.Length, + plate.EdgeSpacing.Bottom - plate.Size.Width); + break; + + case 4: + plateRect.Location = view.PointWorldToGraph(0, -plate.Size.Width); + edgeSpacingRect.Location = view.PointWorldToGraph( + plate.EdgeSpacing.Left, + plate.EdgeSpacing.Bottom - plate.Size.Width); + break; + + default: + return; + } + + plateRect.Y -= plateRect.Height; + edgeSpacingRect.Y -= edgeSpacingRect.Height; + + g.FillRectangle(view.ColorScheme.LayoutFillBrush, plateRect); + + var viewBounds = view.GetViewBounds(); + + if (!edgeSpacingRect.Contains(viewBounds)) + { + g.DrawRectangle(view.ColorScheme.EdgeSpacingPen, + edgeSpacingRect.X, + edgeSpacingRect.Y, + edgeSpacingRect.Width, + edgeSpacingRect.Height); + } + + g.DrawRectangle(view.ColorScheme.LayoutOutlinePen, + plateRect.X, + plateRect.Y, + plateRect.Width, + plateRect.Height); + } + + public void DrawParts(Graphics g) + { + var viewBounds = view.GetViewBounds(); + var layoutParts = view.LayoutParts; + + for (var i = 0; i < layoutParts.Count; ++i) + { + var part = layoutParts[i]; + + if (part.IsDirty) + part.Update(view); + + var path = part.Path; + var pathBounds = path.GetBounds(); + + if (!pathBounds.IntersectsWith(viewBounds)) + continue; + + part.Draw(g, (i + 1).ToString()); + DrawBendLines(g, part.BasePart); + DrawEtchMarks(g, part.BasePart); + DrawGrainWarning(g, part.BasePart); + } + + var previewParts = view.PreviewParts; + var previewBrush = view.PreviewBrush; + var previewPen = view.PreviewPen; + + for (var i = 0; i < previewParts.Count; i++) + { + var part = previewParts[i]; + + if (part.IsDirty) + part.Update(view); + + var path = part.Path; + if (!path.GetBounds().IntersectsWith(viewBounds)) + continue; + + g.FillPath(previewBrush, path); + g.DrawPath(previewPen, path); + } + + if (view.DrawOffset && view.Plate.PartSpacing > 0) + DrawOffsetGeometry(g); + + if (view.DrawBounds) + { + var bounds = view.SelectedParts.Select(p => p.BasePart).ToList().GetBoundingBox(); + DrawBox(g, bounds); + } + + if (view.DrawRapid) + DrawRapids(g); + + if (view.DrawPiercePoints) + DrawAllPiercePoints(g); + + if (view.DrawCutDirection) + DrawAllCutDirectionArrows(g); + } + + public void DrawCutOffs(Graphics g) + { + var plate = view.Plate; + if (plate?.CutOffs == null || plate.CutOffs.Count == 0) + return; + + using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f); + using var selectedPen = new Pen(Color.FromArgb(0, 120, 255), 3.5f); + + foreach (var cutoff in plate.CutOffs) + { + var program = cutoff.Drawing?.Program; + if (program == null || program.Codes.Count == 0) + continue; + + var activePen = cutoff == view.SelectedCutOff ? selectedPen : pen; + + for (var i = 0; i < program.Codes.Count - 1; i += 2) + { + if (program.Codes[i] is RapidMove rapid && + program.Codes[i + 1] is LinearMove linear) + { + DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen); + } + } + } + } + + public void DrawActiveWorkArea(Graphics g) + { + var workArea = view.ActiveWorkArea; + if (workArea == null) + return; + + var rect = new RectangleF + { + Location = view.PointWorldToGraph(workArea.Location), + Width = view.LengthWorldToGui(workArea.Width), + Height = view.LengthWorldToGui(workArea.Length) + }; + rect.Y -= rect.Height; + + using var pen = new Pen(Color.Red, 1.5f) + { + DashStyle = DashStyle.Dash + }; + g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height); + } + + private static readonly Color[] PriorityFills = + { + Color.FromArgb(60, Color.LimeGreen), + Color.FromArgb(60, Color.Gold), + Color.FromArgb(60, Color.Salmon), + }; + + private static readonly Color[] PriorityBorders = + { + Color.FromArgb(180, Color.Green), + Color.FromArgb(180, Color.DarkGoldenrod), + Color.FromArgb(180, Color.DarkRed), + }; + + public void DrawDebugRemnants(Graphics g) + { + var remnants = view.DebugRemnants; + if (remnants == null || remnants.Count == 0) + return; + + for (var i = 0; i < remnants.Count; i++) + { + var box = remnants[i]; + var loc = view.PointWorldToGraph(box.Location); + var w = view.LengthWorldToGui(box.Width); + var h = view.LengthWorldToGui(box.Length); + var rect = new RectangleF(loc.X, loc.Y - h, w, h); + + var priority = view.DebugRemnantPriorities != null && i < view.DebugRemnantPriorities.Count + ? System.Math.Min(view.DebugRemnantPriorities[i], 2) + : 0; + + using var brush = new SolidBrush(PriorityFills[priority]); + g.FillRectangle(brush, rect); + + using var pen = new Pen(PriorityBorders[priority], 1.5f); + g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height); + + var label = $"P{priority} {box.Width:F1}x{box.Length:F1}"; + using var font = new Font("Segoe UI", 8f); + using var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; + g.DrawString(label, font, Brushes.Black, rect, sf); + } + } + + private void DrawBendLines(Graphics g, Part part) + { + if (!view.ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0) + return; + + using var bendPen = new Pen(Color.Yellow, 1.5f) + { + DashStyle = System.Drawing.Drawing2D.DashStyle.Dash + }; + + foreach (var bend in part.BaseDrawing.Bends) + { + var start = bend.StartPoint; + var end = bend.EndPoint; + + if (part.Rotation != 0) + { + start = start.Rotate(part.Rotation); + end = end.Rotate(part.Rotation); + } + + start = start + part.Location; + end = end + part.Location; + + var pt1 = view.PointWorldToGraph(start); + var pt2 = view.PointWorldToGraph(end); + + g.DrawLine(bendPen, pt1, pt2); + } + } + + private void DrawEtchMarks(Graphics g, Part part) + { + if (!view.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; + + if (part.Rotation != 0) + { + start = start.Rotate(part.Rotation); + end = end.Rotate(part.Rotation); + } + + 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 = view.PointWorldToGraph(start); + var pt2 = view.PointWorldToGraph(end); + g.DrawLine(etchPen, pt1, pt2); + } + else + { + var dx = System.Math.Cos(angle) * etchLength; + var dy = System.Math.Sin(angle) * etchLength; + + var s1 = view.PointWorldToGraph(start); + var e1 = view.PointWorldToGraph(new Vector(start.X + dx, start.Y + dy)); + g.DrawLine(etchPen, s1, e1); + + var s2 = view.PointWorldToGraph(end); + var e2 = view.PointWorldToGraph(new Vector(end.X - dx, end.Y - dy)); + g.DrawLine(etchPen, s2, e2); + } + } + } + + private void DrawGrainWarning(Graphics g, Part part) + { + var plate = view.Plate; + if (!view.ShowBendLines || plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0) + return; + + var grainAngle = plate.GrainAngle; + var tolerance = Angle.ToRadians(5); + + foreach (var bend in part.BaseDrawing.Bends) + { + var bendAngle = bend.LineAngle + part.Rotation; + bendAngle = bendAngle % System.Math.PI; + if (bendAngle < 0) bendAngle += System.Math.PI; + + var grainNormalized = grainAngle % System.Math.PI; + if (grainNormalized < 0) grainNormalized += System.Math.PI; + + var diff = System.Math.Abs(bendAngle - grainNormalized); + diff = System.Math.Min(diff, System.Math.PI - diff); + + if (diff > tolerance) + { + var box = part.BaseDrawing.Program.BoundingBox(); + var location = part.Location; + var pt1 = view.PointWorldToGraph(location); + var pt2 = view.PointWorldToGraph(new Vector( + location.X + box.Width, location.Y + box.Length)); + using var warnPen = new Pen(Color.FromArgb(180, 255, 140, 0), 2f); + g.DrawRectangle(warnPen, pt1.X, pt2.Y, + System.Math.Abs(pt2.X - pt1.X), System.Math.Abs(pt2.Y - pt1.Y)); + return; + } + } + } + + private void DrawOffsetGeometry(Graphics g) + { + var layoutParts = view.LayoutParts; + using var offsetPen = new Pen(Color.FromArgb(120, 255, 100, 100)); + + for (var i = 0; i < layoutParts.Count; i++) + { + var layoutPart = layoutParts[i]; + + if (layoutPart.IsDirty) + layoutPart.Update(view); + + layoutPart.UpdateOffset(view.Plate.PartSpacing, view.OffsetTolerance, view.Matrix); + + if (layoutPart.OffsetPath != null) + g.DrawPath(offsetPen, layoutPart.OffsetPath); + } + } + + private void DrawRapids(Graphics g) + { + var pos = new Vector(0, 0); + + for (var i = 0; i < view.Plate.Parts.Count; ++i) + { + var part = view.Plate.Parts[i]; + var pgm = part.Program; + + var piercePoint = GetFirstPiercePoint(pgm, part.Location); + DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen); + + pos = part.Location; + DrawRapids(g, pgm, ref pos, skipFirstRapid: true); + } + } + + private static Vector GetFirstPiercePoint(Program pgm, Vector partLocation) + { + for (var i = 0; i < pgm.Length; i++) + { + if (pgm[i] is Motion motion) + { + if (pgm.Mode == Mode.Incremental) + return motion.EndPoint + partLocation; + return motion.EndPoint; + } + } + return partLocation; + } + + private void DrawRapids(Graphics g, Program pgm, ref Vector pos, bool skipFirstRapid = false) + { + var firstRapidSkipped = false; + + for (var i = 0; i < pgm.Length; ++i) + { + var code = pgm[i]; + + if (code.Type == CodeType.SubProgramCall) + { + var subpgm = (SubProgramCall)code; + var program = subpgm.Program; + + if (program != null) + DrawRapids(g, program, ref pos); + } + else + { + var motion = code as Motion; + + if (motion != null) + { + if (pgm.Mode == Mode.Incremental) + { + var endpt = motion.EndPoint + pos; + + if (code.Type == CodeType.RapidMove) + { + if (skipFirstRapid && !firstRapidSkipped) + firstRapidSkipped = true; + else + DrawLine(g, pos, endpt, view.ColorScheme.RapidPen); + } + pos = endpt; + } + else + { + if (code.Type == CodeType.RapidMove) + { + if (skipFirstRapid && !firstRapidSkipped) + firstRapidSkipped = true; + else + DrawLine(g, pos, motion.EndPoint, view.ColorScheme.RapidPen); + } + pos = motion.EndPoint; + } + } + } + } + } + + private void DrawAllPiercePoints(Graphics g) + { + using var brush = new SolidBrush(Color.Red); + using var pen = new Pen(Color.DarkRed, 1f); + + for (var i = 0; i < view.Plate.Parts.Count; ++i) + { + var part = view.Plate.Parts[i]; + var pgm = part.Program; + var pos = part.Location; + DrawProgramPiercePoints(g, pgm, ref pos, brush, pen); + } + } + + private void DrawProgramPiercePoints(Graphics g, Program pgm, ref Vector pos, Brush brush, Pen pen) + { + for (var i = 0; i < pgm.Length; ++i) + { + var code = pgm[i]; + + if (code.Type == CodeType.SubProgramCall) + { + var subpgm = (SubProgramCall)code; + if (subpgm.Program != null) + DrawProgramPiercePoints(g, subpgm.Program, ref pos, brush, pen); + } + else + { + var motion = code as Motion; + if (motion == null) continue; + + var endpt = pgm.Mode == Mode.Incremental + ? motion.EndPoint + pos + : motion.EndPoint; + + if (code.Type == CodeType.RapidMove) + { + var pt = view.PointWorldToGraph(endpt); + var radius = 2f; + g.FillEllipse(brush, pt.X - radius, pt.Y - radius, radius * 2, radius * 2); + g.DrawEllipse(pen, pt.X - radius, pt.Y - radius, radius * 2, radius * 2); + } + + pos = endpt; + } + } + } + + private void DrawAllCutDirectionArrows(Graphics g) + { + using var pen = new Pen(Color.FromArgb(220, Color.DarkCyan), 1.5f); + using var brush = new SolidBrush(Color.FromArgb(220, Color.DarkCyan)); + + var arrowSpacingWorld = view.LengthGuiToWorld(60f); + var arrowSize = 5f; + + for (var i = 0; i < view.Plate.Parts.Count; ++i) + { + var part = view.Plate.Parts[i]; + var pgm = part.Program; + var pos = part.Location; + DrawProgramCutDirectionArrows(g, pgm, ref pos, pen, brush, arrowSpacingWorld, arrowSize); + } + } + + private void DrawProgramCutDirectionArrows(Graphics g, Program pgm, ref Vector pos, + Pen pen, Brush brush, double spacing, float arrowSize) + { + for (var i = 0; i < pgm.Length; ++i) + { + var code = pgm[i]; + + if (code.Type == CodeType.SubProgramCall) + { + var subpgm = (SubProgramCall)code; + if (subpgm.Program != null) + DrawProgramCutDirectionArrows(g, subpgm.Program, ref pos, pen, brush, spacing, arrowSize); + continue; + } + + if (code is not Motion motion) continue; + + var endpt = pgm.Mode == Mode.Incremental + ? motion.EndPoint + pos + : motion.EndPoint; + + if (code.Type == CodeType.LinearMove) + { + var line = (LinearMove)code; + if (!line.Suppressed) + DrawLineDirectionArrows(g, pos, endpt, brush, spacing, arrowSize); + } + else if (code.Type == CodeType.ArcMove) + { + var arc = (ArcMove)code; + if (!arc.Suppressed) + { + var center = pgm.Mode == Mode.Incremental + ? arc.CenterPoint + pos + : arc.CenterPoint; + DrawArcDirectionArrows(g, pos, endpt, center, arc.Rotation, brush, spacing, arrowSize); + } + } + + pos = endpt; + } + } + + private void DrawLineDirectionArrows(Graphics g, Vector start, Vector end, + Brush brush, double spacing, float arrowSize) + { + var dx = end.X - start.X; + var dy = end.Y - start.Y; + var length = System.Math.Sqrt(dx * dx + dy * dy); + if (length < spacing * 0.5) return; + + var dirX = dx / length; + var dirY = dy / length; + + var count = System.Math.Max(1, (int)(length / spacing)); + var step = length / (count + 1); + + for (var i = 1; i <= count; i++) + { + var t = step * i; + var pt = new Vector(start.X + dirX * t, start.Y + dirY * t); + var screenPt = view.PointWorldToGraph(pt); + var angle = System.Math.Atan2(-dirY, dirX); + DrawArrowHead(g, brush, screenPt, angle, arrowSize); + } + } + + private void DrawArcDirectionArrows(Graphics g, Vector start, Vector end, Vector center, + RotationType rotation, Brush brush, double spacing, float arrowSize) + { + var radius = center.DistanceTo(start); + if (radius < Tolerance.Epsilon) return; + + var startAngle = System.Math.Atan2(start.Y - center.Y, start.X - center.X); + var endAngle = System.Math.Atan2(end.Y - center.Y, end.X - center.X); + + double sweep; + if (rotation == RotationType.CCW) + { + sweep = endAngle - startAngle; + if (sweep <= 0) sweep += 2 * System.Math.PI; + } + else + { + sweep = startAngle - endAngle; + if (sweep <= 0) sweep += 2 * System.Math.PI; + } + + var arcLength = radius * System.Math.Abs(sweep); + if (arcLength < spacing * 0.5) return; + + var count = System.Math.Max(1, (int)(arcLength / spacing)); + var stepAngle = sweep / (count + 1); + + for (var i = 1; i <= count; i++) + { + double angle; + if (rotation == RotationType.CCW) + angle = startAngle + stepAngle * i; + else + angle = startAngle - stepAngle * i; + + var pt = new Vector( + center.X + radius * System.Math.Cos(angle), + center.Y + radius * System.Math.Sin(angle)); + var screenPt = view.PointWorldToGraph(pt); + + double tangent; + if (rotation == RotationType.CCW) + tangent = angle + System.Math.PI / 2; + else + tangent = angle - System.Math.PI / 2; + + var screenAngle = System.Math.Atan2(-System.Math.Sin(tangent), System.Math.Cos(tangent)); + DrawArrowHead(g, brush, screenPt, screenAngle, arrowSize); + } + } + + private static void DrawArrowHead(Graphics g, Brush brush, PointF tip, double angle, float size) + { + var sin = (float)System.Math.Sin(angle); + var cos = (float)System.Math.Cos(angle); + + var backX = -size * cos; + var backY = -size * sin; + var wingX = size * 0.5f * sin; + var wingY = -size * 0.5f * cos; + + var points = new PointF[] + { + tip, + new PointF(tip.X + backX + wingX, tip.Y + backY + wingY), + new PointF(tip.X + backX - wingX, tip.Y + backY - wingY), + }; + + g.FillPolygon(brush, points); + } + + private void DrawLine(Graphics g, Vector pt1, Vector pt2, Pen pen) + { + var point1 = view.PointWorldToGraph(pt1); + var point2 = view.PointWorldToGraph(pt2); + + g.DrawLine(pen, point1, point2); + } + + private void DrawBox(Graphics g, Box box) + { + var rect = new RectangleF + { + Location = view.PointWorldToGraph(box.Location), + Width = view.LengthWorldToGui(box.Width), + Height = view.LengthWorldToGui(box.Length) + }; + + g.DrawRectangle(view.ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height); + } + } +} diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index 28c2317..7aa94fe 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -1,5 +1,4 @@ using OpenNest.Actions; -using OpenNest.Bending; using OpenNest.CNC; using OpenNest.Collections; using OpenNest.Engine.Fill; @@ -41,6 +40,7 @@ namespace OpenNest.Controls private Point middleMouseDownPoint; private Box activeWorkArea; private List debugRemnants; + private PlateRenderer renderer; public Box ActiveWorkArea { @@ -114,6 +114,7 @@ namespace OpenNest.Controls DrawBounds = true; DrawOffset = false; FillParts = true; + renderer = new PlateRenderer(this); SetAction(typeof(ActionSelect)); UpdateMatrix(); @@ -137,12 +138,30 @@ namespace OpenNest.Controls public bool DrawOffset { get; set; } + public bool DrawCutDirection { get; set; } + public bool ShowBendLines { get; set; } public double OffsetTolerance { get; set; } = 0.001; public bool FillParts { get; set; } + internal List LayoutParts => parts; + + internal IReadOnlyList PreviewParts => + activeParts.Count > 0 ? activeParts : stationaryParts; + + internal Brush PreviewBrush => + activeParts.Count > 0 ? ColorScheme.ActivePreviewPartBrush : ColorScheme.PreviewPartBrush; + + internal Pen PreviewPen => + activeParts.Count > 0 ? ColorScheme.ActivePreviewPartPen : ColorScheme.PreviewPartPen; + + internal RectangleF GetViewBounds() => + new RectangleF(-origin.X, -origin.Y, Width, Height); + + internal PlateRenderer Renderer => renderer; + public CutOffSettings CutOffSettings { get => cutOffSettings; @@ -465,11 +484,11 @@ namespace OpenNest.Controls e.Graphics.TranslateTransform(origin.X, origin.Y); - DrawPlate(e.Graphics); - DrawParts(e.Graphics); - DrawCutOffs(e.Graphics); - DrawActiveWorkArea(e.Graphics); - DrawDebugRemnants(e.Graphics); + renderer.DrawPlate(e.Graphics); + renderer.DrawParts(e.Graphics); + renderer.DrawCutOffs(e.Graphics); + renderer.DrawActiveWorkArea(e.Graphics); + renderer.DrawDebugRemnants(e.Graphics); base.OnPaint(e); } @@ -494,284 +513,6 @@ namespace OpenNest.Controls Invalidate(); } - protected void DrawPlate(Graphics g) - { - var plateRect = new RectangleF - { - Width = LengthWorldToGui(Plate.Size.Length), - Height = LengthWorldToGui(Plate.Size.Width) - }; - - var edgeSpacingRect = new RectangleF - { - Width = LengthWorldToGui(Plate.Size.Length - Plate.EdgeSpacing.Left - Plate.EdgeSpacing.Right), - Height = LengthWorldToGui(Plate.Size.Width - Plate.EdgeSpacing.Top - Plate.EdgeSpacing.Bottom) - }; - - switch (Plate.Quadrant) - { - case 1: - plateRect.Location = PointWorldToGraph(0, 0); - edgeSpacingRect.Location = PointWorldToGraph( - Plate.EdgeSpacing.Left, - Plate.EdgeSpacing.Bottom); - break; - - case 2: - plateRect.Location = PointWorldToGraph(-Plate.Size.Length, 0); - edgeSpacingRect.Location = PointWorldToGraph( - Plate.EdgeSpacing.Left - Plate.Size.Length, - Plate.EdgeSpacing.Bottom); - break; - - case 3: - plateRect.Location = PointWorldToGraph(-Plate.Size.Length, -Plate.Size.Width); - edgeSpacingRect.Location = PointWorldToGraph( - Plate.EdgeSpacing.Left - Plate.Size.Length, - Plate.EdgeSpacing.Bottom - Plate.Size.Width); - break; - - case 4: - plateRect.Location = PointWorldToGraph(0, -Plate.Size.Width); - edgeSpacingRect.Location = PointWorldToGraph( - Plate.EdgeSpacing.Left, - Plate.EdgeSpacing.Bottom - Plate.Size.Width); - break; - - default: - return; - } - - plateRect.Y -= plateRect.Height; - edgeSpacingRect.Y -= edgeSpacingRect.Height; - - g.FillRectangle(ColorScheme.LayoutFillBrush, plateRect); - - var viewBounds = new RectangleF(-origin.X, -origin.Y, Width, Height); - - if (!edgeSpacingRect.Contains(viewBounds)) - { - g.DrawRectangle(ColorScheme.EdgeSpacingPen, - edgeSpacingRect.X, - edgeSpacingRect.Y, - edgeSpacingRect.Width, - edgeSpacingRect.Height); - } - - g.DrawRectangle(ColorScheme.LayoutOutlinePen, - plateRect.X, - plateRect.Y, - plateRect.Width, - plateRect.Height); - } - - protected void DrawParts(Graphics g) - { - var viewBounds = new RectangleF(-origin.X, -origin.Y, Width, Height); - - for (int i = 0; i < parts.Count; ++i) - { - var part = parts[i]; - - if (part.IsDirty) - part.Update(this); - - var path = part.Path; - var pathBounds = path.GetBounds(); - - if (!pathBounds.IntersectsWith(viewBounds)) - continue; - - part.Draw(g, (i + 1).ToString()); - DrawBendLines(g, part.BasePart); - DrawEtchMarks(g, part.BasePart); - DrawGrainWarning(g, part.BasePart); - } - - // Draw preview parts — active (current strategy) takes precedence - // over stationary (overall best) to avoid overlapping fills. - var previewParts = activeParts.Count > 0 ? activeParts : stationaryParts; - var previewBrush = activeParts.Count > 0 ? ColorScheme.ActivePreviewPartBrush : ColorScheme.PreviewPartBrush; - var previewPen = activeParts.Count > 0 ? ColorScheme.ActivePreviewPartPen : ColorScheme.PreviewPartPen; - - for (var i = 0; i < previewParts.Count; i++) - { - var part = previewParts[i]; - - if (part.IsDirty) - part.Update(this); - - var path = part.Path; - if (!path.GetBounds().IntersectsWith(viewBounds)) - continue; - - g.FillPath(previewBrush, path); - g.DrawPath(previewPen, path); - } - - if (DrawOffset && Plate.PartSpacing > 0) - DrawOffsetGeometry(g); - - if (DrawBounds) - { - var bounds = SelectedParts.Select(p => p.BasePart).ToList().GetBoundingBox(); - DrawBox(g, bounds); - } - - if (DrawRapid) - DrawRapids(g); - - if (DrawPiercePoints) - DrawAllPiercePoints(g); - } - - private void DrawBendLines(Graphics g, Part part) - { - if (!ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0) - return; - - using var bendPen = new Pen(Color.Yellow, 1.5f) - { - DashStyle = System.Drawing.Drawing2D.DashStyle.Dash - }; - - foreach (var bend in part.BaseDrawing.Bends) - { - 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 pt1 = PointWorldToGraph(start); - var pt2 = PointWorldToGraph(end); - - g.DrawLine(bendPen, pt1, pt2); - } - } - - 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) - return; - - var grainAngle = Plate.GrainAngle; - var tolerance = Angle.ToRadians(5); - - foreach (var bend in part.BaseDrawing.Bends) - { - var bendAngle = bend.LineAngle + part.Rotation; - bendAngle = bendAngle % System.Math.PI; - if (bendAngle < 0) bendAngle += System.Math.PI; - - var grainNormalized = grainAngle % System.Math.PI; - if (grainNormalized < 0) grainNormalized += System.Math.PI; - - var diff = System.Math.Abs(bendAngle - grainNormalized); - diff = System.Math.Min(diff, System.Math.PI - diff); - - if (diff > tolerance) - { - var box = part.BaseDrawing.Program.BoundingBox(); - var location = part.Location; - var pt1 = PointWorldToGraph(location); - var pt2 = PointWorldToGraph(new Vector( - location.X + box.Width, location.Y + box.Length)); - using var warnPen = new Pen(Color.FromArgb(180, 255, 140, 0), 2f); - g.DrawRectangle(warnPen, pt1.X, pt2.Y, - System.Math.Abs(pt2.X - pt1.X), System.Math.Abs(pt2.Y - pt1.Y)); - return; - } - } - } - - private void DrawCutOffs(Graphics g) - { - if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0) - return; - - using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f); - using var selectedPen = new Pen(Color.FromArgb(0, 120, 255), 3.5f); - - foreach (var cutoff in Plate.CutOffs) - { - var program = cutoff.Drawing?.Program; - if (program == null || program.Codes.Count == 0) - continue; - - var activePen = cutoff == selectedCutOff ? selectedPen : pen; - - for (var i = 0; i < program.Codes.Count - 1; i += 2) - { - if (program.Codes[i] is RapidMove rapid && - program.Codes[i + 1] is LinearMove linear) - { - DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen); - } - } - } - } - public CutOff GetCutOffAtPoint(Vector point, double tolerance) { if (Plate?.CutOffs == null) @@ -798,243 +539,6 @@ namespace OpenNest.Controls return null; } - private void DrawOffsetGeometry(Graphics g) - { - using (var offsetPen = new Pen(Color.FromArgb(120, 255, 100, 100))) - { - for (var i = 0; i < parts.Count; i++) - { - var layoutPart = parts[i]; - - if (layoutPart.IsDirty) - layoutPart.Update(this); - - layoutPart.UpdateOffset(Plate.PartSpacing, OffsetTolerance, Matrix); - - if (layoutPart.OffsetPath != null) - g.DrawPath(offsetPen, layoutPart.OffsetPath); - } - } - } - - private void DrawRapids(Graphics g) - { - var pos = new Vector(0, 0); - - for (int i = 0; i < Plate.Parts.Count; ++i) - { - var part = Plate.Parts[i]; - var pgm = part.Program; - - // Draw approach rapid directly to the program's first pierce - // point instead of to part.Location (the coordinate origin), - // which may not be at a cutting feature. - var piercePoint = GetFirstPiercePoint(pgm, part.Location); - DrawLine(g, pos, piercePoint, ColorScheme.RapidPen); - - pos = part.Location; - DrawRapids(g, pgm, ref pos, skipFirstRapid: true); - } - } - - private static Vector GetFirstPiercePoint(Program pgm, Vector partLocation) - { - for (var i = 0; i < pgm.Length; i++) - { - if (pgm[i] is Motion motion) - { - if (pgm.Mode == Mode.Incremental) - return motion.EndPoint + partLocation; - return motion.EndPoint; - } - } - return partLocation; - } - - private void DrawRapids(Graphics g, Program pgm, ref Vector pos, bool skipFirstRapid = false) - { - var firstRapidSkipped = false; - - for (int i = 0; i < pgm.Length; ++i) - { - var code = pgm[i]; - - if (code.Type == CodeType.SubProgramCall) - { - var subpgm = (SubProgramCall)code; - var program = subpgm.Program; - - if (program != null) - DrawRapids(g, program, ref pos); - } - else - { - var motion = code as Motion; - - if (motion != null) - { - if (pgm.Mode == Mode.Incremental) - { - var endpt = motion.EndPoint + pos; - - if (code.Type == CodeType.RapidMove) - { - if (skipFirstRapid && !firstRapidSkipped) - firstRapidSkipped = true; - else - DrawLine(g, pos, endpt, ColorScheme.RapidPen); - } - pos = endpt; - } - else - { - if (code.Type == CodeType.RapidMove) - { - if (skipFirstRapid && !firstRapidSkipped) - firstRapidSkipped = true; - else - DrawLine(g, pos, motion.EndPoint, ColorScheme.RapidPen); - } - pos = motion.EndPoint; - } - } - } - } - } - - private void DrawAllPiercePoints(Graphics g) - { - using var brush = new SolidBrush(Color.Red); - using var pen = new Pen(Color.DarkRed, 1f); - - for (var i = 0; i < Plate.Parts.Count; ++i) - { - var part = Plate.Parts[i]; - var pgm = part.Program; - var pos = part.Location; - DrawProgramPiercePoints(g, pgm, ref pos, brush, pen); - } - } - - private void DrawProgramPiercePoints(Graphics g, Program pgm, ref Vector pos, Brush brush, Pen pen) - { - for (var i = 0; i < pgm.Length; ++i) - { - var code = pgm[i]; - - if (code.Type == CodeType.SubProgramCall) - { - var subpgm = (SubProgramCall)code; - if (subpgm.Program != null) - DrawProgramPiercePoints(g, subpgm.Program, ref pos, brush, pen); - } - else - { - var motion = code as Motion; - if (motion == null) continue; - - var endpt = pgm.Mode == Mode.Incremental - ? motion.EndPoint + pos - : motion.EndPoint; - - if (code.Type == CodeType.RapidMove) - { - var pt = PointWorldToGraph(endpt); - var radius = 2f; - g.FillEllipse(brush, pt.X - radius, pt.Y - radius, radius * 2, radius * 2); - g.DrawEllipse(pen, pt.X - radius, pt.Y - radius, radius * 2, radius * 2); - } - - pos = endpt; - } - } - } - - private void DrawLine(Graphics g, Vector pt1, Vector pt2, Pen pen) - { - var point1 = PointWorldToGraph(pt1); - var point2 = PointWorldToGraph(pt2); - - g.DrawLine(pen, point1, point2); - } - - private void DrawBox(Graphics g, Box box) - { - var rect = new RectangleF - { - Location = PointWorldToGraph(box.Location), - Width = LengthWorldToGui(box.Width), - Height = LengthWorldToGui(box.Length) - }; - - g.DrawRectangle(ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height); - } - - private void DrawActiveWorkArea(Graphics g) - { - if (activeWorkArea == null) - return; - - var rect = new RectangleF - { - Location = PointWorldToGraph(activeWorkArea.Location), - Width = LengthWorldToGui(activeWorkArea.Width), - Height = LengthWorldToGui(activeWorkArea.Length) - }; - rect.Y -= rect.Height; - - using var pen = new Pen(Color.Red, 1.5f) - { - DashStyle = DashStyle.Dash - }; - g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height); - } - - // Priority 0 = green (preferred), 1 = yellow (extend), 2 = red (last resort) - private static readonly Color[] PriorityFills = - { - Color.FromArgb(60, Color.LimeGreen), - Color.FromArgb(60, Color.Gold), - Color.FromArgb(60, Color.Salmon), - }; - - private static readonly Color[] PriorityBorders = - { - Color.FromArgb(180, Color.Green), - Color.FromArgb(180, Color.DarkGoldenrod), - Color.FromArgb(180, Color.DarkRed), - }; - - private void DrawDebugRemnants(Graphics g) - { - if (debugRemnants == null || debugRemnants.Count == 0) - return; - - for (var i = 0; i < debugRemnants.Count; i++) - { - var box = debugRemnants[i]; - var loc = PointWorldToGraph(box.Location); - var w = LengthWorldToGui(box.Width); - var h = LengthWorldToGui(box.Length); - var rect = new RectangleF(loc.X, loc.Y - h, w, h); - - var priority = DebugRemnantPriorities != null && i < DebugRemnantPriorities.Count - ? System.Math.Min(DebugRemnantPriorities[i], 2) - : 0; - - using var brush = new SolidBrush(PriorityFills[priority]); - g.FillRectangle(brush, rect); - - using var pen = new Pen(PriorityBorders[priority], 1.5f); - g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height); - - var label = $"P{priority} {box.Width:F1}x{box.Length:F1}"; - using var font = new Font("Segoe UI", 8f); - using var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; - g.DrawString(label, font, Brushes.Black, rect, sf); - } - } - public LayoutPart GetPartAtControlPoint(Point pt) { var pt2 = PointControlToGraph(pt); diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index db3f7c2..a41b16a 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -498,6 +498,12 @@ namespace OpenNest.Forms PlateView.Invalidate(); } + public void ToggleCutDirection() + { + PlateView.DrawCutDirection = !PlateView.DrawCutDirection; + PlateView.Invalidate(); + } + public void ToggleFillParts() { PlateView.FillParts = !PlateView.FillParts; diff --git a/OpenNest/Forms/MainForm.Designer.cs b/OpenNest/Forms/MainForm.Designer.cs index 18df21b..74a261b 100644 --- a/OpenNest/Forms/MainForm.Designer.cs +++ b/OpenNest/Forms/MainForm.Designer.cs @@ -52,6 +52,7 @@ mnuViewDrawPiercePoints = new System.Windows.Forms.ToolStripMenuItem(); mnuViewDrawBounds = new System.Windows.Forms.ToolStripMenuItem(); mnuViewDrawOffset = new System.Windows.Forms.ToolStripMenuItem(); + mnuViewDrawCutDirection = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem5 = new System.Windows.Forms.ToolStripSeparator(); mnuViewZoomTo = new System.Windows.Forms.ToolStripMenuItem(); mnuViewZoomToArea = new System.Windows.Forms.ToolStripMenuItem(); @@ -305,7 +306,7 @@ // // mnuView // - mnuView.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuViewDrawRapids, mnuViewDrawPiercePoints, mnuViewDrawBounds, mnuViewDrawOffset, toolStripMenuItem5, mnuViewZoomTo, mnuViewZoomIn, mnuViewZoomOut }); + mnuView.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuViewDrawRapids, mnuViewDrawPiercePoints, mnuViewDrawBounds, mnuViewDrawOffset, mnuViewDrawCutDirection, toolStripMenuItem5, mnuViewZoomTo, mnuViewZoomIn, mnuViewZoomOut }); mnuView.Name = "mnuView"; mnuView.Size = new System.Drawing.Size(44, 20); mnuView.Text = "&View"; @@ -340,7 +341,15 @@ mnuViewDrawOffset.Size = new System.Drawing.Size(222, 22); mnuViewDrawOffset.Text = "Draw Offset"; mnuViewDrawOffset.Click += ToggleDrawOffset_Click; - // + // + // mnuViewDrawCutDirection + // + mnuViewDrawCutDirection.CheckOnClick = true; + mnuViewDrawCutDirection.Name = "mnuViewDrawCutDirection"; + mnuViewDrawCutDirection.Size = new System.Drawing.Size(222, 22); + mnuViewDrawCutDirection.Text = "Draw Cut Direction"; + mnuViewDrawCutDirection.Click += ToggleDrawCutDirection_Click; + // // toolStripMenuItem5 // toolStripMenuItem5.Name = "toolStripMenuItem5"; @@ -1144,6 +1153,7 @@ private System.Windows.Forms.ToolStripMenuItem mnuViewDrawPiercePoints; private System.Windows.Forms.ToolStripMenuItem mnuViewDrawBounds; private System.Windows.Forms.ToolStripMenuItem mnuViewDrawOffset; + private System.Windows.Forms.ToolStripMenuItem mnuViewDrawCutDirection; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem5; private System.Windows.Forms.ToolStripMenuItem mnuTools; private System.Windows.Forms.ToolStripMenuItem mnuToolsMachineConfig; diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 1636740..3bcf5ae 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -396,6 +396,7 @@ namespace OpenNest.Forms mnuViewDrawRapids.Checked = activeForm.PlateView.DrawRapid; mnuViewDrawPiercePoints.Checked = activeForm.PlateView.DrawPiercePoints; mnuViewDrawBounds.Checked = activeForm.PlateView.DrawBounds; + mnuViewDrawCutDirection.Checked = activeForm.PlateView.DrawCutDirection; statusLabel1.Text = activeForm.PlateView.Status; } @@ -585,6 +586,13 @@ namespace OpenNest.Forms mnuViewDrawOffset.Checked = activeForm.PlateView.DrawOffset; } + private void ToggleDrawCutDirection_Click(object sender, EventArgs e) + { + if (activeForm == null) return; + activeForm.ToggleCutDirection(); + mnuViewDrawCutDirection.Checked = activeForm.PlateView.DrawCutDirection; + } + private void ZoomToArea_Click(object sender, EventArgs e) { if (activeForm == null) return;