Refactored MainForm

This commit is contained in:
AJ
2025-09-29 13:22:47 -04:00
parent 6b37f0f6f7
commit 2d5ffdf5c0
37 changed files with 3235 additions and 862 deletions

View File

@@ -1,77 +0,0 @@
using OfficeOpenXml;
using System;
using System.Collections.Generic;
using System.IO;
namespace ExportDXF
{
public class BomToExcel
{
private const string DefaultTemplatePath = "Templates/BomTemplate.xlsx";
private const string DefaultSheetName = "Parts";
public string TemplatePath { get; set; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultTemplatePath);
public void CreateBOMExcelFile(string filepath, IList<Item> items)
{
try
{
CopyTemplate(filepath);
using (var pkg = new ExcelPackage(new FileInfo(filepath)))
{
var partsSheet = pkg.Workbook.Worksheets[DefaultSheetName] ?? throw new Exception($"Worksheet '{DefaultSheetName}' not found in template.");
PopulateSheet(partsSheet, items);
AutoFitColumns(partsSheet);
pkg.Save();
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to create BOM Excel file: {ex.Message}");
throw;
}
}
private void CopyTemplate(string filepath)
{
if (!File.Exists(TemplatePath))
throw new FileNotFoundException("Template file not found.", TemplatePath);
File.Copy(TemplatePath, filepath, true);
}
private void PopulateSheet(ExcelWorksheet sheet, IList<Item> items)
{
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
var row = i + 2; // Assuming row 1 is the header
var col = 1;
sheet.Cells[row, col++].Value = item.ItemNo;
sheet.Cells[row, col++].Value = item.FileName;
sheet.Cells[row, col++].Value = item.Quantity;
sheet.Cells[row, col++].Value = item.Description;
sheet.Cells[row, col++].Value = item.PartName;
sheet.Cells[row, col++].Value = item.Configuration;
sheet.Cells[row, col++].Value = item.Thickness > 0 ? (object)item.Thickness : null;
sheet.Cells[row, col++].Value = item.Material;
sheet.Cells[row, col++].Value = item.KFactor > 0 ? (object)item.KFactor : null;
sheet.Cells[row, col++].Value = item.BendRadius > 0 ? (object)item.BendRadius : null;
}
}
private void AutoFitColumns(ExcelWorksheet sheet)
{
for (int i = 1; i <= sheet.Dimension.Columns; i++)
{
var column = sheet.Column(i);
column.AutoFit();
column.Width += 1; // Adding padding for better visibility
}
}
}
}

View File

@@ -94,18 +94,34 @@
<Compile Include="BendOrientation.cs" />
<Compile Include="Bounds.cs" />
<Compile Include="DrawingInfo.cs" />
<Compile Include="BomToExcel.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="Extensions\StringExtensions.cs" />
<Compile Include="Extensions\SolidWorksExtensions.cs" />
<Compile Include="Extensions\TimeSpanExtensions.cs" />
<Compile Include="Extensions\UIExtensions.cs" />
<Compile Include="Extensions\UnitConversionExtensions.cs" />
<Compile Include="ItemExtractors\AssemblyItemExtractor.cs" />
<Compile Include="ItemExtractors\BomColumnIndices.cs" />
<Compile Include="ItemExtractors\BomItemExtractor.cs" />
<Compile Include="ItemExtractors\ItemExtractor.cs" />
<Compile Include="Forms\ViewFlipDeciderComboboxItem.cs" />
<Compile Include="Item.cs" />
<Compile Include="Models\DocumentType.cs" />
<Compile Include="Models\ExportContext.cs" />
<Compile Include="Models\Item.cs" />
<Compile Include="Models\SolidWorksDocument.cs" />
<Compile Include="Services\BomExcelExporter.cs" />
<Compile Include="Services\BomExtractor.cs" />
<Compile Include="Services\BomExcelSettings.cs" />
<Compile Include="Services\DrawingExporter.cs" />
<Compile Include="Services\DxfExportService.cs" />
<Compile Include="Services\PartExporter.cs" />
<Compile Include="Services\SolidWorksService.cs" />
<Compile Include="Utilities\SheetMetalProperties.cs" />
<Compile Include="Utilities\SolidWorksHelper.cs" />
<Compile Include="Utilities\TextHelper.cs" />
<Compile Include="Utilities\ViewHelper.cs" />
<Compile Include="ViewFlipDeciders\AskViewFlipDecider.cs" />
<Compile Include="ViewFlipDeciders\AutoViewFlipDecider.cs" />
<Compile Include="ViewFlipDeciders\IViewFlipDecider.cs" />
<Compile Include="Helper.cs" />
<Compile Include="Forms\MainForm.cs">
<SubType>Form</SubType>
</Compile>
@@ -114,10 +130,8 @@
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SolidWorksExtensions.cs" />
<Compile Include="Units.cs" />
<Compile Include="ViewFlipDeciders\PreferUpViewFlipDecider.cs" />
<Compile Include="ViewHelper.cs" />
<Compile Include="ViewFlipDeciders\ViewFlipDeciderFactory.cs" />
<EmbeddedResource Include="Forms\MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource>
@@ -185,6 +199,7 @@
<Name>EtchBendLines</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View File

@@ -1,74 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace ExportDXF
{
public static class Extensions
{
public static void AppendText(this RichTextBox box, string text, Color color)
{
box.SelectionStart = box.TextLength;
box.SelectionLength = 0;
box.SelectionColor = color;
box.AppendText(text);
box.SelectionColor = box.ForeColor;
}
public static string ToReadableFormat(this TimeSpan ts)
{
var s = new StringBuilder();
if (ts.TotalHours >= 1)
{
var hrs = ts.Hours + ts.Days * 24.0;
s.Append(string.Format("{0}hrs ", hrs));
s.Append(string.Format("{0}min ", ts.Minutes));
s.Append(string.Format("{0}sec", ts.Seconds));
}
else if (ts.TotalMinutes >= 1)
{
s.Append(string.Format("{0}min ", ts.Minutes));
s.Append(string.Format("{0}sec", ts.Seconds));
}
else
{
s.Append(string.Format("{0} seconds", ts.Seconds));
}
return s.ToString();
}
public static string PunctuateList(this IEnumerable<string> stringList)
{
var list = stringList.ToList();
switch (list.Count)
{
case 0:
return string.Empty;
case 1:
return list[0];
case 2:
return string.Format("{0} and {1}", list[0], list[1]);
default:
var s = string.Empty;
for (int i = 0; i < list.Count - 1; i++)
s += list[i] + ", ";
s += "and " + list.Last();
return s;
}
}
}
}

View File

