Files
OpenNest/OpenNest/Forms/CadConverterForm.cs

733 lines
26 KiB
C#

using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Controls;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
using OpenNest.IO.Bending;
using OpenNest.Properties;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace OpenNest.Forms
{
public partial class CadConverterForm : Form
{
private static int colorIndex;
private SimplifierViewerForm simplifierViewer;
private bool staleProgram = true;
public CadConverterForm()
{
InitializeComponent();
fileList.SelectedIndexChanged += OnFileSelected;
filterPanel.FilterChanged += OnFilterChanged;
filterPanel.BendLineSelected += OnBendLineSelected;
filterPanel.BendLineRemoved += OnBendLineRemoved;
filterPanel.AddBendLineClicked += OnAddBendLineClicked;
entityView1.LinePicked += OnLinePicked;
entityView1.PickCancelled += OnPickCancelled;
btnSplit.Click += OnSplitClicked;
numQuantity.ValueChanged += OnQuantityChanged;
txtCustomer.TextChanged += OnCustomerChanged;
cboBendDetector.SelectedIndexChanged += OnBendDetectorChanged;
// Populate bend detector dropdown
cboBendDetector.Items.Add("Auto");
foreach (var detector in BendDetectorRegistry.Detectors)
cboBendDetector.Items.Add(detector.Name);
cboBendDetector.SelectedIndex = 0;
viewTabs.SelectedIndexChanged += OnViewTabChanged;
// Drag & drop
AllowDrop = true;
DragEnter += OnDragEnter;
DragDrop += OnDragDrop;
}
private FileListItem CurrentItem => fileList.SelectedItem;
#region File Import
public void AddFile(string file) => AddFile(file, 0, null);
private void AddFile(string file, int detectorIndex, string detectorName)
{
try
{
var importer = new DxfImporter();
importer.SplinePrecision = Settings.Default.ImportSplinePrecision;
var result = importer.Import(file);
if (result.Entities.Count == 0)
return;
// Compute bounds
var bounds = result.Entities.GetBoundingBox();
// Detect bends (detectorIndex/Name captured on UI thread)
var bends = new List<Bend>();
if (result.Document != null)
{
bends = detectorIndex == 0
? BendDetectorRegistry.AutoDetect(result.Document)
: BendDetectorRegistry.GetByName(detectorName)
?.DetectBends(result.Document)
?? new List<Bend>();
}
Bend.UpdateEtchEntities(result.Entities, bends);
var item = new FileListItem
{
Name = Path.GetFileNameWithoutExtension(file),
Entities = result.Entities,
Path = file,
Quantity = 1,
Customer = string.Empty,
Bends = bends,
Bounds = bounds,
EntityCount = result.Entities.Count
};
if (InvokeRequired)
BeginInvoke((Action)(() => fileList.AddItem(item)));
else
fileList.AddItem(item);
}
catch (Exception ex)
{
MessageBox.Show($"Error importing \"{file}\": {ex.Message}");
}
}
public void AddFiles(IEnumerable<string> files)
{
var fileArray = files.ToArray();
// Capture UI state on main thread before entering parallel loop
var detectorIndex = cboBendDetector.SelectedIndex;
var detectorName = cboBendDetector.SelectedItem?.ToString();
System.Threading.Tasks.Task.Run(() =>
{
Parallel.ForEach(fileArray, file => AddFile(file, detectorIndex, detectorName));
});
}
#endregion
#region Event Handlers
private void OnFileSelected(object sender, int index)
{
var item = CurrentItem;
if (item == null)
{
ClearDetailBar();
return;
}
LoadItem(item);
staleProgram = true;
if (viewTabs.SelectedTab == tabProgram)
LoadProgramTab();
else
programEditor.Clear();
}
private void LoadItem(FileListItem item)
{
entityView1.ClearPenCache();
if (entityView1.IsPickingBendLine)
{
entityView1.IsPickingBendLine = false;
filterPanel.SetPickMode(false);
}
entityView1.OriginalEntities = chkShowOriginal.Checked ? item.OriginalEntities : null;
entityView1.Entities.Clear();
entityView1.Entities.AddRange(item.Entities);
entityView1.Bends = item.Bends ?? new List<Bend>();
item.Entities.ForEach(e => e.IsVisible = true);
if (item.Entities.Any(e => e.Layer != null))
item.Entities.ForEach(e => e.Layer.IsVisible = true);
ReHidePromotedEntities(item.Bends);
ApplyContourColors(item.Entities);
filterPanel.LoadItem(item.Entities, item.Bends);
numQuantity.Value = item.Quantity;
txtCustomer.Text = item.Customer ?? "";
var bounds = item.Bounds;
lblDimensions.Text = bounds != null
? $"{bounds.Width:0.#} x {bounds.Length:0.#}"
: "";
lblEntityCount.Text = $"{item.EntityCount} entities";
entityView1.ZoomToFit();
CheckSimplifiable(item);
}
private static void ApplyContourColors(List<Entity> entities)
{
var visible = entities.Where(e => e.IsVisible && e.Layer != null && e.Layer.IsVisible).ToList();
if (visible.Count == 0) return;
var shapes = ShapeBuilder.GetShapes(visible);
if (shapes.Count == 0) return;
var contours = ContourInfo.Classify(shapes);
foreach (var contour in contours)
{
var color = contour.Type switch
{
ContourClassification.Perimeter => System.Drawing.Color.FromArgb(80, 180, 120),
ContourClassification.Hole => System.Drawing.Color.FromArgb(100, 140, 255),
ContourClassification.Etch => System.Drawing.Color.FromArgb(255, 170, 50),
ContourClassification.Open => System.Drawing.Color.FromArgb(200, 200, 100),
_ => System.Drawing.Color.Gray,
};
foreach (var entity in contour.Shape.Entities)
entity.Color = color;
}
}
private void CheckSimplifiable(FileListItem item)
{
ResetSimplifyButton();
// Only check original (unsimplified) entities
var entities = item.OriginalEntities ?? item.Entities;
if (entities == null || entities.Count < 10) return;
// Quick line count check — need at least MinLines consecutive lines
var lineCount = entities.Count(e => e is Geometry.Line);
if (lineCount < 3) return;
// Run a quick analysis on a background thread
var capturedEntities = new List<Entity>(entities);
Task.Run(() =>
{
var shapes = ShapeBuilder.GetShapes(capturedEntities);
var simplifier = new GeometrySimplifier();
var count = 0;
foreach (var shape in shapes)
count += simplifier.Analyze(shape).Count;
return count;
}).ContinueWith(t =>
{
if (t.IsCompletedSuccessfully && t.Result > 0)
HighlightSimplifyButton(t.Result);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
private void HighlightSimplifyButton(int candidateCount)
{
btnSimplify.Text = $"Simplify ({candidateCount})";
btnSimplify.BackColor = Color.FromArgb(60, 120, 60);
btnSimplify.ForeColor = Color.White;
}
private void ResetSimplifyButton()
{
btnSimplify.Text = "Simplify...";
btnSimplify.BackColor = SystemColors.Control;
btnSimplify.ForeColor = SystemColors.ControlText;
}
private void ClearDetailBar()
{
numQuantity.Value = 1;
txtCustomer.Text = "";
lblDimensions.Text = "";
lblEntityCount.Text = "";
entityView1.Entities.Clear();
entityView1.Invalidate();
}
private void OnFilterChanged(object sender, EventArgs e)
{
var item = CurrentItem;
if (item == null) return;
filterPanel.ApplyFilters(item.Entities);
ReHidePromotedEntities(item.Bends);
entityView1.Invalidate();
staleProgram = true;
}
private void OnViewTabChanged(object sender, EventArgs e)
{
if (viewTabs.SelectedTab == tabProgram && staleProgram)
LoadProgramTab();
}
private void LoadProgramTab()
{
var item = CurrentItem;
if (item == null)
{
programEditor.Clear();
staleProgram = false;
return;
}
var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList();
if (entities.Count == 0)
{
programEditor.Clear();
staleProgram = false;
return;
}
var normalized = ShapeProfile.NormalizeEntities(entities);
programEditor.LoadEntities(normalized);
staleProgram = false;
// Refresh CAD view to show contour-type colors
entityView1.ClearPenCache();
entityView1.Invalidate();
}
private void OnBendLineSelected(object sender, int index)
{
entityView1.SelectedBendIndex = index;
entityView1.Invalidate();
}
private void OnBendLineRemoved(object sender, int index)
{
var item = CurrentItem;
if (item == null || index < 0 || index >= item.Bends.Count) return;
var bend = item.Bends[index];
if (bend.SourceEntity != null)
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);
entityView1.Invalidate();
}
private void OnQuantityChanged(object sender, EventArgs e)
{
var item = CurrentItem;
if (item == null) return;
item.Quantity = (int)numQuantity.Value;
fileList.Invalidate();
}
private void OnCustomerChanged(object sender, EventArgs e)
{
var item = CurrentItem;
if (item != null)
item.Customer = txtCustomer.Text;
}
private void OnBendDetectorChanged(object sender, EventArgs e)
{
// Re-run bend detection on current item if it has a document
// For now, bend detection only runs at import time
}
private void OnSplitClicked(object sender, EventArgs e)
{
var item = CurrentItem;
if (item == null) return;
var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList();
if (entities.Count == 0) return;
var normalized = ShapeProfile.NormalizeEntities(entities);
var pgm = ConvertGeometry.ToProgram(normalized);
var originOffset = Vector.Zero;
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
{
var rapid = (RapidMove)pgm[0];
originOffset = rapid.EndPoint;
pgm.Offset(-originOffset);
pgm.Codes.RemoveAt(0);
}
var drawing = new Drawing(item.Name, pgm);
drawing.Bends = item.Bends.Select(b => new Bend
{
StartPoint = new Vector(b.StartPoint.X - originOffset.X, b.StartPoint.Y - originOffset.Y),
EndPoint = new Vector(b.EndPoint.X - originOffset.X, b.EndPoint.Y - originOffset.Y),
Direction = b.Direction,
Angle = b.Angle,
Radius = b.Radius,
NoteText = b.NoteText,
}).ToList();
using var form = new SplitDrawingForm(drawing);
if (form.ShowDialog(this) != DialogResult.OK || form.ResultDrawings?.Count <= 1)
return;
// Write split DXF files and re-import
var sourceDir = Path.GetDirectoryName(item.Path);
var baseName = Path.GetFileNameWithoutExtension(item.Path);
var writableDir = Directory.Exists(sourceDir) && IsDirectoryWritable(sourceDir)
? sourceDir
: Path.GetTempPath();
var index = fileList.SelectedIndex;
var newItems = new List<string>();
var splitWriter = new SplitDxfWriter();
var splitItems = new List<FileListItem>();
for (var i = 0; i < form.ResultDrawings.Count; i++)
{
var splitDrawing = form.ResultDrawings[i];
var splitName = $"{baseName}-{i + 1}.dxf";
var splitPath = GetUniquePath(Path.Combine(writableDir, splitName));
splitWriter.Write(splitPath, splitDrawing);
newItems.Add(splitPath);
// Re-import geometry but keep bends from the split drawing
var importer = new DxfImporter();
importer.SplinePrecision = Settings.Default.ImportSplinePrecision;
var result = importer.Import(splitPath);
var splitItem = new FileListItem
{
Name = Path.GetFileNameWithoutExtension(splitPath),
Entities = result.Entities,
Path = splitPath,
Quantity = item.Quantity,
Customer = item.Customer,
Bends = splitDrawing.Bends ?? new List<Bend>(),
Bounds = result.Entities.GetBoundingBox(),
EntityCount = result.Entities.Count
};
splitItems.Add(splitItem);
}
// Remove original and add split items directly (preserving bend info)
fileList.RemoveAt(index);
foreach (var splitItem in splitItems)
fileList.AddItem(splitItem);
if (writableDir != sourceDir)
MessageBox.Show($"Split files written to: {writableDir}", "Split Output",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void OnAddBendLineClicked(object sender, EventArgs e)
{
var active = !entityView1.IsPickingBendLine;
entityView1.IsPickingBendLine = active;
filterPanel.SetPickMode(active);
}
private void OnLinePicked(object sender, Line line)
{
using var dialog = new BendLineDialog();
if (dialog.ShowDialog(this) != DialogResult.OK)
return;
var item = CurrentItem;
if (item == null) return;
var bend = new Bend
{
StartPoint = line.StartPoint,
EndPoint = line.EndPoint,
Direction = dialog.Direction,
Angle = dialog.BendAngle,
Radius = dialog.BendRadius,
SourceEntity = line
};
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();
}
private void OnPickCancelled(object sender, EventArgs e)
{
entityView1.IsPickingBendLine = false;
filterPanel.SetPickMode(false);
}
private void OnDragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
e.Effect = DragDropEffects.Copy;
}
private void OnDragDrop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
var dxfFiles = files.Where(f =>
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)).ToArray();
if (dxfFiles.Length > 0)
AddFiles(dxfFiles);
}
}
private void OnSimplifyClick(object sender, EventArgs e)
{
if (entityView1.Entities == null || entityView1.Entities.Count == 0)
return;
// Always simplify from original geometry to prevent tolerance creep
var item = CurrentItem;
if (item != null && item.OriginalEntities == null)
item.OriginalEntities = new List<Entity>(item.Entities);
var sourceEntities = item?.OriginalEntities ?? entityView1.Entities;
var shapes = ShapeBuilder.GetShapes(sourceEntities);
if (shapes.Count == 0)
return;
if (simplifierViewer == null || simplifierViewer.IsDisposed)
{
simplifierViewer = new SimplifierViewerForm();
simplifierViewer.Owner = this;
simplifierViewer.Applied += OnSimplifierApplied;
// Position next to this form
var screen = Screen.FromControl(this);
simplifierViewer.Location = new Point(
System.Math.Min(Right, screen.WorkingArea.Right - simplifierViewer.Width),
Top);
}
simplifierViewer.LoadShapes(shapes, entityView1);
}
private void OnSimplifierApplied(List<Entity> entities)
{
entityView1.Entities.Clear();
entityView1.Entities.AddRange(entities);
entityView1.ZoomToFit();
entityView1.Invalidate();
var item = CurrentItem;
if (item != null)
{
item.Entities = entities;
item.EntityCount = entities.Count;
item.Bounds = entities.GetBoundingBox();
}
lblEntityCount.Text = $"{entities.Count} entities";
ResetSimplifyButton();
}
private void OnShowOriginalChanged(object sender, EventArgs e)
{
var item = CurrentItem;
entityView1.OriginalEntities = chkShowOriginal.Checked ? item?.OriginalEntities : null;
entityView1.Invalidate();
}
private void OnLabelsChanged(object sender, EventArgs e)
{
entityView1.ShowEntityLabels = chkLabels.Checked;
entityView1.Invalidate();
}
private void OnExportDxfClick(object sender, EventArgs e)
{
var item = CurrentItem;
if (item == null) return;
using var dlg = new SaveFileDialog
{
Filter = "DXF 2018 (*.dxf)|*.dxf|" +
"DXF 2013 (*.dxf)|*.dxf|" +
"DXF 2010 (*.dxf)|*.dxf|" +
"DXF 2007 (*.dxf)|*.dxf|" +
"DXF 2004 (*.dxf)|*.dxf|" +
"DXF 2000 (*.dxf)|*.dxf|" +
"DXF R14 (*.dxf)|*.dxf",
FileName = Path.ChangeExtension(item.Name, ".dxf"),
};
if (dlg.ShowDialog() != DialogResult.OK) return;
var version = dlg.FilterIndex switch
{
2 => ACadSharp.ACadVersion.AC1027,
3 => ACadSharp.ACadVersion.AC1024,
4 => ACadSharp.ACadVersion.AC1021,
5 => ACadSharp.ACadVersion.AC1018,
6 => ACadSharp.ACadVersion.AC1015,
7 => ACadSharp.ACadVersion.AC1014,
_ => ACadSharp.ACadVersion.AC1032,
};
var doc = new ACadSharp.CadDocument(version);
foreach (var entity in item.Entities)
{
switch (entity)
{
case Geometry.Line line:
doc.Entities.Add(new ACadSharp.Entities.Line
{
StartPoint = new CSMath.XYZ(line.StartPoint.X, line.StartPoint.Y, 0),
EndPoint = new CSMath.XYZ(line.EndPoint.X, line.EndPoint.Y, 0),
});
break;
case Geometry.Arc arc:
var startAngle = arc.StartAngle;
var endAngle = arc.EndAngle;
if (arc.IsReversed)
OpenNest.Math.Generic.Swap(ref startAngle, ref endAngle);
doc.Entities.Add(new ACadSharp.Entities.Arc
{
Center = new CSMath.XYZ(arc.Center.X, arc.Center.Y, 0),
Radius = arc.Radius,
StartAngle = startAngle,
EndAngle = endAngle,
});
break;
case Geometry.Circle circle:
doc.Entities.Add(new ACadSharp.Entities.Circle
{
Center = new CSMath.XYZ(circle.Center.X, circle.Center.Y, 0),
Radius = circle.Radius,
});
break;
}
}
using var writer = new ACadSharp.IO.DxfWriter(dlg.FileName, doc, false);
writer.Write();
}
#endregion
#region Output
public List<Drawing> GetDrawings()
{
var drawings = new List<Drawing>();
foreach (var item in fileList.Items)
{
var entities = item.Entities.Where(e => e.Layer.IsVisible && e.IsVisible).ToList();
if (entities.Count == 0)
continue;
var drawing = new Drawing(item.Name);
drawing.Color = GetNextColor();
drawing.Customer = item.Customer;
drawing.Source.Path = item.Path;
drawing.Quantity.Required = item.Quantity;
// Copy bends
if (item.Bends != null)
drawing.Bends.AddRange(item.Bends);
var normalized = ShapeProfile.NormalizeEntities(entities);
var pgm = ConvertGeometry.ToProgram(normalized);
var firstCode = pgm[0];
if (firstCode.Type == CodeType.RapidMove)
{
var rapid = (RapidMove)firstCode;
drawing.Source.Offset = rapid.EndPoint;
pgm.Offset(-rapid.EndPoint);
pgm.Codes.RemoveAt(0);
}
if (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null)
drawing.Program = programEditor.Program;
else
drawing.Program = pgm;
drawings.Add(drawing);
Thread.Sleep(20);
}
return drawings;
}
#endregion
#region Helpers
private static void ReHidePromotedEntities(List<Bend> bends)
{
if (bends == null) return;
foreach (var bend in bends)
{
if (bend.SourceEntity != null)
bend.SourceEntity.IsVisible = false;
}
}
private static Color GetNextColor()
{
var color = ColorScheme.PartColors[colorIndex % ColorScheme.PartColors.Length];
colorIndex++;
return color;
}
private static bool IsDirectoryWritable(string path)
{
try
{
var testFile = Path.Combine(path, $".writetest_{Guid.NewGuid()}");
File.WriteAllText(testFile, "");
File.Delete(testFile);
return true;
}
catch { return false; }
}
private static string GetUniquePath(string path)
{
if (!File.Exists(path)) return path;
var dir = Path.GetDirectoryName(path);
var name = Path.GetFileNameWithoutExtension(path);
var ext = Path.GetExtension(path);
var counter = 2;
while (File.Exists(path))
{
path = Path.Combine(dir, $"{name}_{counter}{ext}");
counter++;
}
return path;
}
#endregion
}
}