Files
OpenNest/OpenNest/Controls/ProgramEditorControl.cs

471 lines
16 KiB
C#

using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace OpenNest.Controls
{
public partial class ProgramEditorControl : UserControl
{
private List<ContourInfo> contours = new();
private bool isDirty;
private bool isLoaded;
public ProgramEditorControl()
{
InitializeComponent();
contourList.DrawItem += OnDrawContourItem;
contourList.MeasureItem += OnMeasureContourItem;
contourList.SelectedIndexChanged += OnContourSelectionChanged;
reverseButton.Click += OnReverseClicked;
menuReverse.Click += OnReverseClicked;
menuMoveUp.Click += OnMoveUpClicked;
menuMoveDown.Click += OnMoveDownClicked;
menuSequence.Click += OnSequenceClicked;
contourMenu.Opening += OnContourMenuOpening;
applyButton.Click += OnApplyClicked;
preview.PaintOverlay = OnPreviewPaintOverlay;
}
public Program Program { get; private set; }
public bool IsDirty => isDirty;
public bool IsLoaded => isLoaded;
public event EventHandler ProgramChanged;
public void LoadEntities(List<Entity> entities)
{
var shapes = ShapeBuilder.GetShapes(entities);
if (shapes.Count == 0)
{
Clear();
return;
}
contours = ContourInfo.Classify(shapes);
// Assign contour-type colors once so the CAD view also picks them up
foreach (var contour in contours)
{
var color = GetContourColor(contour.Type, false);
foreach (var entity in contour.Shape.Entities)
entity.Color = color;
}
Program = BuildProgram(contours);
isDirty = false;
isLoaded = true;
PopulateContourList();
UpdateGcodeText();
RefreshPreview();
}
public void Clear()
{
contours.Clear();
contourList.Items.Clear();
preview.Entities.Clear();
preview.Invalidate();
gcodeEditor.Clear();
Program = null;
isDirty = false;
isLoaded = false;
}
private static Program BuildProgram(List<ContourInfo> contours)
{
var pgm = new Program();
foreach (var contour in contours)
{
var sub = ConvertGeometry.ToProgram(contour.Shape);
pgm.Merge(sub);
}
pgm.Mode = Mode.Incremental;
return pgm;
}
private void PopulateContourList()
{
contourList.Items.Clear();
foreach (var contour in contours)
contourList.Items.Add(contour);
}
private void UpdateGcodeText()
{
gcodeEditor.Text = Program != null ? FormatProgram(Program) : string.Empty;
}
private static string FormatProgram(Program pgm)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine(pgm.Mode == Mode.Absolute ? "G90" : "G91");
var lastWasRapid = false;
foreach (var code in pgm.Codes)
{
if (code is RapidMove rapid)
{
if (!lastWasRapid && sb.Length > 0)
sb.AppendLine();
sb.AppendLine($"G00 X{FormatCoord(rapid.EndPoint.X)} Y{FormatCoord(rapid.EndPoint.Y)}");
lastWasRapid = true;
}
else if (code is ArcMove arc)
{
var g = arc.Rotation == RotationType.CW ? "G02" : "G03";
sb.AppendLine($"{g} X{FormatCoord(arc.EndPoint.X)} Y{FormatCoord(arc.EndPoint.Y)} I{FormatCoord(arc.CenterPoint.X)} J{FormatCoord(arc.CenterPoint.Y)}");
lastWasRapid = false;
}
else if (code is LinearMove linear)
{
sb.AppendLine($"G01 X{FormatCoord(linear.EndPoint.X)} Y{FormatCoord(linear.EndPoint.Y)}");
lastWasRapid = false;
}
}
return sb.ToString();
}
private static string FormatCoord(double value)
{
return System.Math.Round(value, 4).ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
}
private void RefreshPreview()
{
preview.ClearPenCache();
preview.Entities.Clear();
// Restore base colors first (undo any selection highlight)
foreach (var contour in contours)
{
var baseColor = GetContourColor(contour.Type, false);
foreach (var entity in contour.Shape.Entities)
entity.Color = baseColor;
}
for (var i = 0; i < contours.Count; i++)
{
var contour = contours[i];
var selected = contourList.SelectedIndices.Contains(i);
if (selected)
{
var selColor = GetContourColor(contour.Type, true);
foreach (var entity in contour.Shape.Entities)
entity.Color = selColor;
}
preview.Entities.AddRange(contour.Shape.Entities);
}
preview.ZoomToFit();
preview.Invalidate();
}
private static Color GetContourColor(ContourClassification type, bool selected)
{
if (selected)
return Color.White;
return type switch
{
ContourClassification.Perimeter => Color.FromArgb(80, 180, 120),
ContourClassification.Hole => Color.FromArgb(100, 140, 255),
ContourClassification.Etch => Color.FromArgb(255, 170, 50),
ContourClassification.Open => Color.FromArgb(200, 200, 100),
_ => Color.Gray,
};
}
private void OnMeasureContourItem(object sender, MeasureItemEventArgs e)
{
e.ItemHeight = 42;
}
private void OnDrawContourItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0 || e.Index >= contours.Count) return;
var contour = contours[e.Index];
var selected = (e.State & DrawItemState.Selected) != 0;
var bounds = e.Bounds;
// Background
using var bgBrush = new SolidBrush(selected
? Color.FromArgb(230, 238, 255)
: Color.White);
e.Graphics.FillRectangle(bgBrush, bounds);
// Accent bar
var accentColor = GetContourColor(contour.Type, false);
using var accentBrush = new SolidBrush(accentColor);
e.Graphics.FillRectangle(accentBrush, bounds.X, bounds.Y + 4, 3, bounds.Height - 8);
// Direction icon
var icon = contour.Type switch
{
ContourClassification.Perimeter or ContourClassification.Hole =>
contour.DirectionLabel == "CW" ? "\u21BB" : "\u21BA",
ContourClassification.Etch => "\u2014",
_ => "\u2014",
};
using var iconFont = new Font("Segoe UI", 14f);
using var iconBrush = new SolidBrush(accentColor);
e.Graphics.DrawString(icon, iconFont, iconBrush, bounds.X + 8, bounds.Y + 6);
// Label
using var labelFont = new Font("Segoe UI", 9f, FontStyle.Bold);
using var labelBrush = new SolidBrush(Color.FromArgb(40, 40, 40));
e.Graphics.DrawString(contour.Label, labelFont, labelBrush, bounds.X + 32, bounds.Y + 4);
// Info line
var info = $"{contour.DirectionLabel} \u00B7 {contour.DimensionLabel}";
using var infoFont = new Font("Segoe UI", 8f);
using var infoBrush = new SolidBrush(Color.Gray);
e.Graphics.DrawString(info, infoFont, infoBrush, bounds.X + 32, bounds.Y + 22);
// Separator
using var sepPen = new Pen(Color.FromArgb(230, 230, 230));
e.Graphics.DrawLine(sepPen, bounds.X + 8, bounds.Bottom - 1, bounds.Right - 8, bounds.Bottom - 1);
}
private void OnContourSelectionChanged(object sender, EventArgs e)
{
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 OnContourMenuOpening(object sender, System.ComponentModel.CancelEventArgs e)
{
var index = contourList.SelectedIndex;
var perimeterIndex = contours.FindIndex(c => c.Type == ContourClassification.Perimeter);
var isPerimeter = index >= 0 && index == perimeterIndex;
// Can't move perimeter, can't move past perimeter
menuMoveUp.Enabled = index > 0 && !isPerimeter;
menuMoveDown.Enabled = index >= 0 && index < perimeterIndex - 1;
}
private void OnMoveUpClicked(object sender, EventArgs e)
{
var index = contourList.SelectedIndex;
if (index <= 0) return;
if (contours[index].Type == ContourClassification.Perimeter) return;
(contours[index], contours[index - 1]) = (contours[index - 1], contours[index]);
RebuildAfterReorder(index - 1);
}
private void OnMoveDownClicked(object sender, EventArgs e)
{
var index = contourList.SelectedIndex;
if (index < 0 || index >= contours.Count - 1) return;
if (contours[index].Type == ContourClassification.Perimeter) return;
if (contours[index + 1].Type == ContourClassification.Perimeter) return;
(contours[index], contours[index + 1]) = (contours[index + 1], contours[index]);
RebuildAfterReorder(index + 1);
}
private void OnSequenceClicked(object sender, EventArgs e)
{
// Nearest-neighbor sort for non-perimeter contours
var perimeterIndex = contours.FindIndex(c => c.Type == ContourClassification.Perimeter);
if (perimeterIndex < 0) return;
var nonPerimeter = contours.Where(c => c.Type != ContourClassification.Perimeter).ToList();
if (nonPerimeter.Count <= 1) return;
var sorted = new List<ContourInfo>();
var remaining = new List<ContourInfo>(nonPerimeter);
var pos = new Vector();
while (remaining.Count > 0)
{
var nearest = 0;
var nearestDist = double.MaxValue;
for (var i = 0; i < remaining.Count; i++)
{
var startPt = GetContourStartPoint(remaining[i]);
var dist = pos.DistanceTo(startPt);
if (dist < nearestDist)
{
nearestDist = dist;
nearest = i;
}
}
var next = remaining[nearest];
sorted.Add(next);
pos = GetContourEndPoint(next);
remaining.RemoveAt(nearest);
}
// Put perimeter back at the end
sorted.Add(contours[perimeterIndex]);
contours = sorted;
RebuildAfterReorder(-1);
}
private static Vector GetContourStartPoint(ContourInfo contour)
{
var entity = contour.Shape.Entities[0];
return entity switch
{
Line line => line.StartPoint,
Arc arc => arc.StartPoint(),
Circle circle => new Vector(circle.Center.X + circle.Radius, circle.Center.Y),
_ => new Vector(),
};
}
private static Vector GetContourEndPoint(ContourInfo contour)
{
var entity = contour.Shape.Entities[^1];
return entity switch
{
Line line => line.EndPoint,
Arc arc => arc.EndPoint(),
Circle circle => new Vector(circle.Center.X + circle.Radius, circle.Center.Y),
_ => new Vector(),
};
}
private void RebuildAfterReorder(int selectIndex)
{
RelabelContours();
Program = BuildProgram(contours);
isDirty = true;
PopulateContourList();
if (selectIndex >= 0 && selectIndex < contourList.Items.Count)
contourList.SelectedIndex = selectIndex;
UpdateGcodeText();
RefreshPreview();
ProgramChanged?.Invoke(this, EventArgs.Empty);
}
private void RelabelContours()
{
var holeCount = 0;
var etchCount = 0;
var openCount = 0;
foreach (var contour in contours)
{
switch (contour.Type)
{
case ContourClassification.Hole:
holeCount++;
contour.SetLabel($"Hole {holeCount}");
break;
case ContourClassification.Etch:
etchCount++;
contour.SetLabel(etchCount == 1 ? "Etch" : $"Etch {etchCount}");
break;
case ContourClassification.Open:
openCount++;
contour.SetLabel(openCount == 1 ? "Open" : $"Open {openCount}");
break;
}
}
}
private void OnPreviewPaintOverlay(Graphics g)
{
if (contours.Count == 0) return;
var spacing = preview.LengthGuiToWorld(60f);
var arrowSize = 8f;
using var pen = new Pen(Color.LightGray, 1.5f);
for (var i = 0; i < contours.Count; i++)
{
if (!contourList.SelectedIndices.Contains(i)) continue;
var contour = contours[i];
var pgm = ConvertGeometry.ToProgram(contour.Shape);
if (pgm == null) continue;
var pos = new Vector();
CutDirectionArrows.DrawProgram(g, preview, pgm, ref pos, pen, spacing, arrowSize);
}
}
private void OnApplyClicked(object sender, EventArgs e)
{
var text = gcodeEditor.Text;
if (string.IsNullOrWhiteSpace(text))
{
MessageBox.Show("G-code is empty.", "Apply", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
using var stream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(text));
var reader = new ProgramReader(stream);
var parsed = reader.Read();
if (parsed == null || parsed.Length == 0)
{
MessageBox.Show("No valid G-code found.", "Apply", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// Rebuild shapes from the parsed program
var entities = ConvertProgram.ToGeometry(parsed);
var shapes = ShapeBuilder.GetShapes(entities);
if (shapes.Count == 0)
{
MessageBox.Show("No contours found in parsed G-code.", "Apply", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
contours = ContourInfo.Classify(shapes);
Program = parsed;
isDirty = true;
PopulateContourList();
RefreshPreview();
ProgramChanged?.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
MessageBox.Show($"Error parsing G-code: {ex.Message}", "Apply",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}