@@ -5,7 +5,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace ExportDXF
namespace ExportDXF.Extensions
{
public static class SolidWorksExtensions
{

View File

@@ -0,0 +1,48 @@
namespace ExportDXF.Extensions
{
/// <summary>
/// Extension methods for string manipulation.
/// </summary>
public static class StringExtensions
{
/// <summary>
/// Extension method to remove XML tags from a string.
/// </summary>
public static string RemoveXmlTags(this string input)
{
return ExportDXF.Utilities.TextHelper.RemoveXmlTags(input);
}
/// <summary>
/// Extension method to clean text (remove XML and normalize whitespace).
/// </summary>
public static string CleanText(this string input)
{
return ExportDXF.Utilities.TextHelper.CleanText(input);
}
/// <summary>
/// Extension method to sanitize a filename.
/// </summary>
public static string SanitizeFileName(this string input)
{
return ExportDXF.Utilities.TextHelper.SanitizeFileName(input);
}
/// <summary>
/// Extension method to truncate a string.
/// </summary>
public static string Truncate(this string input, int maxLength, bool useEllipsis = true)
{
return ExportDXF.Utilities.TextHelper.Truncate(input, maxLength, useEllipsis);
}
/// <summary>
/// Extension method to convert to title case.
/// </summary>
public static string ToTitleCase(this string input)
{
return ExportDXF.Utilities.TextHelper.ToTitleCase(input);
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
namespace ExportDXF.Extensions
{
/// <summary>
/// Extension methods for TimeSpan formatting.
/// </summary>
public static class TimeSpanExtensions
{
/// <summary>
/// Formats a TimeSpan into a human-readable string.
/// </summary>
/// <param name="ts">The TimeSpan to format.</param>
/// <returns>A human-readable duration string.</returns>
public static string ToReadableFormat(this TimeSpan ts)
{
if (ts.TotalHours >= 1)
{
var totalHours = (int)(ts.TotalDays * 24 + ts.Hours);
return $"{totalHours}hrs {ts.Minutes}min {ts.Seconds}sec";
}
if (ts.TotalMinutes >= 1)
{
return $"{ts.Minutes}min {ts.Seconds}sec";
}
return $"{ts.Seconds} seconds";
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Drawing;
using System.Windows.Forms;
namespace ExportDXF.Extensions
{
public static class UIExtensions
{
public static void AppendText(this RichTextBox box, string text, Color color)
{
box.SelectionStart = box.TextLength;
box.SelectionLength = 0;
box.SelectionColor = color;
box.AppendText(text);
box.SelectionColor = box.ForeColor;
}
}
}

View File

@@ -0,0 +1,41 @@
namespace ExportDXF.Extensions
{
/// <summary>
/// Extension methods for unit conversion between SolidWorks (meters) and millimeters.
/// </summary>
public static class UnitConversionExtensions
{
private const double METERS_TO_MM = .0001;
private const double METERS_TO_INCHES = 0.0254;
/// <summary>
/// Converts a SolidWorks dimension (in meters) to millimeters.
/// </summary>
/// <param name="meters">The value in meters.</param>
/// <returns>The value in millimeters.</returns>
public static double FromSolidWorksToMM(this double meters)
{
return meters * METERS_TO_MM;
}
/// <summary>
/// Converts millimeters to SolidWorks dimension (meters).
/// </summary>
/// <param name="millimeters">The value in millimeters.</param>
/// <returns>The value in meters.</returns>
public static double FromMMToSolidWorks(this double millimeters)
{
return millimeters / METERS_TO_MM;
}
public static double FromSolidWorksToInches(this double meters)
{
return meters * METERS_TO_INCHES;
}
public static double FromInchesToSolidWorks(this double inches)
{
return inches / METERS_TO_INCHES;
}
}
}

View File

@@ -48,7 +48,7 @@
this.activeDocTitleBox.ReadOnly = true;
this.activeDocTitleBox.Size = new System.Drawing.Size(584, 25);
this.activeDocTitleBox.TabIndex = 2;
this.activeDocTitleBox.TextChanged += new System.EventHandler(this.textBox1_TextChanged);
this.activeDocTitleBox.TextChanged += new System.EventHandler(this.activeDocTitleBox_TextChanged);
//
// richTextBox1
//

View File

@@ -1,15 +1,11 @@
using ExportDXF.ItemExtractors;
using ExportDXF.Extensions;
using ExportDXF.Models;
using ExportDXF.Services;
using ExportDXF.ViewFlipDeciders;
using OfficeOpenXml;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -17,627 +13,217 @@ namespace ExportDXF.Forms
{
public partial class MainForm : Form
{
private SldWorks sldWorks;
private BackgroundWorker worker;
private DrawingDoc templateDrawing;
private DateTime timeStarted;
private IViewFlipDecider viewFlipDecider;
private readonly ISolidWorksService _solidWorksService;
private readonly IDxfExportService _exportService;
private CancellationTokenSource _cancellationTokenSource;
public MainForm()
public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService)
{
InitializeComponent();
worker = new BackgroundWorker();
worker.WorkerSupportsCancellation = true;
worker.DoWork += Worker_DoWork;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
_solidWorksService = solidWorksService ??
throw new ArgumentNullException(nameof(solidWorksService));
var type = typeof(IViewFlipDecider);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) && p.IsClass)
.ToList();
_exportService = exportService ??
throw new ArgumentNullException(nameof(exportService));
comboBox1.DataSource = GetItems();
comboBox1.DisplayMember = "Name";
InitializeViewFlipDeciders();
}
protected override void OnLoad(EventArgs e)
~MainForm()
{
_cancellationTokenSource?.Dispose();
_solidWorksService?.Dispose();
components?.Dispose();
Dispose(false);
}
protected override async void OnLoad(EventArgs e)
{
base.OnLoad(e);
button1.Enabled = false;
var task = new Task(ConnectToSolidWorks);
task.ContinueWith((t) =>
{
Invoke(new MethodInvoker(() =>
{
SetActiveDocName();
button1.Enabled = true;
}));
});
task.Start();
await InitializeAsync();
}
private List<ViewFlipDeciderComboboxItem> GetItems()
private async Task InitializeAsync()
{
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => typeof(IViewFlipDecider).IsAssignableFrom(p) && p.IsClass)
try
{
LogMessage("Connecting to SolidWorks, this may take a minute...");
await _solidWorksService.ConnectAsync();
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
LogMessage("Ready", Color.Green);
UpdateActiveDocumentDisplay();
button1.Enabled = true;
}
catch (Exception ex)
{
LogMessage($"Failed to connect to SolidWorks: {ex.Message}", Color.Red);
MessageBox.Show("Failed to connect to SolidWorks.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
Application.Exit();
}
}
private void InitializeViewFlipDeciders()
{
var items = ViewFlipDeciderFactory.GetAvailableDeciders()
.Select(d => new ViewFlipDeciderComboboxItem
{
Name = d.Name,
ViewFlipDecider = d
})
.ToList();
var items = new List<ViewFlipDeciderComboboxItem>();
foreach (var type in types)
{
var obj = (IViewFlipDecider)Activator.CreateInstance(type);
items.Add(new ViewFlipDeciderComboboxItem
{
Name = obj.Name,
ViewFlipDecider = obj
});
}
// Move "Automatic" to the top if it exists
var automatic = items.FirstOrDefault(i => i.Name == "Automatic");
if (automatic != null)
{
items.Remove(automatic);
items.Insert(0, automatic);
}
return items;
comboBox1.DataSource = items;
comboBox1.DisplayMember = "Name";
}
private void button1_Click(object sender, EventArgs e)
private async void button1_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
if (_cancellationTokenSource != null)
{
button1.Enabled = false;
worker.CancelAsync();
return;
CancelExport();
}
else
{
worker.RunWorkerAsync();
await StartExportAsync();
}
}
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
timeStarted = DateTime.Now;
sldWorks.UserControl = false;
sldWorks.ActiveModelDocChangeNotify -= SldWorks_ActiveModelDocChangeNotify;
Invoke(new MethodInvoker(() =>
{
var item = comboBox1.SelectedItem as ViewFlipDeciderComboboxItem;
viewFlipDecider = item.ViewFlipDecider;
activeDocTitleBox.Enabled = false;
prefixTextBox.Enabled = false;
comboBox1.Enabled = false;
button1.Image = Properties.Resources.stop_alt;
if (richTextBox1.TextLength != 0)
richTextBox1.AppendText("\n\n");
}));
var model = sldWorks.ActiveDoc as ModelDoc2;
Print("Started at " + DateTime.Now.ToShortTimeString());
DetermineModelTypeAndExportToDXF(model);
sldWorks.UserControl = true;
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Invoke(new MethodInvoker(() =>
{
if (templateDrawing != null)
{
sldWorks.CloseDoc(((ModelDoc2)templateDrawing).GetTitle());
templateDrawing = null;
}
activeDocTitleBox.Enabled = true;
prefixTextBox.Enabled = true;
comboBox1.Enabled = true;
button1.Image = Properties.Resources.play;
button1.Enabled = true;
}));
var duration = DateTime.Now - timeStarted;
Print("Run time: " + duration.ToReadableFormat());
Print("Done.", Color.Green);
sldWorks.ActiveModelDocChangeNotify += SldWorks_ActiveModelDocChangeNotify;
}
private int SldWorks_ActiveModelDocChangeNotify()
{
Invoke(new MethodInvoker(() =>
{
SetActiveDocName();
UpdatePrefix();
}));
return 1;
}
private void Print(string s)
{
Invoke(new MethodInvoker(() =>
{
richTextBox1.AppendText(s + System.Environment.NewLine);
richTextBox1.ScrollToCaret();
}));
}
private void Print(string s, Color color)
{
Invoke(new MethodInvoker(() =>
{
richTextBox1.AppendText(s + System.Environment.NewLine, color);
richTextBox1.ScrollToCaret();
}));
}
private void ConnectToSolidWorks()
{
Print("Connecting to SolidWorks, this may take a minute...");
sldWorks = Activator.CreateInstance(Type.GetTypeFromProgID("SldWorks.Application")) as SldWorks;
if (sldWorks == null)
{
MessageBox.Show("Failed to connect to SolidWorks.");
Application.Exit();
return;
}
sldWorks.Visible = true;
sldWorks.ActiveModelDocChangeNotify += SldWorks_ActiveModelDocChangeNotify;
Print("Ready", Color.Green);
}
private void SetActiveDocName()
{
var model = sldWorks.ActiveDoc as ModelDoc2;
activeDocTitleBox.Text = model == null ? "<No Document Open>" : model.GetTitle();
}
private void DetermineModelTypeAndExportToDXF(ModelDoc2 model)
{
if (model is PartDoc)
{
Print("Active document is a Part");
ExportToDXF(model as PartDoc);
}
else if (model is DrawingDoc)
{
Print("Active document is a Drawing");
ExportToDXF(model as DrawingDoc);
}
else if (model is AssemblyDoc)
{
Print("Active document is a Assembly");
ExportToDXF(model as AssemblyDoc);
}
}
private string GetPdfFileName(DrawingDoc drawingDoc)
{
var model = drawingDoc as ModelDoc2;
var modelFilePath = model.GetPathName();
var pdfFileName = Path.GetFileNameWithoutExtension(modelFilePath) + ".pdf";
return pdfFileName;
}
private void ExportDrawingToPDF(DrawingDoc drawingDoc, string savePath)
{
var model = drawingDoc as ModelDoc2;
var exportData = sldWorks.GetExportFileData((int)swExportDataFileType_e.swExportPdfData) as ExportPdfData;
exportData.ViewPdfAfterSaving = false;
exportData.SetSheets((int)swExportDataSheetsToExport_e.swExportData_ExportAllSheets, drawingDoc);
int errors = 0;
int warnings = 0;
var modelExtension = model.Extension;
modelExtension.SaveAs(savePath, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, exportData, ref errors, ref warnings);
Print($"Saved drawing to PDF file \"{savePath}\"", Color.Green);
}
private void ExportToDXF(DrawingDoc drawing)
{
Print("Finding BOM tables...");
var bomTables = drawing.GetBomTables();
if (bomTables.Count == 0)
{
Print("Error: Bill of materials not found.", Color.Red);
return;
}
Print($"Found {bomTables.Count} BOM table(s)\n");
var items = new List<Item>();
foreach (var bom in bomTables)
{
if (worker.CancellationPending)
return;
var bomItems = GetItems(bom);
if (bomItems == null)
return;
items.AddRange(bomItems);
}
Print($"Found {items.Count} component(s)");
var saveDirectory = UserSelectFolder();
if (saveDirectory == null)
{
Print("Canceled\n", Color.Red);
return;
}
var pdfName = GetPdfFileName(drawing);
var pdfPath = Path.Combine(saveDirectory, pdfName);
ExportDrawingToPDF(drawing, pdfPath);
ExportToDXF(items, saveDirectory);
}
private List<Item> GetItems(BomTableAnnotation bom)
private async Task StartExportAsync()
{
try
{
var itemExtractor = new BomItemExtractor(bom);
itemExtractor.SkipHiddenRows = true;
_cancellationTokenSource = new CancellationTokenSource();
var token = _cancellationTokenSource.Token;
Print($"Fetching components from {bom.BomFeature.Name}");
UpdateUIForExportStart();
return itemExtractor.GetItems();
}
catch (Exception ex)
{
Print($"Failed to get items from BOM. {ex.Message}", Color.Red);
}
return null;
}
private void ExportToDXF(PartDoc part)
{
var prefix = prefixTextBox.Text;
var model = part as ModelDoc2;
var activeConfig = ((Configuration)model.GetActiveConfiguration()).Name;
var dir = UserSelectFolder();
if (dir == null)
{
Print("Canceled\n", Color.Red);
return;
}
if (dir == null)
return;
var title = model.GetTitle().Replace(".SLDPRT", "");
var config = model.ConfigurationManager.ActiveConfiguration.Name;
var name = config.ToLower() == "default" ? title : string.Format("{0} [{1}]", title, config);
var savePath = Path.Combine(dir, prefix + name + ".dxf");
SavePartToDXF(part, savePath);
model.ShowConfiguration(activeConfig);
}
private void ExportToDXF(AssemblyDoc assembly)
{
Print("Fetching components...");
var itemExtractor = new AssemblyItemExtractor(assembly);
itemExtractor.TopLevelOnly = false;
var items = itemExtractor.GetItems();
Print($"Found {items.Count} item(s).\n");
var saveDirectory = UserSelectFolder();
if (saveDirectory == null)
{
Print("Canceled\n", Color.Red);
return;
}
ExportToDXF(items, saveDirectory);
}
/// <summary>
///
/// </summary>
/// <param name="items"></param>
/// <param name="saveDirectory">Directory to save the DXF file in.</param>
private void ExportToDXF(IEnumerable<Item> items, string saveDirectory)
{
var prefix = prefixTextBox.Text;
templateDrawing = CreateDrawing();
Print("");
foreach (var item in items)
{
if (worker.CancellationPending)
var activeDoc = _solidWorksService.GetActiveDocument();
if (activeDoc == null)
{
LogMessage("No active document.", Color.Red);
return;
if (item.Component == null)
continue;
var fileName = GetFileName(item);
var savepath = Path.Combine(saveDirectory, fileName + ".dxf");
item.Component.SetLightweightToResolved();
var model = item.Component.GetModelDoc2() as ModelDoc2;
var part = model as PartDoc;
if (part == null)
{
Print(item.ItemNo + " - skipped, not a part document");
continue;
}
var config = item.Component.ReferencedConfiguration;
var viewFlipDecider = GetSelectedViewFlipDecider();
var prefix = prefixTextBox.Text;
var sheetMetal = model.GetFeatureByTypeName("SheetMetal");
var sheetMetalData = sheetMetal?.GetDefinition() as SheetMetalFeatureData;
if (sheetMetalData != null)
var exportContext = new ExportContext
{
item.Thickness = sheetMetalData.Thickness.FromSldWorks();
item.KFactor = sheetMetalData.KFactor;
item.BendRadius = sheetMetalData.BendRadius.FromSldWorks();
}
ActiveDocument = activeDoc,
ViewFlipDecider = viewFlipDecider,
FilePrefix = prefix,
CancellationToken = token,
ProgressCallback = LogMessage
};
if (item.Description == null)
item.Description = model.Extension.CustomPropertyManager[config].Get("Description");
LogMessage($"Started at {DateTime.Now:t}");
if (item.Description == null)
item.Description = model.Extension.CustomPropertyManager[""].Get("Description");
await Task.Run(() => _exportService.Export(exportContext), token);
item.Description = RemoveFontXml(item.Description);
var db = string.Empty;
item.Material = part.GetMaterialPropertyName2(config, out db);
if (part == null)
continue;
if (SavePartToDXF(part, config, savepath))
{
item.FileName = Path.GetFileNameWithoutExtension(savepath);
}
else
{
var desc = item.Description.ToLower();
if (desc.Contains("laser"))
{
Print($"Failed to export item #{item.ItemNo} but description says it is laser cut.", Color.Red);
}
else if (desc.Contains("plasma"))
{
Print($"Failed to export item #{item.ItemNo} but description says it is plasma cut.", Color.Red);
}
}
Print("");
Application.DoEvents();
LogMessage("Done.", Color.Green);
}
try
catch (OperationCanceledException)
{
var drawingInfo = DrawingInfo.Parse(prefix);
var bomName = drawingInfo != null ? $"{drawingInfo.JobNo} {drawingInfo.DrawingNo} BOM" : "BOM";
var bomFile = Path.Combine(saveDirectory, bomName + ".xlsx");
var excelReport = new BomToExcel();
excelReport.CreateBOMExcelFile(bomFile, items.ToList());
LogMessage("Export canceled.", Color.Red);
}
catch (Exception ex)
{
Print(ex.Message, Color.Red);
LogMessage($"Export failed: {ex.Message}", Color.Red);
MessageBox.Show($"Export failed: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
UpdateUIForExportComplete();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
}
private string RemoveFontXml(string s)
private void CancelExport()
{
if (s == null)
return null;
var fontXmlRegex = new Regex("<FONT.*?\\>");
var matches = fontXmlRegex.Matches(s)
.Cast<Match>()
.OrderByDescending(m => m.Index);
foreach (var match in matches)
{
s = s.Remove(match.Index, match.Length);
}
return s;
button1.Enabled = false;
_cancellationTokenSource?.Cancel();
}
private string GetFileName(Item item)
private IViewFlipDecider GetSelectedViewFlipDecider()
{
var prefix = prefixTextBox.Text.Replace("\"", "''");
var item = comboBox1.SelectedItem as ViewFlipDeciderComboboxItem;
return item?.ViewFlipDecider;
}
if (string.IsNullOrWhiteSpace(item.ItemNo))
private void UpdateUIForExportStart()
{
activeDocTitleBox.Enabled = false;
prefixTextBox.Enabled = false;
comboBox1.Enabled = false;
button1.Image = Properties.Resources.stop_alt;
if (richTextBox1.TextLength != 0)
{
return prefix + item.PartName;
}
else
{
return prefix + item.ItemNo.PadLeft(2, '0');
richTextBox1.AppendText("\n\n");
}
}
private bool SavePartToDXF(PartDoc part, string savePath)
private void UpdateUIForExportComplete()
{
var partModel = part as ModelDoc2;
var config = partModel.ConfigurationManager.ActiveConfiguration.Name;
return SavePartToDXF(part, config, savePath);
activeDocTitleBox.Enabled = true;
prefixTextBox.Enabled = true;
comboBox1.Enabled = true;
button1.Image = Properties.Resources.play;
button1.Enabled = true;
}
private bool SavePartToDXF(PartDoc part, string partConfiguration, string savePath)
private void OnActiveDocumentChanged(object sender, EventArgs e)
{
try
if (InvokeRequired)
{
var partModel = part as ModelDoc2;
Invoke(new Action(() => OnActiveDocumentChanged(sender, e)));
return;
}
if (partModel.IsSheetMetal() == false)
UpdateActiveDocumentDisplay();
UpdatePrefixFromActiveDocument();
}
private void UpdateActiveDocumentDisplay()
{
var activeDoc = _solidWorksService.GetActiveDocument();
activeDocTitleBox.Text = activeDoc?.Title ?? "<No Document Open>";
}
private void UpdatePrefixFromActiveDocument()
{
var activeDoc = _solidWorksService.GetActiveDocument();
if (activeDoc == null)
{
prefixTextBox.Text = string.Empty;
return;
}
if (activeDoc.DocumentType == DocumentType.Drawing)
{
var drawingInfo = DrawingInfo.Parse(activeDoc.Title);
if (drawingInfo != null)
{
Print(partModel.GetTitle() + " - skipped, not sheet metal");
return false;
prefixTextBox.Text = $"{drawingInfo.JobNo} {drawingInfo.DrawingNo} PT";
prefixTextBox.SelectionStart = prefixTextBox.Text.Length;
}
if (templateDrawing == null)
templateDrawing = CreateDrawing();
Helper.UncheckFlatPatternCornerTreatment(partModel);
var sheet = templateDrawing.IGetCurrentSheet();
var modelName = Path.GetFileNameWithoutExtension(partModel.GetPathName());
sheet.SetName(modelName);
Print(partModel.GetTitle() + " - Creating flat pattern.");
SolidWorks.Interop.sldworks.View view;
view = templateDrawing.CreateFlatPatternViewFromModelView3(partModel.GetPathName(), partConfiguration, 0, 0, 0, false, false);
if (view == null)
return false;
view.ShowSheetMetalBendNotes = true;
var drawingModel = templateDrawing as ModelDoc2;
drawingModel.ViewZoomtofit2();
var flatPatternModel = ViewHelper.GetModelFromView(view);
Helper.SetFlatPatternSuppressionState(flatPatternModel, swComponentSuppressionState_e.swComponentFullyResolved);
if (ViewHelper.HasSupressedBends(view))
{
Print("A bend is suppressed, please check flat pattern", Color.Red);
}
if (ViewHelper.HideModelSketches(view))
{
// delete the current view that has sketches shown
drawingModel.SelectByName(0, view.Name);
drawingModel.DeleteSelection(false);
// recreate the flat pattern view
view = templateDrawing.CreateFlatPatternViewFromModelView3(partModel.GetPathName(), partConfiguration, 0, 0, 0, false, false);
view.ShowSheetMetalBendNotes = true;
}
if (viewFlipDecider.ShouldFlip(view))
{
Print(partModel.GetTitle() + " - Flipped view", Color.Blue);
view.FlipView = true;
}
drawingModel.SaveAs(savePath);
var etcher = new EtchBendLines.Etcher();
etcher.AddEtchLines(savePath);
Print(partModel.GetTitle() + " - Saved to \"" + savePath + "\"", Color.Green);
drawingModel.SelectByName(0, view.Name);
drawingModel.DeleteSelection(false);
return true;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
}
private string UserSelectFolder()
{
string path = null;
Invoke(new MethodInvoker(() =>
{
var dlg = new FolderBrowserDialog();
dlg.Description = "Where do you want to save the DXF files?";
if (dlg.ShowDialog() != DialogResult.OK)
throw new Exception("Export canceled by user.");
path = dlg.SelectedPath;
}));
return path;
}
private DrawingDoc CreateDrawing()
{
return sldWorks.NewDocument(
DrawingTemplatePath,
(int)swDwgPaperSizes_e.swDwgPaperDsize,
1,
1) as DrawingDoc;
}
private static string DrawingTemplatePath
{
get { return Path.Combine(Application.StartupPath, "Templates", "Blank.drwdot"); }
}
private void textBox1_TextChanged(object sender, EventArgs e)
{
var model = sldWorks.ActiveDoc as ModelDoc2;
var isDrawing = model is DrawingDoc;
if (isDrawing)
{
var drawingInfo = DrawingInfo.Parse(activeDocTitleBox.Text);
if (drawingInfo == null)
return;
prefixTextBox.Text = $"{drawingInfo.JobNo} {drawingInfo.DrawingNo} PT";
prefixTextBox.SelectionStart = prefixTextBox.Text.Length;
}
else
{
@@ -645,30 +231,29 @@ namespace ExportDXF.Forms
}
}
private void UpdatePrefix()
private void activeDocTitleBox_TextChanged(object sender, EventArgs e)
{
var model = sldWorks.ActiveDoc as ModelDoc2;
UpdatePrefixFromActiveDocument();
}
if (model == null)
return;
var isDrawing = model is DrawingDoc;
var title = model.GetTitle();
if (isDrawing)
private void LogMessage(string message, Color? color = null)
{
if (InvokeRequired)
{
var drawingInfo = DrawingInfo.Parse(title);
Invoke(new Action(() => LogMessage(message, color)));
return;
}
if (drawingInfo == null)
return;
prefixTextBox.Text = $"{drawingInfo.JobNo} {drawingInfo.DrawingNo} PT";
prefixTextBox.SelectionStart = prefixTextBox.Text.Length;
if (color.HasValue)
{
richTextBox1.AppendText(message + System.Environment.NewLine, color.Value);
}
else
{
prefixTextBox.Text = string.Empty;
richTextBox1.AppendText(message + System.Environment.NewLine);
}
richTextBox1.ScrollToCaret();
}
}
}
}

View File

@@ -1,61 +0,0 @@
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace ExportDXF
{
public static class Helper
{
public static string GetNumWithSuffix(int i)
{
if (i >= 11 && i <= 13)
return i.ToString() + "th";
var j = i % 10;
switch (j)
{
case 1: return i.ToString() + "st";
case 2: return i.ToString() + "nd";
case 3: return i.ToString() + "rd";
default: return i.ToString() + "th";
}
}
public static void UncheckFlatPatternCornerTreatment(ModelDoc2 model)
{
var flatPattern = model.GetFeatureByTypeName("FlatPattern");
var flatPatternFeatureData = flatPattern.GetDefinition() as FlatPatternFeatureData;
flatPatternFeatureData.CornerTreatment = false;
flatPatternFeatureData.SimplifyBends = true;
var ret = flatPattern.ModifyDefinition(flatPatternFeatureData, model, null);
}
public static bool SetFlatPatternSuppressionState(ModelDoc2 model, swComponentSuppressionState_e suppressionState)
{
var flatPattern = model.GetFeatureByTypeName("FlatPattern");
flatPattern.SetSuppression((int)suppressionState);
return flatPattern.IsSuppressed();
}
public static string RemoveXmlTags(string input)
{
// Define the regular expression pattern to match XML tags
string pattern = @"<[^>]+>";
// Replace all matches of the pattern with an empty string
string result = Regex.Replace(input, pattern, "");
return result;
}
}
}

View File

@@ -1,29 +0,0 @@
using SolidWorks.Interop.sldworks;
namespace ExportDXF
{
public class Item
{
public string ItemNo { get; set; }
public string FileName { get; set; }
public string PartName { get; set; }
public string Configuration { get; set; }
public int Quantity { get; set; }
public string Description { get; set; }
public double Thickness { get; set; }
public double KFactor { get; set; }
public double BendRadius { get; set; }
public string Material { get; set; }
public Component2 Component { get; set; }
}
}

View File

@@ -1,4 +1,6 @@
using SolidWorks.Interop.sldworks;
using ExportDXF.Extensions;
using ExportDXF.Services;
using SolidWorks.Interop.sldworks;
using System;
using System.Collections.Generic;
using System.IO;

View File

@@ -1,4 +1,7 @@
using SolidWorks.Interop.sldworks;
using ExportDXF.Extensions;
using ExportDXF.Services;
using ExportDXF.Utilities;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Collections.Generic;
@@ -52,7 +55,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.ItemNumber != -1)
{
var x = table.DisplayedText[rowIndex, columnIndices.ItemNumber];
x = Helper.RemoveXmlTags(x);
x = TextHelper.RemoveXmlTags(x);
double d;
@@ -69,7 +72,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.PartNumber != -1)
{
var x = table.DisplayedText[rowIndex, columnIndices.PartNumber];
x = Helper.RemoveXmlTags(x);
x = TextHelper.RemoveXmlTags(x);
item.PartName = x;
@@ -78,7 +81,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.Description != -1)
{
var x = table.DisplayedText[rowIndex, columnIndices.Description];
x = Helper.RemoveXmlTags(x);
x = TextHelper.RemoveXmlTags(x);
item.Description = x;
}
@@ -86,7 +89,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.Quantity != -1)
{
var qtyString = table.DisplayedText[rowIndex, columnIndices.Quantity];
qtyString = Helper.RemoveXmlTags(qtyString);
qtyString = TextHelper.RemoveXmlTags(qtyString);
int qty = 0;
int.TryParse(qtyString, out qty);

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using ExportDXF.Services;
using System.Collections.Generic;
namespace ExportDXF.ItemExtractors
{

View File

@@ -0,0 +1,13 @@
namespace ExportDXF.Models
{
/// <summary>
/// Enumeration of SolidWorks document types.
/// </summary>
public enum DocumentType
{
Unknown,
Part,
Assembly,
Drawing
}
}

View File

@@ -0,0 +1,38 @@
using ExportDXF.ViewFlipDeciders;
using System;
using System.Drawing;
using System.Threading;
namespace ExportDXF.Services
{
/// <summary>
/// Context object containing all information needed for an export operation.
/// </summary>
public class ExportContext
{
/// <summary>
/// The document to be exported.
/// </summary>
public SolidWorksDocument ActiveDocument { get; set; }
/// <summary>
/// The view flip decider to determine if views should be flipped.
/// </summary>
public IViewFlipDecider ViewFlipDecider { get; set; }
/// <summary>
/// Prefix to prepend to exported filenames.
/// </summary>
public string FilePrefix { get; set; }
/// <summary>
/// Cancellation token for canceling the export operation.
/// </summary>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// Callback for reporting progress and status messages.
/// </summary>
public Action<string, Color?> ProgressCallback { get; set; }
}
}

65
ExportDXF/Models/Item.cs Normal file
View File

@@ -0,0 +1,65 @@
using SolidWorks.Interop.sldworks;
namespace ExportDXF.Services
{
/// <summary>
/// Represents an item extracted from a BOM or assembly.
/// </summary>
public class Item
{
/// <summary>
/// Item number from the BOM.
/// </summary>
public string ItemNo { get; set; }
/// <summary>
/// Part name or file name.
/// </summary>
public string PartName { get; set; }
/// <summary>
/// Configuration name.
/// </summary>
public string Configuration { get; set; }
/// <summary>
/// Item description.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Quantity of this item.
/// </summary>
public int Quantity { get; set; }
/// <summary>
/// Material specification.
/// </summary>
public string Material { get; set; }
/// <summary>
/// Sheet metal thickness in millimeters.
/// </summary>
public double Thickness { get; set; }
/// <summary>
/// Sheet metal K-factor.
/// </summary>
public double KFactor { get; set; }
/// <summary>
/// Bend radius in millimeters.
/// </summary>
public double BendRadius { get; set; }
/// <summary>
/// The exported DXF filename (without path or extension).
/// </summary>
public string FileName { get; set; }
/// <summary>
/// The SolidWorks component reference.
/// </summary>
public Component2 Component { get; set; }
}
}

View File

@@ -0,0 +1,30 @@
using ExportDXF.Models;
namespace ExportDXF.Services
{
/// <summary>
/// Represents a SolidWorks document with essential metadata.
/// </summary>
public class SolidWorksDocument
{
/// <summary>
/// The title/name of the document.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The full file path of the document.
/// </summary>
public string FilePath { get; set; }
/// <summary>
/// The type of document (Part, Assembly, or Drawing).
/// </summary>
public DocumentType DocumentType { get; set; }
/// <summary>
/// The native SolidWorks document object (ModelDoc2, PartDoc, etc.).
/// </summary>
public object NativeDocument { get; set; }
}
}

View File

@@ -1,19 +1,54 @@
using System;
using ExportDXF.Forms;
using ExportDXF.Services;
using System;
using System.Windows.Forms;
namespace ExportDXF
{
internal static class Program
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
private static void Main()
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Forms.MainForm());
// Simple DI setup - could use a DI container like Microsoft.Extensions.DependencyInjection
var container = new ServiceContainer();
var mainForm = container.Resolve<MainForm>();
Application.Run(mainForm);
}
}
/// <summary>
/// Simple dependency injection container.
/// For production, consider using Microsoft.Extensions.DependencyInjection or similar.
/// </summary>
public class ServiceContainer
{
public MainForm Resolve<T>() where T : MainForm
{
// Create the dependency tree
var solidWorksService = new SolidWorksService();
var bomExtractor = new BomExtractor();
var partExporter = new PartExporter();
var drawingExporter = new DrawingExporter();
var bomExcelExporter = new BomExcelExporter();
var exportService = new DxfExportService(
solidWorksService,
bomExtractor,
partExporter,
drawingExporter,
bomExcelExporter);
return new MainForm(solidWorksService, exportService);
}
}
}

View File

@@ -0,0 +1,396 @@
using ExportDXF.Utilities;
using OfficeOpenXml;
using OfficeOpenXml.Style;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
namespace ExportDXF.Services
{
/// <summary>
/// Service for creating BOM Excel files.
/// </summary>
public interface IBomExcelExporter
{
/// <summary>
/// Creates an Excel file containing the Bill of Materials.
/// </summary>
/// <param name="filepath">The full path where the Excel file will be saved.</param>
/// <param name="items">The list of items to include in the BOM.</param>
void CreateBOMExcelFile(string filepath, IList<Item> items);
}
/// <summary>
/// Service for creating Excel files containing Bill of Materials data.
/// </summary>
public class BomExcelExporter : IBomExcelExporter
{
private const string DEFAULT_TEMPLATE_FILENAME = "BomTemplate.xlsx";
private const string DEFAULT_SHEET_NAME = "Parts";
private const int HEADER_ROW = 1;
private const int DATA_START_ROW = 2;
private const double COLUMN_PADDING = 2.0;
private const int MAX_COLUMN_WIDTH = 50;
private readonly string _templatePath;
private readonly BomExcelSettings _settings;
public BomExcelExporter() : this(GetDefaultTemplatePath(), new BomExcelSettings())
{
}
public BomExcelExporter(string templatePath) : this(templatePath, new BomExcelSettings())
{
}
public BomExcelExporter(string templatePath, BomExcelSettings settings)
{
if (string.IsNullOrWhiteSpace(templatePath))
throw new ArgumentException("Template path cannot be null or empty.", nameof(templatePath));
_templatePath = templatePath;
_settings = settings ?? new BomExcelSettings();
// Set EPPlus license context (required for newer versions)
//ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
}
/// <summary>
/// Creates an Excel file containing the Bill of Materials.
/// </summary>
public void CreateBOMExcelFile(string filepath, IList<Item> items)
{
if (string.IsNullOrWhiteSpace(filepath))
throw new ArgumentException("Filepath cannot be null or empty.", nameof(filepath));
if (items == null)
throw new ArgumentNullException(nameof(items));
ValidateAndPrepareFile(filepath);
using (var package = new ExcelPackage(new FileInfo(filepath)))
{
var worksheet = GetOrCreateWorksheet(package);
PopulateWorksheet(worksheet, items);
FormatWorksheet(worksheet);
package.Save();
}
}
#region Worksheet Management
private ExcelWorksheet GetOrCreateWorksheet(ExcelPackage package)
{
var worksheet = package.Workbook.Worksheets[DEFAULT_SHEET_NAME];
if (worksheet == null)
{
if (_settings.CreateWorksheetIfMissing)
{
worksheet = package.Workbook.Worksheets.Add(DEFAULT_SHEET_NAME);
CreateDefaultHeaders(worksheet);
}
else
{
var availableSheets = string.Join(", ",
package.Workbook.Worksheets.Select(ws => ws.Name));
throw new InvalidOperationException(
$"Worksheet '{DEFAULT_SHEET_NAME}' not found in template. " +
$"Available sheets: {availableSheets}");
}
}
return worksheet;
}
private void CreateDefaultHeaders(ExcelWorksheet worksheet)
{
var headers = new[]
{
"Item No", "File Name", "Qty", "Description", "Part Name",
"Configuration", "Thickness", "Material", "K-Factor", "Bend Radius"
};
for (int i = 0; i < headers.Length; i++)
{
var cell = worksheet.Cells[HEADER_ROW, i + 1];
cell.Value = headers[i];
if (_settings.FormatHeaders)
{
cell.Style.Font.Bold = true;
cell.Style.Fill.PatternType = ExcelFillStyle.Solid;
cell.Style.Fill.BackgroundColor.SetColor(Color.LightGray);
}
}
}
#endregion
#region Data Population
private void PopulateWorksheet(ExcelWorksheet worksheet, IList<Item> items)
{
var filteredItems = _settings.SkipItemsWithoutFiles
? items.Where(i => !string.IsNullOrEmpty(i.FileName)).ToList()
: items.ToList();
for (int i = 0; i < filteredItems.Count; i++)
{
var item = filteredItems[i];
var row = DATA_START_ROW + i;
WriteItemToRow(worksheet, row, item);
}
// Add summary information if enabled
if (_settings.AddSummary && filteredItems.Count > 0)
{
AddSummarySection(worksheet, filteredItems, DATA_START_ROW + filteredItems.Count + 2);
}
}
private void WriteItemToRow(ExcelWorksheet worksheet, int row, Item item)
{
int col = 1;
// Item No
worksheet.Cells[row, col++].Value = FormatItemNumber(item.ItemNo);
// File Name
worksheet.Cells[row, col++].Value = item.FileName;
// Quantity
worksheet.Cells[row, col++].Value = item.Quantity;
// Description
worksheet.Cells[row, col++].Value = CleanDescription(item.Description);
// Part Name
worksheet.Cells[row, col++].Value = item.PartName;
// Configuration
worksheet.Cells[row, col++].Value = item.Configuration;
// Thickness (only if > 0)
worksheet.Cells[row, col++].Value = GetFormattedNumericValue(item.Thickness, _settings.ThicknessDecimalPlaces);
// Material
worksheet.Cells[row, col++].Value = item.Material;
// K-Factor (only if > 0)
worksheet.Cells[row, col++].Value = GetFormattedNumericValue(item.KFactor, _settings.KFactorDecimalPlaces);
// Bend Radius (only if > 0)
worksheet.Cells[row, col++].Value = GetFormattedNumericValue(item.BendRadius, _settings.BendRadiusDecimalPlaces);
// Apply row formatting
if (_settings.AlternateRowColors && row % 2 == 0)
{
var rowRange = worksheet.Cells[row, 1, row, col - 1];
rowRange.Style.Fill.PatternType = ExcelFillStyle.Solid;
rowRange.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(240, 240, 240));
}
}
private void AddSummarySection(ExcelWorksheet worksheet, IList<Item> items, int startRow)
{
worksheet.Cells[startRow, 1].Value = "SUMMARY";
worksheet.Cells[startRow, 1].Style.Font.Bold = true;
worksheet.Cells[startRow, 1].Style.Font.Size = 12;
startRow += 2;
// Total items
worksheet.Cells[startRow, 1].Value = "Total Items:";
worksheet.Cells[startRow, 2].Value = items.Count;
// Total quantity
var totalQty = items.Sum(i => i.Quantity);
worksheet.Cells[startRow + 1, 1].Value = "Total Quantity:";
worksheet.Cells[startRow + 1, 2].Value = totalQty;
// Unique materials
var uniqueMaterials = items.Where(i => !string.IsNullOrEmpty(i.Material))
.Select(i => i.Material)
.Distinct()
.Count();
worksheet.Cells[startRow + 2, 1].Value = "Unique Materials:";
worksheet.Cells[startRow + 2, 2].Value = uniqueMaterials;
// Thickness range
var thicknesses = items.Where(i => i.Thickness > 0).Select(i => i.Thickness).ToList();
if (thicknesses.Any())
{
worksheet.Cells[startRow + 3, 1].Value = "Thickness Range:";
worksheet.Cells[startRow + 3, 2].Value = $"{thicknesses.Min():F1} - {thicknesses.Max():F1} mm";
}
}
#endregion
#region Data Formatting
private string FormatItemNumber(string itemNo)
{
if (string.IsNullOrWhiteSpace(itemNo))
return string.Empty;
// Try to parse as number and pad with zeros if it's a simple number
if (int.TryParse(itemNo, out int number))
{
return number.ToString().PadLeft(_settings.ItemNumberPadding, '0');
}
return itemNo;
}
private string CleanDescription(string description)
{
if (string.IsNullOrWhiteSpace(description))
return string.Empty;
// Remove any remaining XML tags that might have been missed
description = TextHelper.RemoveXmlTags(description);
// Trim and normalize whitespace
return System.Text.RegularExpressions.Regex.Replace(description.Trim(), @"\s+", " ");
}
private object GetFormattedNumericValue(double value, int decimalPlaces)
{
if (value <= 0)
return null;
return Math.Round(value, decimalPlaces);
}
#endregion
#region Worksheet Formatting
private void FormatWorksheet(ExcelWorksheet worksheet)
{
if (worksheet.Dimension == null)
return;
// Auto-fit columns with limits
for (int col = 1; col <= worksheet.Dimension.Columns; col++)
{
worksheet.Column(col).AutoFit();
if (worksheet.Column(col).Width > MAX_COLUMN_WIDTH)
worksheet.Column(col).Width = MAX_COLUMN_WIDTH;
else
worksheet.Column(col).Width += COLUMN_PADDING;
}
// Add borders if enabled
if (_settings.AddBorders)
{
var dataRange = worksheet.Cells[HEADER_ROW, 1, worksheet.Dimension.End.Row, worksheet.Dimension.End.Column];
dataRange.Style.Border.Top.Style = ExcelBorderStyle.Thin;
dataRange.Style.Border.Left.Style = ExcelBorderStyle.Thin;
dataRange.Style.Border.Right.Style = ExcelBorderStyle.Thin;
dataRange.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
}
// Format numeric columns
FormatNumericColumns(worksheet);
// Freeze header row if enabled
if (_settings.FreezeHeaderRow)
{
worksheet.View.FreezePanes(DATA_START_ROW, 1);
}
}
private void FormatNumericColumns(ExcelWorksheet worksheet)
{
if (worksheet.Dimension == null)
return;
var lastRow = worksheet.Dimension.End.Row;
// Format quantity column (assuming it's column 3)
if (worksheet.Dimension.Columns >= 3)
{
var qtyRange = worksheet.Cells[DATA_START_ROW, 3, lastRow, 3];
qtyRange.Style.Numberformat.Format = "0";
qtyRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
}
// Format thickness column (assuming it's column 7)
if (worksheet.Dimension.Columns >= 7)
{
var thicknessRange = worksheet.Cells[DATA_START_ROW, 7, lastRow, 7];
thicknessRange.Style.Numberformat.Format = $"0.{new string('0', _settings.ThicknessDecimalPlaces)}";
thicknessRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Right;
}
}
#endregion
#region File Management
private void ValidateAndPrepareFile(string filepath)
{
var directory = Path.GetDirectoryName(filepath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
if (UseTemplate())
{
CopyTemplateToDestination(filepath);
}
else if (File.Exists(filepath))
{
File.Delete(filepath); // Remove existing file to start fresh
}
}
private bool UseTemplate()
{
return !string.IsNullOrEmpty(_templatePath) && File.Exists(_templatePath);
}
private void CopyTemplateToDestination(string filepath)
{
if (!File.Exists(_templatePath))
{
throw new FileNotFoundException(
$"BOM template file not found at: {_templatePath}. " +
"Either provide a valid template or enable CreateWorksheetIfMissing.",
_templatePath);
}
try
{
File.Copy(_templatePath, filepath, overwrite: true);
}
catch (Exception ex)
{
throw new IOException($"Failed to copy template to {filepath}: {ex.Message}", ex);
}
}
private static string GetDefaultTemplatePath()
{
return Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"Templates",
DEFAULT_TEMPLATE_FILENAME);
}
#endregion
}
}

View File

@@ -0,0 +1,64 @@
namespace ExportDXF.Services
{
/// <summary>
/// Configuration settings for BOM Excel export.
/// </summary>
public class BomExcelSettings
{
/// <summary>
/// Create the worksheet if it doesn't exist in the template.
/// </summary>
public bool CreateWorksheetIfMissing { get; set; } = true;
/// <summary>
/// Skip items that don't have an associated DXF file.
/// </summary>
public bool SkipItemsWithoutFiles { get; set; } = false;
/// <summary>
/// Add summary section with totals and statistics.
/// </summary>
public bool AddSummary { get; set; } = true;
/// <summary>
/// Format header row with bold text and background color.
/// </summary>
public bool FormatHeaders { get; set; } = true;
/// <summary>
/// Add borders around all cells.
/// </summary>
public bool AddBorders { get; set; } = true;
/// <summary>
/// Freeze the header row for easier scrolling.
/// </summary>
public bool FreezeHeaderRow { get; set; } = true;
/// <summary>
/// Alternate row background colors for easier reading.
/// </summary>
public bool AlternateRowColors { get; set; } = true;
/// <summary>
/// Number of decimal places for thickness values.
/// </summary>
public int ThicknessDecimalPlaces { get; set; } = 2;
/// <summary>
/// Number of decimal places for K-Factor values.
/// </summary>
public int KFactorDecimalPlaces { get; set; } = 3;
/// <summary>
/// Number of decimal places for bend radius values.
/// </summary>
public int BendRadiusDecimalPlaces { get; set; } = 2;
/// <summary>
/// Minimum number of digits for item numbers (pad with zeros).
/// </summary>
public int ItemNumberPadding { get; set; } = 2;
}
}

View File

@@ -0,0 +1,66 @@
using ExportDXF.Extensions;
using ExportDXF.ItemExtractors;
using SolidWorks.Interop.sldworks;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace ExportDXF.Services
{
/// <summary>
/// Service for extracting items from a Bill of Materials (BOM).
/// </summary>
public interface IBomExtractor
{
/// <summary>
/// Extracts items from all BOM tables in a drawing document.
/// </summary>
/// <param name="drawing">The drawing document containing BOM tables.</param>
/// <param name="progressCallback">Optional callback for progress updates.</param>
/// <returns>A list of extracted items.</returns>
List<Item> ExtractFromDrawing(DrawingDoc drawing, Action<string, Color?> progressCallback);
}
public class BomExtractor : IBomExtractor
{
public List<Item> ExtractFromDrawing(DrawingDoc drawing, Action<string, Color?> progressCallback)
{
if (drawing == null)
throw new ArgumentNullException(nameof(drawing));
var bomTables = drawing.GetBomTables();
if (bomTables.Count == 0)
{
progressCallback?.Invoke("Error: Bill of materials not found.", Color.Red);
return new List<Item>();
}
progressCallback?.Invoke($"Found {bomTables.Count} BOM table(s)", null);
var allItems = new List<Item>();
foreach (var bom in bomTables)
{
try
{
var extractor = new BomItemExtractor(bom)
{
SkipHiddenRows = true
};
progressCallback?.Invoke($"Fetching components from {bom.BomFeature.Name}", null);
var items = extractor.GetItems();
allItems.AddRange(items);
}
catch (Exception ex)
{
progressCallback?.Invoke($"Failed to extract from {bom.BomFeature.Name}: {ex.Message}", Color.Red);
}
}
return allItems;
}
}
}

View File

@@ -0,0 +1,85 @@
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Drawing;
using System.IO;
namespace ExportDXF.Services
{
/// <summary>
/// Service for exporting drawing documents to PDF format.
/// </summary>
public interface IDrawingExporter
{
/// <summary>
/// Exports a drawing document to PDF format.
/// </summary>
/// <param name="drawing">The drawing document to export.</param>
/// <param name="saveDirectory">The directory where the PDF file will be saved.</param>
/// <param name="progressCallback">Optional callback for progress updates.</param>
void ExportToPdf(DrawingDoc drawing, string saveDirectory, Action<string, Color?> progressCallback);
}
public class DrawingExporter : IDrawingExporter
{
public void ExportToPdf(DrawingDoc drawing, string saveDirectory, Action<string, Color?> progressCallback)
{
if (drawing == null)
throw new ArgumentNullException(nameof(drawing));
if (string.IsNullOrWhiteSpace(saveDirectory))
throw new ArgumentException("Save directory cannot be null or empty.", nameof(saveDirectory));
try
{
var pdfFileName = GetPdfFileName(drawing);
var pdfPath = Path.Combine(saveDirectory, pdfFileName);
var model = drawing as ModelDoc2;
var sldWorks = (SldWorks)Activator.CreateInstance(Type.GetTypeFromProgID("SldWorks.Application"));
var exportData = sldWorks.GetExportFileData(
(int)swExportDataFileType_e.swExportPdfData) as ExportPdfData;
exportData.ViewPdfAfterSaving = false;
exportData.SetSheets(
(int)swExportDataSheetsToExport_e.swExportData_ExportAllSheets,
drawing);
int errors = 0;
int warnings = 0;
var modelExtension = model.Extension;
modelExtension.SaveAs(
pdfPath,
(int)swSaveAsVersion_e.swSaveAsCurrentVersion,
(int)swSaveAsOptions_e.swSaveAsOptions_Silent,
exportData,
ref errors,
ref warnings);
if (errors == 0)
{
progressCallback?.Invoke($"Saved drawing to PDF: \"{pdfFileName}\"", Color.Green);
}
else
{
progressCallback?.Invoke($"PDF export completed with errors: {errors}", Color.Yellow);
}
}
catch (Exception ex)
{
progressCallback?.Invoke($"Failed to export PDF: {ex.Message}", Color.Red);
throw;
}
}
private string GetPdfFileName(DrawingDoc drawing)
{
var model = drawing as ModelDoc2;
var modelFilePath = model.GetPathName();
return Path.GetFileNameWithoutExtension(modelFilePath) + ".pdf";
}
}
}

View File

@@ -0,0 +1,413 @@
using ExportDXF.Extensions;
using ExportDXF.ItemExtractors;
using ExportDXF.Models;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
namespace ExportDXF.Services
{
public interface IDxfExportService
{
/// <summary>
/// Exports the document specified in the context to DXF format.
/// </summary>
/// <param name="context">The export context containing all necessary information.</param>
void Export(ExportContext context);
}
/// <summary>
/// Service responsible for orchestrating the export of SolidWorks documents to DXF format.
/// </summary>
public class DxfExportService : IDxfExportService
{
private const string DRAWING_TEMPLATE_FOLDER = "Templates";
private const string DRAWING_TEMPLATE_FILE = "Blank.drwdot";
private readonly ISolidWorksService _solidWorksService;
private readonly IBomExtractor _bomExtractor;
private readonly IPartExporter _partExporter;
private readonly IDrawingExporter _drawingExporter;
private readonly IBomExcelExporter _bomExcelExporter;
public DxfExportService(
ISolidWorksService solidWorksService,
IBomExtractor bomExtractor,
IPartExporter partExporter,
IDrawingExporter drawingExporter,
IBomExcelExporter bomExcelExporter)
{
_solidWorksService = solidWorksService ?? throw new ArgumentNullException(nameof(solidWorksService));
_bomExtractor = bomExtractor ?? throw new ArgumentNullException(nameof(bomExtractor));
_partExporter = partExporter ?? throw new ArgumentNullException(nameof(partExporter));
_drawingExporter = drawingExporter ?? throw new ArgumentNullException(nameof(drawingExporter));
_bomExcelExporter = bomExcelExporter ?? throw new ArgumentNullException(nameof(bomExcelExporter));
}
/// <summary>
/// Exports the document specified in the context to DXF format.
/// </summary>
public void Export(ExportContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
ValidateContext(context);
var startTime = DateTime.Now;
try
{
_solidWorksService.EnableUserControl(false);
switch (context.ActiveDocument.DocumentType)
{
case DocumentType.Part:
ExportPart(context);
break;
case DocumentType.Assembly:
ExportAssembly(context);
break;
case DocumentType.Drawing:
ExportDrawing(context);
break;
default:
LogProgress(context, "Unknown document type.", Color.Red);
break;
}
}
finally
{
_solidWorksService.EnableUserControl(true);
var duration = DateTime.Now - startTime;
LogProgress(context, $"Run time: {duration.ToReadableFormat()}", null);
}
}
#region Export Methods by Document Type
private void ExportPart(ExportContext context)
{
LogProgress(context, "Active document is a Part", null);
var part = context.ActiveDocument.NativeDocument as PartDoc;
if (part == null)
{
LogProgress(context, "Failed to get part document.", Color.Red);
return;
}
var saveDirectory = PromptUserForDirectory(context);
if (saveDirectory == null)
{
LogProgress(context, "Canceled", Color.Red);
return;
}
_partExporter.ExportSinglePart(part, saveDirectory, context);
}
private void ExportAssembly(ExportContext context)
{
LogProgress(context, "Active document is an Assembly", null);
LogProgress(context, "Fetching components...", null);
var assembly = context.ActiveDocument.NativeDocument as AssemblyDoc;
if (assembly == null)
{
LogProgress(context, "Failed to get assembly document.", Color.Red);
return;
}
var items = ExtractItemsFromAssembly(assembly, context);
if (items == null || items.Count == 0)
{
LogProgress(context, "No items found in assembly.", Color.Yellow);
return;
}
LogProgress(context, $"Found {items.Count} item(s).", null);
var saveDirectory = PromptUserForDirectory(context);
if (saveDirectory == null)
{
LogProgress(context, "Canceled", Color.Red);
return;
}
ExportItems(items, saveDirectory, context);
}
private void ExportDrawing(ExportContext context)
{
LogProgress(context, "Active document is a Drawing", null);
LogProgress(context, "Finding BOM tables...", null);
var drawing = context.ActiveDocument.NativeDocument as DrawingDoc;
if (drawing == null)
{
LogProgress(context, "Failed to get drawing document.", Color.Red);
return;
}
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
if (items == null || items.Count == 0)
{
LogProgress(context, "Error: Bill of materials not found.", Color.Red);
return;
}
LogProgress(context, $"Found {items.Count} component(s)", null);
var saveDirectory = PromptUserForDirectory(context);
if (saveDirectory == null)
{
LogProgress(context, "Canceled", Color.Red);
return;
}
// Export drawing to PDF first
_drawingExporter.ExportToPdf(drawing, saveDirectory, context.ProgressCallback);
// Then export parts to DXF
ExportItems(items, saveDirectory, context);
}
#endregion
#region Item Processing
private List<Item> ExtractItemsFromAssembly(AssemblyDoc assembly, ExportContext context)
{
try
{
var extractor = new AssemblyItemExtractor(assembly)
{
TopLevelOnly = false
};
return extractor.GetItems();
}
catch (Exception ex)
{
LogProgress(context, $"Failed to extract items from assembly: {ex.Message}", Color.Red);
return new List<Item>();
}
}
private void ExportItems(List<Item> items, string saveDirectory, ExportContext context)
{
DrawingDoc templateDrawing = null;
try
{
templateDrawing = CreateTemplateDrawing();
LogProgress(context, "", null);
int successCount = 0;
int failureCount = 0;
foreach (var item in items)
{
if (context.CancellationToken.IsCancellationRequested)
{
LogProgress(context, "Export canceled by user.", Color.Yellow);
return;
}
try
{
_partExporter.ExportItem(item, templateDrawing, saveDirectory, context);
if (!string.IsNullOrEmpty(item.FileName))
successCount++;
else
failureCount++;
}
catch (Exception ex)
{
LogProgress(context, $"Error exporting item {item.ItemNo}: {ex.Message}", Color.Red);
failureCount++;
}
LogProgress(context, "", null);
}
LogProgress(context, $"Export complete: {successCount} succeeded, {failureCount} failed",
failureCount > 0 ? Color.Yellow : Color.Green);
// Create BOM Excel file
CreateBomExcelFile(items, saveDirectory, context);
}
finally
{
CloseTemplateDrawing(templateDrawing);
}
}
#endregion
#region Template Drawing Management
private DrawingDoc CreateTemplateDrawing()
{
var sldWorks = _solidWorksService.GetNativeSldWorks();
if (sldWorks == null)
throw new InvalidOperationException("SolidWorks service is not connected.");
var templatePath = GetDrawingTemplatePath();
if (!File.Exists(templatePath))
{
throw new FileNotFoundException(
$"Drawing template not found at: {templatePath}",
templatePath);
}
var drawing = sldWorks.NewDocument(
templatePath,
(int)swDwgPaperSizes_e.swDwgPaperDsize,
1,
1) as DrawingDoc;
if (drawing == null)
{
throw new InvalidOperationException(
"Failed to create drawing from template. Please check the template file.");
}
return drawing;
}
private void CloseTemplateDrawing(DrawingDoc drawing)
{
if (drawing == null)
return;
try
{
var model = drawing as ModelDoc2;
var title = model?.GetTitle();
if (!string.IsNullOrEmpty(title))
{
_solidWorksService.CloseDocument(title);
}
}
catch
{
// Ignore errors during cleanup
}
}
private string GetDrawingTemplatePath()
{
return Path.Combine(
Application.StartupPath,
DRAWING_TEMPLATE_FOLDER,
DRAWING_TEMPLATE_FILE);
}
#endregion
#region BOM Excel Creation
private void CreateBomExcelFile(List<Item> items, string saveDirectory, ExportContext context)
{
try
{
var bomFileName = GetBomFileName(context.FilePrefix);
var bomFilePath = Path.Combine(saveDirectory, bomFileName + ".xlsx");
_bomExcelExporter.CreateBOMExcelFile(bomFilePath, items);
LogProgress(context, $"Created BOM Excel file: {bomFileName}.xlsx", Color.Green);
}
catch (Exception ex)
{
LogProgress(context, $"Failed to create BOM Excel: {ex.Message}", Color.Red);
}
}
private string GetBomFileName(string prefix)
{
if (string.IsNullOrWhiteSpace(prefix))
return "BOM";
var drawingInfo = DrawingInfo.Parse(prefix);
if (drawingInfo != null)
{
return $"{drawingInfo.JobNo} {drawingInfo.DrawingNo} BOM";
}
return prefix.Trim() + " BOM";
}
#endregion
#region User Interaction
private string PromptUserForDirectory(ExportContext context)
{
// Check if already canceled
if (context.CancellationToken.IsCancellationRequested)
return null;
string selectedPath = null;
// Must run on STA thread for FolderBrowserDialog
var thread = new System.Threading.Thread(() =>
{
using (var dialog = new FolderBrowserDialog())
{
dialog.Description = "Where do you want to save the DXF files?";
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == DialogResult.OK)
{
selectedPath = dialog.SelectedPath;
}
}
});
thread.SetApartmentState(System.Threading.ApartmentState.STA);
thread.Start();
thread.Join();
return selectedPath;
}
#endregion
#region Helper Methods
private void ValidateContext(ExportContext context)
{
if (context.ActiveDocument == null)
throw new ArgumentException("ActiveDocument cannot be null.", nameof(context));
if (context.ProgressCallback == null)
throw new ArgumentException("ProgressCallback cannot be null.", nameof(context));
}
private void LogProgress(ExportContext context, string message, Color? color)
{
context.ProgressCallback?.Invoke(message, color);
}
#endregion
}
}

View File

@@ -0,0 +1,300 @@
using ExportDXF.Extensions;
using ExportDXF.Utilities;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Drawing;
using System.IO;
namespace ExportDXF.Services
{
/// <summary>
/// Service for exporting parts to DXF format.
/// </summary>
public interface IPartExporter
{
/// <summary>
/// Exports a single part document to DXF.
/// </summary>
/// <param name="part">The part document to export.</param>
/// <param name="saveDirectory">The directory where the DXF file will be saved.</param>
/// <param name="context">The export context.</param>
void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
/// <summary>
/// Exports an item (component from BOM or assembly) to DXF.
/// </summary>
/// <param name="item">The item to export.</param>
/// <param name="templateDrawing">The template drawing to use for creating flat patterns.</param>
/// <param name="saveDirectory">The directory where the DXF file will be saved.</param>
/// <param name="context">The export context.</param>
void ExportItem(Item item, DrawingDoc templateDrawing, string saveDirectory, ExportContext context);
}
public class PartExporter : IPartExporter
{
public void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context)
{
if (part == null)
throw new ArgumentNullException(nameof(part));
var model = part as ModelDoc2;
var activeConfig = model.GetActiveConfiguration() as SolidWorks.Interop.sldworks.Configuration;
var originalConfigName = activeConfig?.Name;
try
{
var fileName = GetSinglePartFileName(model, context.FilePrefix);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
var templateDrawing = CreateTemplateDrawing(context);
try
{
ExportPartToDxf(part, originalConfigName, templateDrawing, savePath, context);
}
finally
{
CloseTemplateDrawing(templateDrawing, context);
}
}
finally
{
if (originalConfigName != null)
{
model.ShowConfiguration(originalConfigName);
}
}
}
public void ExportItem(Item item, DrawingDoc templateDrawing, string saveDirectory, ExportContext context)
{
if (item?.Component == null)
{
context.ProgressCallback?.Invoke($"Item {item?.ItemNo} - skipped, no component", Color.Yellow);
return;
}
context.CancellationToken.ThrowIfCancellationRequested();
item.Component.SetLightweightToResolved();
var model = item.Component.GetModelDoc2() as ModelDoc2;
var part = model as PartDoc;
if (part == null)
{
context.ProgressCallback?.Invoke($"{item.ItemNo} - skipped, not a part document", null);
return;
}
EnrichItemWithMetadata(item, model, part);
var fileName = GetItemFileName(item, context.FilePrefix);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
if (ExportPartToDxf(part, item.Component.ReferencedConfiguration, templateDrawing, savePath, context))
{
item.FileName = Path.GetFileNameWithoutExtension(savePath);
}
else
{
LogExportFailure(item, context);
}
}
private void EnrichItemWithMetadata(Item item, ModelDoc2 model, PartDoc part)
{
// Get sheet metal properties
var sheetMetalProps = SolidWorksHelper.GetSheetMetalProperties(model);
if (sheetMetalProps != null)
{
item.Thickness = sheetMetalProps.Thickness;
item.KFactor = sheetMetalProps.KFactor;
item.BendRadius = sheetMetalProps.BendRadius;
}
// Get description from custom properties
var config = item.Component.ReferencedConfiguration;
item.Description = model.Extension.CustomPropertyManager[config].Get("Description");
item.Description = model.Extension.CustomPropertyManager[""].Get("Description");
item.Description = TextHelper.RemoveXmlTags(item.Description);
// Get material
item.Material = part.GetMaterialPropertyName2(config, out _);
}
private bool ExportPartToDxf(
PartDoc part,
string configName,
DrawingDoc templateDrawing,
string savePath,
ExportContext context)
{
try
{
var model = part as ModelDoc2;
if (!model.IsSheetMetal())
{
context.ProgressCallback?.Invoke($"{model.GetTitle()} - skipped, not sheet metal", null);
return false;
}
SolidWorksHelper.ConfigureFlatPatternSettings(model);
var sheet = templateDrawing.IGetCurrentSheet();
var modelName = Path.GetFileNameWithoutExtension(model.GetPathName());
sheet.SetName(modelName);
context.ProgressCallback?.Invoke($"{model.GetTitle()} - Creating flat pattern", null);
var view = CreateFlatPatternView(templateDrawing, model, configName);
if (view == null)
{
context.ProgressCallback?.Invoke($"{model.GetTitle()} - Failed to create flat pattern", Color.Red);
return false;
}
ConfigureFlatPatternView(view, templateDrawing, model, configName, context);
if (context.ViewFlipDecider?.ShouldFlip(view) == true)
{
context.ProgressCallback?.Invoke($"{model.GetTitle()} - Flipped view", Color.Blue);
view.FlipView = true;
}
var drawingModel = templateDrawing as ModelDoc2;
drawingModel.SaveAs(savePath);
AddEtchLines(savePath);
context.ProgressCallback?.Invoke($"{model.GetTitle()} - Saved to \"{savePath}\"", Color.Green);
DeleteView(drawingModel, view);
return true;
}
catch (Exception ex)
{
context.ProgressCallback?.Invoke($"Export failed: {ex.Message}", Color.Red);
return false;
}
}
private SolidWorks.Interop.sldworks.View CreateFlatPatternView(
DrawingDoc drawing,
ModelDoc2 part,
string configName)
{
return drawing.CreateFlatPatternViewFromModelView3(
part.GetPathName(),
configName,
0, 0, 0,
false,
false);
}
private void ConfigureFlatPatternView(
SolidWorks.Interop.sldworks.View view,
DrawingDoc drawing,
ModelDoc2 partModel,
string configName,
ExportContext context)
{
view.ShowSheetMetalBendNotes = true;
var drawingModel = drawing as ModelDoc2;
drawingModel.ViewZoomtofit2();
var flatPatternModel = ViewHelper.GetModelFromView(view);
SolidWorksHelper.SetFlatPatternSuppressionState(
flatPatternModel,
swComponentSuppressionState_e.swComponentFullyResolved);
if (ViewHelper.HasSupressedBends(view))
{
context.ProgressCallback?.Invoke("A bend is suppressed, please check flat pattern", Color.Red);
}
if (ViewHelper.HideModelSketches(view))
{
// Recreate view without sketches
DeleteView(drawingModel, view);
view = CreateFlatPatternView(drawing, partModel, configName);
view.ShowSheetMetalBendNotes = true;
}
}
private void DeleteView(ModelDoc2 drawing, SolidWorks.Interop.sldworks.View view)
{
drawing.SelectByName(0, view.Name);
drawing.DeleteSelection(false);
}
private void AddEtchLines(string dxfPath)
{
try
{
var etcher = new EtchBendLines.Etcher();
etcher.AddEtchLines(dxfPath);
}
catch (Exception)
{
// Silently fail if etch lines can't be added
}
}
private string GetSinglePartFileName(ModelDoc2 model, string prefix)
{
var title = model.GetTitle().Replace(".SLDPRT", "");
var config = model.ConfigurationManager.ActiveConfiguration.Name;
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
var name = isDefaultConfig ? title : $"{title} [{config}]";
return prefix + name;
}
private string GetItemFileName(Item item, string prefix)
{
prefix = prefix?.Replace("\"", "''") ?? string.Empty;
if (string.IsNullOrWhiteSpace(item.ItemNo))
{
return prefix + item.PartName;
}
return prefix + item.ItemNo.PadLeft(2, '0');
}
private void LogExportFailure(Item item, ExportContext context)
{
var desc = item.Description?.ToLower() ?? string.Empty;
if (desc.Contains("laser"))
{
context.ProgressCallback?.Invoke(
$"Failed to export item #{item.ItemNo} but description says it is laser cut.",
Color.Red);
}
else if (desc.Contains("plasma"))
{
context.ProgressCallback?.Invoke(
$"Failed to export item #{item.ItemNo} but description says it is plasma cut.",
Color.Red);
}
}
private DrawingDoc CreateTemplateDrawing(ExportContext context)
{
throw new NotImplementedException("Template drawing creation not implemented.");
}
private void CloseTemplateDrawing(DrawingDoc drawing, ExportContext context)
{
throw new NotImplementedException("Template drawing creation not implemented.");
}
}
}

View File

@@ -0,0 +1,406 @@
using ExportDXF.Models;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace ExportDXF.Services
{
/// <summary>
/// Service for managing SolidWorks application connection and document lifecycle.
/// </summary>
public interface ISolidWorksService : IDisposable
{
/// <summary>
/// Connects to the SolidWorks application asynchronously.
/// </summary>
Task ConnectAsync();
/// <summary>
/// Gets the currently active SolidWorks document.
/// </summary>
/// <returns>The active document or null if no document is open.</returns>
SolidWorksDocument GetActiveDocument();
/// <summary>
/// Enables or disables user control of the SolidWorks application.
/// </summary>
/// <param name="enable">True to enable user control, false to disable.</param>
void EnableUserControl(bool enable);
/// <summary>
/// Gets the SolidWorks application instance.
/// </summary>
/// <returns>The SldWorks instance.</returns>
SldWorks GetNativeSldWorks();
/// <summary>
/// Closes the document with the specified title.
/// </summary>
/// <param name="documentTitle"></param>
/// <returns></returns>
bool CloseDocument(string documentTitle);
/// <summary>
/// Event raised when the active document changes.
/// </summary>
event EventHandler ActiveDocumentChanged;
}
/// <summary>
/// Service for managing SolidWorks application connection and document lifecycle.
/// </summary>
public class SolidWorksService : ISolidWorksService
{
private SldWorks _sldWorks;
private bool _disposed;
/// <summary>
/// Event raised when the active document changes in SolidWorks.
/// </summary>
public event EventHandler ActiveDocumentChanged;
/// <summary>
/// Connects to the SolidWorks application asynchronously.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when connection to SolidWorks fails.</exception>
public async Task ConnectAsync()
{
await Task.Run(() =>
{
try
{
// Try to get running instance first
_sldWorks = GetRunningInstance();
// If no running instance, create new one
if (_sldWorks == null)
{
_sldWorks = CreateNewInstance();
}
if (_sldWorks == null)
{
throw new InvalidOperationException(
"Failed to connect to SolidWorks. Please ensure SolidWorks is installed.");
}
// Make SolidWorks visible
_sldWorks.Visible = true;
// Subscribe to document change events
_sldWorks.ActiveModelDocChangeNotify += OnSolidWorksActiveDocChanged;
}
catch (COMException ex)
{
throw new InvalidOperationException(
$"COM error while connecting to SolidWorks: {ex.Message}", ex);
}
});
}
/// <summary>
/// Gets the currently active SolidWorks document.
/// </summary>
/// <returns>The active document wrapper, or null if no document is open.</returns>
public SolidWorksDocument GetActiveDocument()
{
if (_sldWorks == null)
return null;
var model = _sldWorks.ActiveDoc as ModelDoc2;
if (model == null)
return null;
return CreateDocumentWrapper(model);
}
/// <summary>
/// Enables or disables user control of the SolidWorks application.
/// When disabled, SolidWorks won't show dialogs or allow user interaction.
/// </summary>
/// <param name="enable">True to enable user control, false to disable.</param>
public void EnableUserControl(bool enable)
{
if (_sldWorks != null)
{
_sldWorks.UserControl = enable;
}
}
/// <summary>
/// Gets the native SolidWorks application instance.
/// Use this when you need direct access to the SolidWorks API.
/// </summary>
/// <returns>The SldWorks instance, or null if not connected.</returns>
public SldWorks GetNativeSldWorks()
{
return _sldWorks;
}
/// <summary>
/// Checks if SolidWorks is connected and ready.
/// </summary>
public bool IsConnected => _sldWorks != null;
/// <summary>
/// Gets the version of the connected SolidWorks instance.
/// </summary>
public string GetSolidWorksVersion()
{
if (_sldWorks == null)
return null;
try
{
return _sldWorks.RevisionNumber();
}
catch
{
return "Unknown";
}
}
/// <summary>
/// Closes a document by its title.
/// </summary>
/// <param name="documentTitle">The title of the document to close.</param>
/// <returns>True if successfully closed, false otherwise.</returns>
public bool CloseDocument(string documentTitle)
{
if (_sldWorks == null || string.IsNullOrWhiteSpace(documentTitle))
return false;
try
{
_sldWorks.CloseDoc(documentTitle);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Creates a new document from a template.
/// </summary>
/// <param name="templatePath">Path to the template file.</param>
/// <param name="paperSize">Paper size for drawings.</param>
/// <param name="width">Width dimension.</param>
/// <param name="height">Height dimension.</param>
/// <returns>The created document, or null if creation failed.</returns>
public ModelDoc2 CreateDocument(string templatePath, int paperSize, double width, double height)
{
if (_sldWorks == null)
return null;
try
{
return _sldWorks.NewDocument(templatePath, paperSize, width, height) as ModelDoc2;
}
catch
{
return null;
}
}
/// <summary>
/// Opens an existing document.
/// </summary>
/// <param name="filePath">Full path to the document.</param>
/// <param name="options">Open options.</param>
/// <param name="configuration">Configuration to open (empty for default).</param>
/// <returns>The opened document, or null if open failed.</returns>
public ModelDoc2 OpenDocument(string filePath, int options = 0, string configuration = "")
{
if (_sldWorks == null || string.IsNullOrWhiteSpace(filePath))
return null;
try
{
int errors = 0;
int warnings = 0;
var doc = _sldWorks.OpenDoc6(
filePath,
(int)GetDocumentType(filePath),
options,
configuration,
ref errors,
ref warnings);
return doc as ModelDoc2;
}
catch
{
return null;
}
}
#region Private Methods
private SldWorks GetRunningInstance()
{
try
{
return Marshal.GetActiveObject("SldWorks.Application") as SldWorks;
}
catch (COMException)
{
// No running instance
return null;
}
}
private SldWorks CreateNewInstance()
{
try
{
var type = Type.GetTypeFromProgID("SldWorks.Application");
if (type == null)
return null;
return Activator.CreateInstance(type) as SldWorks;
}
catch
{
return null;
}
}
private SolidWorksDocument CreateDocumentWrapper(ModelDoc2 model)
{
return new SolidWorksDocument
{
Title = model.GetTitle(),
FilePath = model.GetPathName(),
DocumentType = DetermineDocumentType(model),
NativeDocument = model
};
}
private DocumentType DetermineDocumentType(ModelDoc2 model)
{
if (model is PartDoc) return DocumentType.Part;
if (model is AssemblyDoc) return DocumentType.Assembly;
if (model is DrawingDoc) return DocumentType.Drawing;
return DocumentType.Unknown;
}
private swDocumentTypes_e GetDocumentType(string filePath)
{
var extension = System.IO.Path.GetExtension(filePath)?.ToLowerInvariant();
switch (extension)
{
case ".sldprt":
return swDocumentTypes_e.swDocPART;
case ".sldasm":
return swDocumentTypes_e.swDocASSEMBLY;
case ".slddrw":
return swDocumentTypes_e.swDocDRAWING;
default:
return swDocumentTypes_e.swDocNONE;
}
}
private int OnSolidWorksActiveDocChanged()
{
try
{
ActiveDocumentChanged?.Invoke(this, EventArgs.Empty);
}
catch
{
// Swallow exceptions from event handlers
}
return 1; // SolidWorks expects 1 to be returned
}
#endregion
#region IDisposable Implementation
/// <summary>
/// Releases all resources used by the SolidWorksService.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing">True to release both managed and unmanaged resources;
/// false to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
// Unsubscribe from events
if (_sldWorks != null)
{
try
{
_sldWorks.ActiveModelDocChangeNotify -= OnSolidWorksActiveDocChanged;
}
catch
{
// Ignore errors during cleanup
}
}
}
// Release COM object
if (_sldWorks != null)
{
try
{
Marshal.ReleaseComObject(_sldWorks);
}
catch
{
// Ignore errors during cleanup
}
_sldWorks = null;
}
_disposed = true;
}
public SldWorks GetSldWorks()
{
if (_sldWorks == null)
{
_sldWorks = GetRunningInstance();
return _sldWorks;
}
else
{
_sldWorks = CreateNewInstance();
return _sldWorks;
}
}
/// <summary>
/// Finalizer to ensure COM objects are released.
/// </summary>
~SolidWorksService()
{
Dispose(false);
}
#endregion
}
}

View File

@@ -1,22 +0,0 @@
using System;
namespace ExportDXF
{
public static class Units
{
/// <summary>
/// Multiply factor needed to convert the desired units to meters.
/// </summary>
public static double ScaleFactor = 0.0254; // inches to meters
public static double ToSldWorks(this double d)
{
return Math.Round(d * ScaleFactor, 8);
}
public static double FromSldWorks(this double d)
{
return Math.Round(d / ScaleFactor, 8);
}
}
}

View File

@@ -0,0 +1,43 @@
namespace ExportDXF.Utilities
{
/// <summary>
/// Contains sheet metal properties extracted from a SolidWorks part.
/// </summary>
public class SheetMetalProperties
{
/// <summary>
/// Material thickness in millimeters.
/// </summary>
public double Thickness { get; set; }
/// <summary>
/// K-factor for bend calculations.
/// </summary>
public double KFactor { get; set; }
/// <summary>
/// Bend radius in millimeters.
/// </summary>
public double BendRadius { get; set; }
/// <summary>
/// Bend allowance in millimeters.
/// </summary>
public double BendAllowance { get; set; }
/// <summary>
/// Whether auto relief is enabled.
/// </summary>
public bool AutoRelief { get; set; }
/// <summary>
/// Relief ratio for auto relief.
/// </summary>
public double ReliefRatio { get; set; }
public override string ToString()
{
return $"Thickness: {Thickness:F2}mm, K-Factor: {KFactor:F3}, Bend Radius: {BendRadius:F2}mm";
}
}
}

View File

@@ -0,0 +1,513 @@
using ExportDXF.Extensions;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Collections.Generic;
using System.Linq;
namespace ExportDXF.Utilities
{
/// <summary>
/// Utility class for SolidWorks-specific operations and helper functions.
/// </summary>
public static class SolidWorksHelper
{
#region Feature Names Constants
private const string FLAT_PATTERN_FEATURE = "FlatPattern";
private const string SHEET_METAL_FEATURE = "SheetMetal";
private const string BASE_FLANGE_FEATURE = "BaseFlange";
private const string FLAT_PATTERN_FOLDER = "Flat-Pattern";
#endregion
#region Sheet Metal Operations
/// <summary>
/// Gets sheet metal properties from the model.
/// </summary>
/// <param name="model">The model to extract properties from.</param>
/// <returns>Sheet metal properties, or null if not a sheet metal part.</returns>
public static SheetMetalProperties GetSheetMetalProperties(ModelDoc2 model)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
var sheetMetalFeature = model.GetFeatureByTypeName(SHEET_METAL_FEATURE);
if (sheetMetalFeature == null)
return null;
var sheetMetalData = sheetMetalFeature.GetDefinition() as SheetMetalFeatureData;
if (sheetMetalData == null)
return null;
return new SheetMetalProperties
{
Thickness = sheetMetalData.Thickness.FromSolidWorksToInches(),
KFactor = sheetMetalData.KFactor,
BendRadius = sheetMetalData.BendRadius.FromSolidWorksToInches(),
BendAllowance = sheetMetalData.BendAllowance.FromSolidWorksToInches(),
AutoRelief = sheetMetalData.UseAutoRelief,
ReliefRatio = sheetMetalData.ReliefRatio
};
}
/// <summary>
/// Configures flat pattern settings for optimal DXF export.
/// Unchecks corner treatment and enables simplify bends.
/// </summary>
/// <param name="model">The model containing the flat pattern.</param>
/// <returns>True if settings were successfully modified.</returns>
public static bool ConfigureFlatPatternSettings(ModelDoc2 model)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
var flatPattern = model.GetFeatureByTypeName(FLAT_PATTERN_FEATURE);
if (flatPattern == null)
return false;
var featureData = flatPattern.GetDefinition() as FlatPatternFeatureData;
if (featureData == null)
return false;
try
{
// Configure for cleaner DXF output
featureData.CornerTreatment = false; // Remove corner treatments
featureData.SimplifyBends = true; // Simplify bend representations
return flatPattern.ModifyDefinition(featureData, model, null);
}
catch
{
return false;
}
}
/// <summary>
/// Sets the suppression state of the flat pattern feature.
/// </summary>
/// <param name="model">The model containing the flat pattern.</param>
/// <param name="suppressionState">The desired suppression state.</param>
/// <returns>True if the flat pattern is suppressed after the operation.</returns>
public static bool SetFlatPatternSuppressionState(
ModelDoc2 model,
swComponentSuppressionState_e suppressionState)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
var flatPattern = model.GetFeatureByTypeName(FLAT_PATTERN_FEATURE);
if (flatPattern == null)
return false;
try
{
flatPattern.SetSuppression((int)suppressionState);
return flatPattern.IsSuppressed();
}
catch
{
return false;
}
}
/// <summary>
/// Gets the flat pattern feature from a model.
/// </summary>
/// <param name="model">The model to search.</param>
/// <returns>The flat pattern feature, or null if not found.</returns>
public static Feature GetFlatPatternFeature(ModelDoc2 model)
{
return model?.GetFeatureByTypeName(FLAT_PATTERN_FEATURE);
}
/// <summary>
/// Checks if a model has a flat pattern feature.
/// </summary>
/// <param name="model">The model to check.</param>
/// <returns>True if the model has a flat pattern feature.</returns>
public static bool HasFlatPattern(ModelDoc2 model)
{
return GetFlatPatternFeature(model) != null;
}
#endregion
#region Feature Operations
/// <summary>
/// Gets all features of a specific type from a model.
/// </summary>
/// <param name="model">The model to search.</param>
/// <param name="featureTypeName">The name of the feature type to find.</param>
/// <returns>A list of features of the specified type.</returns>
public static List<Feature> GetFeaturesByTypeName(ModelDoc2 model, string featureTypeName)
{
if (model == null || string.IsNullOrEmpty(featureTypeName))
return new List<Feature>();
var features = new List<Feature>();
var feature = model.FirstFeature() as Feature;
while (feature != null)
{
if (string.Equals(feature.GetTypeName2(), featureTypeName, StringComparison.OrdinalIgnoreCase))
{
features.Add(feature);
}
// Check sub-features
var subFeature = feature.GetFirstSubFeature() as Feature;
while (subFeature != null)
{
if (string.Equals(subFeature.GetTypeName2(), featureTypeName, StringComparison.OrdinalIgnoreCase))
{
features.Add(subFeature);
}
subFeature = subFeature.GetNextSubFeature() as Feature;
}
feature = feature.GetNextFeature() as Feature;
}
return features;
}
/// <summary>
/// Suppresses or unsuppresses a feature by name.
/// </summary>
/// <param name="model">The model containing the feature.</param>
/// <param name="featureName">The name of the feature to suppress/unsuppress.</param>
/// <param name="suppress">True to suppress, false to unsuppress.</param>
/// <returns>True if the operation was successful.</returns>
public static bool SetFeatureSuppressionByName(ModelDoc2 model, string featureName, bool suppress)
{
if (model == null || string.IsNullOrEmpty(featureName))
return false;
try
{
// Use Extension.SelectByID2 to find and select the feature
var modelExtension = model.Extension;
bool selected = modelExtension.SelectByID2(featureName, "BODYFEATURE", 0, 0, 0, false, 0, null, 0);
if (!selected)
return false;
var selectionManager = model.SelectionManager as SelectionMgr;
var feature = selectionManager.GetSelectedObject6(1, -1) as Feature;
if (feature == null)
return false;
var suppressionState = suppress
? swFeatureSuppressionAction_e.swSuppressFeature
: swFeatureSuppressionAction_e.swUnSuppressFeature;
feature.SetSuppression((int)suppressionState);
// Clear the selection
model.ClearSelection2(true);
return true;
}
catch
{
return false;
}
}
#endregion
#region Configuration Operations
/// <summary>
/// Gets all configuration names from a model.
/// </summary>
/// <param name="model">The model to query.</param>
/// <returns>A list of configuration names.</returns>
public static List<string> GetConfigurationNames(ModelDoc2 model)
{
if (model == null)
return new List<string>();
var configNames = (string[])model.GetConfigurationNames();
return configNames?.ToList() ?? new List<string>();
}
/// <summary>
/// Activates a specific configuration.
/// </summary>
/// <param name="model">The model to modify.</param>
/// <param name="configurationName">The name of the configuration to activate.</param>
/// <returns>True if the configuration was successfully activated.</returns>
public static bool ActivateConfiguration(ModelDoc2 model, string configurationName)
{
if (model == null || string.IsNullOrEmpty(configurationName))
return false;
try
{
return model.ShowConfiguration2(configurationName);
}
catch
{
return false;
}
}
/// <summary>
/// Gets the active configuration name.
/// </summary>
/// <param name="model">The model to query.</param>
/// <returns>The name of the active configuration, or null if unavailable.</returns>
public static string GetActiveConfigurationName(ModelDoc2 model)
{
if (model == null)
return null;
try
{
var config = model.GetActiveConfiguration() as Configuration;
return config?.Name;
}
catch
{
return null;
}
}
#endregion
#region Custom Properties
/// <summary>
/// Gets a custom property value from a model.
/// </summary>
/// <param name="model">The model to query.</param>
/// <param name="propertyName">The name of the property.</param>
/// <param name="configurationName">The configuration name (empty string for file-level properties).</param>
/// <returns>The property value, or null if not found.</returns>
public static string GetCustomProperty(ModelDoc2 model, string propertyName, string configurationName = "")
{
if (model == null || string.IsNullOrEmpty(propertyName))
return null;
try
{
var customPropertyManager = model.Extension.CustomPropertyManager[configurationName ?? ""];
return customPropertyManager.Get(propertyName);
}
catch
{
return null;
}
}
/// <summary>
/// Sets a custom property value on a model.
/// </summary>
/// <param name="model">The model to modify.</param>
/// <param name="propertyName">The name of the property.</param>
/// <param name="propertyValue">The value to set.</param>
/// <param name="configurationName">The configuration name (empty string for file-level properties).</param>
/// <returns>True if the property was successfully set.</returns>
public static bool SetCustomProperty(ModelDoc2 model, string propertyName, string propertyValue, string configurationName = "")
{
if (model == null || string.IsNullOrEmpty(propertyName))
return false;
try
{
var customPropertyManager = model.Extension.CustomPropertyManager[configurationName ?? ""];
var result = customPropertyManager.Add3(propertyName, (int)swCustomInfoType_e.swCustomInfoText, propertyValue, (int)swCustomPropertyAddOption_e.swCustomPropertyReplaceValue);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Gets all custom property names from a model.
/// </summary>
/// <param name="model">The model to query.</param>
/// <param name="configurationName">The configuration name (empty string for file-level properties).</param>
/// <returns>A list of custom property names.</returns>
public static List<string> GetCustomPropertyNames(ModelDoc2 model, string configurationName = "")
{
if (model == null)
return new List<string>();
try
{
var customPropertyManager = model.Extension.CustomPropertyManager[configurationName ?? ""];
var propertyNames = (string[])customPropertyManager.GetNames();
return propertyNames?.ToList() ?? new List<string>();
}
catch
{
return new List<string>();
}
}
#endregion
#region Material Operations
/// <summary>
/// Gets the material name for a part document.
/// </summary>
/// <param name="part">The part document.</param>
/// <param name="configurationName">The configuration name.</param>
/// <returns>The material name, or null if not assigned.</returns>
public static string GetMaterial(PartDoc part, string configurationName)
{
if (part == null)
return null;
try
{
return part.GetMaterialPropertyName2(configurationName, out _);
}
catch
{
return null;
}
}
/// <summary>
/// Sets the material for a part document.
/// </summary>
/// <param name="part">The part document.</param>
/// <param name="materialName">The name of the material to assign.</param>
/// <param name="databasePath">The path to the material database.</param>
/// <returns>True if the material was successfully assigned.</returns>
public static bool SetMaterial(PartDoc part, string materialName, string databasePath)
{
if (part == null || string.IsNullOrEmpty(materialName))
return false;
try
{
part.SetMaterialPropertyName2("", databasePath, materialName);
return true;
}
catch
{
return false;
}
}
#endregion
#region Document Operations
/// <summary>
/// Gets the file path of a model document.
/// </summary>
/// <param name="model">The model document.</param>
/// <returns>The full file path, or empty string if not saved.</returns>
public static string GetFilePath(ModelDoc2 model)
{
return model?.GetPathName() ?? string.Empty;
}
/// <summary>
/// Gets the document title (filename without path).
/// </summary>
/// <param name="model">The model document.</param>
/// <returns>The document title.</returns>
public static string GetTitle(ModelDoc2 model)
{
return model?.GetTitle() ?? string.Empty;
}
/// <summary>
/// Checks if a document has been saved.
/// </summary>
/// <param name="model">The model document.</param>
/// <returns>True if the document has been saved to disk.</returns>
public static bool IsSaved(ModelDoc2 model)
{
return !string.IsNullOrEmpty(GetFilePath(model));
}
/// <summary>
/// Saves a document.
/// </summary>
/// <param name="model">The model document to save.</param>
/// <returns>True if the save operation was successful.</returns>
public static bool Save(ModelDoc2 model)
{
if (model == null)
return false;
try
{
int errors = 0;
int warnings = 0;
return model.Save3((int)swSaveAsOptions_e.swSaveAsOptions_Silent, ref errors, ref warnings);
}
catch
{
return false;
}
}
#endregion
#region Component Operations
/// <summary>
/// Resolves a lightweight component to fully loaded.
/// </summary>
/// <param name="component">The component to resolve.</param>
/// <returns>True if the component was successfully resolved.</returns>
public static bool ResolveComponent(Component2 component)
{
if (component == null)
return false;
try
{
component.SetLightweightToResolved();
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Checks if a component is suppressed.
/// </summary>
/// <param name="component">The component to check.</param>
/// <returns>True if the component is suppressed.</returns>
public static bool IsComponentSuppressed(Component2 component)
{
return component?.IsSuppressed() ?? false;
}
/// <summary>
/// Gets the model document from a component.
/// </summary>
/// <param name="component">The component.</param>
/// <returns>The model document, or null if unavailable.</returns>
public static ModelDoc2 GetModelFromComponent(Component2 component)
{
return component?.GetModelDoc2() as ModelDoc2;
}
#endregion
}
}

View File

@@ -0,0 +1,274 @@
using System;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace ExportDXF.Utilities
{
/// <summary>
/// Utility class for text processing and string manipulation operations.
/// </summary>
public static class TextHelper
{
private static readonly Regex XmlTagRegex = new Regex(@"<[^>]+>", RegexOptions.Compiled);
private static readonly Regex WhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled);
private static readonly Regex FontTagRegex = new Regex(@"<FONT.*?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// Removes all XML tags from the input string.
/// </summary>
/// <param name="input">The string containing XML tags to remove.</param>
/// <returns>The string with all XML tags removed, or the original input if null/empty.</returns>
public static string RemoveXmlTags(string input)
{
if (string.IsNullOrEmpty(input))
return input;
return XmlTagRegex.Replace(input, string.Empty);
}
/// <summary>
/// Removes specific SolidWorks font XML tags from the input string.
/// This is more targeted than RemoveXmlTags and handles SolidWorks-specific formatting.
/// </summary>
/// <param name="input">The string containing font tags to remove.</param>
/// <returns>The string with font tags removed.</returns>
public static string RemoveFontXmlTags(string input)
{
if (string.IsNullOrEmpty(input))
return input;
var result = input;
var matches = FontTagRegex.Matches(result);
// Process matches in reverse order to maintain indices
for (int i = matches.Count - 1; i >= 0; i--)
{
var match = matches[i];
result = result.Remove(match.Index, match.Length);
}
return result;
}
/// <summary>
/// Normalizes whitespace in a string by replacing multiple consecutive whitespace characters with a single space.
/// </summary>
/// <param name="input">The string to normalize.</param>
/// <returns>The string with normalized whitespace.</returns>
public static string NormalizeWhitespace(string input)
{
if (string.IsNullOrEmpty(input))
return input;
return WhitespaceRegex.Replace(input.Trim(), " ");
}
/// <summary>
/// Cleans text by removing XML tags and normalizing whitespace.
/// This is a common operation for processing text from SolidWorks.
/// </summary>
/// <param name="input">The text to clean.</param>
/// <returns>Cleaned text with XML tags removed and whitespace normalized.</returns>
public static string CleanText(string input)
{
if (string.IsNullOrEmpty(input))
return input;
var cleaned = RemoveXmlTags(input);
return NormalizeWhitespace(cleaned);
}
/// <summary>
/// Returns a number with its ordinal suffix (1st, 2nd, 3rd, 4th, etc.).
/// </summary>
/// <param name="number">The number to format.</param>
/// <returns>The number with appropriate ordinal suffix.</returns>
public static string GetOrdinalSuffix(int number)
{
if (number <= 0)
return number.ToString();
// Special cases for 11th, 12th, 13th
if (number >= 11 && number <= 13)
return number + "th";
return number + GetSuffix(number % 10);
}
/// <summary>
/// Converts a string to title case (first letter of each word capitalized).
/// </summary>
/// <param name="input">The string to convert.</param>
/// <returns>The string in title case.</returns>
public static string ToTitleCase(string input)
{
if (string.IsNullOrEmpty(input))
return input;
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(input.ToLowerInvariant());
}
/// <summary>
/// Truncates a string to the specified maximum length, optionally adding an ellipsis.
/// </summary>
/// <param name="input">The string to truncate.</param>
/// <param name="maxLength">The maximum length of the result.</param>
/// <param name="useEllipsis">Whether to add "..." when truncating.</param>
/// <returns>The truncated string.</returns>
public static string Truncate(string input, int maxLength, bool useEllipsis = true)
{
if (string.IsNullOrEmpty(input))
return input;
if (input.Length <= maxLength)
return input;
if (useEllipsis && maxLength > 3)
{
return input.Substring(0, maxLength - 3) + "...";
}
return input.Substring(0, maxLength);
}
/// <summary>
/// Removes invalid filename characters from a string, replacing them with underscores.
/// </summary>
/// <param name="filename">The filename to sanitize.</param>
/// <returns>A safe filename with invalid characters replaced.</returns>
public static string SanitizeFileName(string filename)
{
if (string.IsNullOrEmpty(filename))
return filename;
var invalidChars = System.IO.Path.GetInvalidFileNameChars();
var sb = new StringBuilder(filename);
foreach (var invalidChar in invalidChars)
{
sb.Replace(invalidChar, '_');
}
// Also replace some additional problematic characters
sb.Replace(' ', '_'); // Spaces can be problematic
sb.Replace('"', '\''); // Double quotes to single quotes
return sb.ToString();
}
/// <summary>
/// Checks if a string is null, empty, or contains only whitespace.
/// </summary>
/// <param name="input">The string to check.</param>
/// <returns>True if the string is null, empty, or whitespace only.</returns>
public static bool IsNullOrWhiteSpace(string input)
{
return string.IsNullOrWhiteSpace(input);
}
/// <summary>
/// Safely gets a substring without throwing exceptions for invalid indices.
/// </summary>
/// <param name="input">The source string.</param>
/// <param name="startIndex">The starting index.</param>
/// <param name="length">The length of the substring.</param>
/// <returns>The substring, or empty string if indices are invalid.</returns>
public static string SafeSubstring(string input, int startIndex, int length)
{
if (string.IsNullOrEmpty(input) || startIndex < 0 || startIndex >= input.Length)
return string.Empty;
var actualLength = Math.Min(length, input.Length - startIndex);
return actualLength <= 0 ? string.Empty : input.Substring(startIndex, actualLength);
}
/// <summary>
/// Safely gets a substring from the start index to the end of the string.
/// </summary>
/// <param name="input">The source string.</param>
/// <param name="startIndex">The starting index.</param>
/// <returns>The substring from start index to end, or empty if invalid.</returns>
public static string SafeSubstring(string input, int startIndex)
{
if (string.IsNullOrEmpty(input) || startIndex < 0 || startIndex >= input.Length)
return string.Empty;
return input.Substring(startIndex);
}
/// <summary>
/// Pads a string to a specific length, truncating if too long.
/// </summary>
/// <param name="input">The string to pad or truncate.</param>
/// <param name="totalLength">The desired total length.</param>
/// <param name="paddingChar">The character to use for padding.</param>
/// <param name="padLeft">True to pad on the left, false to pad on the right.</param>
/// <returns>A string of exactly the specified length.</returns>
public static string PadOrTruncate(string input, int totalLength, char paddingChar = ' ', bool padLeft = false)
{
if (string.IsNullOrEmpty(input))
input = string.Empty;
if (input.Length == totalLength)
return input;
if (input.Length > totalLength)
return input.Substring(0, totalLength);
return padLeft
? input.PadLeft(totalLength, paddingChar)
: input.PadRight(totalLength, paddingChar);
}
/// <summary>
/// Converts a string to a safe identifier (letters, numbers, underscores only).
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>A safe identifier string.</returns>
public static string ToSafeIdentifier(string input)
{
if (string.IsNullOrEmpty(input))
return "Identifier";
var sb = new StringBuilder();
foreach (char c in input)
{
if (char.IsLetterOrDigit(c))
{
sb.Append(c);
}
else if (c == ' ' || c == '-' || c == '.')
{
sb.Append('_');
}
}
var result = sb.ToString();
// Ensure it starts with a letter or underscore
if (result.Length > 0 && char.IsDigit(result[0]))
{
result = "_" + result;
}
return string.IsNullOrEmpty(result) ? "Identifier" : result;
}
#region Private Helper Methods
private static string GetSuffix(int lastDigit)
{
switch (lastDigit)
{
case 1: return "st";
case 2: return "nd";
case 3: return "rd";
default: return "th";
}
}
#endregion
}
}

View File

@@ -1,11 +1,12 @@
using SolidWorks.Interop.sldworks;
using ExportDXF.Extensions;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace ExportDXF
namespace ExportDXF.Utilities
{
internal static class ViewHelper
{

View File

@@ -1,4 +1,5 @@
using System.Windows.Forms;
using ExportDXF.Utilities;
using System.Windows.Forms;
namespace ExportDXF.ViewFlipDeciders
{

View File

@@ -1,4 +1,5 @@
using System.Linq;
using ExportDXF.Utilities;
using System.Linq;
namespace ExportDXF.ViewFlipDeciders
{

View File

@@ -1,4 +1,5 @@
using System.Linq;
using ExportDXF.Utilities;
using System.Linq;
using System.Windows.Forms;
namespace ExportDXF.ViewFlipDeciders

View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace ExportDXF.ViewFlipDeciders
{
/// <summary>
/// Factory for discovering and creating IViewFlipDecider implementations.
/// </summary>
public static class ViewFlipDeciderFactory
{
private static readonly object _lock = new object();
private static List<IViewFlipDecider> _cachedDeciders;
/// <summary>
/// Gets all available IViewFlipDecider implementations from loaded assemblies.
/// Results are cached for performance.
/// </summary>
/// <returns>An enumerable collection of IViewFlipDecider instances.</returns>
public static IEnumerable<IViewFlipDecider> GetAvailableDeciders()
{
if (_cachedDeciders != null)
return _cachedDeciders;
lock (_lock)
{
if (_cachedDeciders != null)
return _cachedDeciders;
_cachedDeciders = DiscoverDeciders().ToList();
return _cachedDeciders;
}
}
/// <summary>
/// Gets a specific view flip decider by name.
/// </summary>
/// <param name="name">The name of the decider to retrieve.</param>
/// <returns>The matching decider, or null if not found.</returns>
public static IViewFlipDecider GetDeciderByName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return null;
return GetAvailableDeciders()
.FirstOrDefault(d => string.Equals(d.Name, name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Gets the default (Automatic) view flip decider, or the first available if Automatic doesn't exist.
/// </summary>
/// <returns>The default decider.</returns>
public static IViewFlipDecider GetDefaultDecider()
{
var automatic = GetDeciderByName("Automatic");
if (automatic != null)
return automatic;
return GetAvailableDeciders().FirstOrDefault();
}
/// <summary>
/// Clears the cached deciders, forcing rediscovery on next call.
/// Useful for testing or if assemblies are loaded dynamically.
/// </summary>
public static void ClearCache()
{
lock (_lock)
{
_cachedDeciders = null;
}
}
/// <summary>
/// Discovers all IViewFlipDecider implementations in loaded assemblies.
/// </summary>
private static IEnumerable<IViewFlipDecider> DiscoverDeciders()
{
var deciderType = typeof(IViewFlipDecider);
var discoveredTypes = new List<Type>();
try
{
// Search all loaded assemblies
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
try
{
var types = assembly.GetTypes()
.Where(t => deciderType.IsAssignableFrom(t) &&
t.IsClass &&
!t.IsAbstract &&
HasParameterlessConstructor(t));
discoveredTypes.AddRange(types);
}
catch (ReflectionTypeLoadException)
{
// Skip assemblies that can't be loaded
continue;
}
}
}
catch (Exception)
{
// If discovery fails entirely, return empty collection
yield break;
}
// Create instances of discovered types
foreach (var type in discoveredTypes)
{
IViewFlipDecider instance = null;
try
{
instance = (IViewFlipDecider)Activator.CreateInstance(type);
}
catch (Exception)
{
// Skip types that can't be instantiated
continue;
}
if (instance != null)
{
yield return instance;
}
}
}
/// <summary>
/// Checks if a type has a public parameterless constructor.
/// </summary>
private static bool HasParameterlessConstructor(Type type)
{
return type.GetConstructor(
BindingFlags.Public | BindingFlags.Instance,
null,
Type.EmptyTypes,
null) != null;
}
}
}