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

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "EtchBendLines"] [submodule "EtchBendLines"]
path = EtchBendLines path = EtchBendLines
url = https://git.nforge.net/aj/etchbendlines.git url = https://git.thecozycat.net/aj/etchbendlines.git

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="BendOrientation.cs" />
<Compile Include="Bounds.cs" /> <Compile Include="Bounds.cs" />
<Compile Include="DrawingInfo.cs" /> <Compile Include="DrawingInfo.cs" />
<Compile Include="BomToExcel.cs" /> <Compile Include="Extensions\StringExtensions.cs" />
<Compile Include="Extensions.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\AssemblyItemExtractor.cs" />
<Compile Include="ItemExtractors\BomColumnIndices.cs" /> <Compile Include="ItemExtractors\BomColumnIndices.cs" />
<Compile Include="ItemExtractors\BomItemExtractor.cs" /> <Compile Include="ItemExtractors\BomItemExtractor.cs" />
<Compile Include="ItemExtractors\ItemExtractor.cs" /> <Compile Include="ItemExtractors\ItemExtractor.cs" />
<Compile Include="Forms\ViewFlipDeciderComboboxItem.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\AskViewFlipDecider.cs" />
<Compile Include="ViewFlipDeciders\AutoViewFlipDecider.cs" /> <Compile Include="ViewFlipDeciders\AutoViewFlipDecider.cs" />
<Compile Include="ViewFlipDeciders\IViewFlipDecider.cs" /> <Compile Include="ViewFlipDeciders\IViewFlipDecider.cs" />
<Compile Include="Helper.cs" />
<Compile Include="Forms\MainForm.cs"> <Compile Include="Forms\MainForm.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@@ -114,10 +130,8 @@
</Compile> </Compile>
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SolidWorksExtensions.cs" />
<Compile Include="Units.cs" />
<Compile Include="ViewFlipDeciders\PreferUpViewFlipDecider.cs" /> <Compile Include="ViewFlipDeciders\PreferUpViewFlipDecider.cs" />
<Compile Include="ViewHelper.cs" /> <Compile Include="ViewFlipDeciders\ViewFlipDeciderFactory.cs" />
<EmbeddedResource Include="Forms\MainForm.resx"> <EmbeddedResource Include="Forms\MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon> <DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource> </EmbeddedResource>
@@ -185,6 +199,7 @@
<Name>EtchBendLines</Name> <Name>EtchBendLines</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- 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. 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.Diagnostics;
using System.Linq; using System.Linq;
namespace ExportDXF namespace ExportDXF.Extensions
{ {
public static class SolidWorksExtensions 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.ReadOnly = true;
this.activeDocTitleBox.Size = new System.Drawing.Size(584, 25); this.activeDocTitleBox.Size = new System.Drawing.Size(584, 25);
this.activeDocTitleBox.TabIndex = 2; this.activeDocTitleBox.TabIndex = 2;
this.activeDocTitleBox.TextChanged += new System.EventHandler(this.textBox1_TextChanged); this.activeDocTitleBox.TextChanged += new System.EventHandler(this.activeDocTitleBox_TextChanged);
// //
// richTextBox1 // richTextBox1
// //

View File

@@ -1,15 +1,11 @@
using ExportDXF.ItemExtractors; using ExportDXF.Extensions;
using ExportDXF.Models;
using ExportDXF.Services;
using ExportDXF.ViewFlipDeciders; using ExportDXF.ViewFlipDeciders;
using OfficeOpenXml;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System; using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing; using System.Drawing;
using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
@@ -17,627 +13,217 @@ namespace ExportDXF.Forms
{ {
public partial class MainForm : Form public partial class MainForm : Form
{ {
private SldWorks sldWorks; private readonly ISolidWorksService _solidWorksService;
private BackgroundWorker worker; private readonly IDxfExportService _exportService;
private DrawingDoc templateDrawing; private CancellationTokenSource _cancellationTokenSource;
private DateTime timeStarted;
private IViewFlipDecider viewFlipDecider;
public MainForm() public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService)
{ {
InitializeComponent(); InitializeComponent();
worker = new BackgroundWorker(); _solidWorksService = solidWorksService ??
worker.WorkerSupportsCancellation = true; throw new ArgumentNullException(nameof(solidWorksService));
worker.DoWork += Worker_DoWork;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
var type = typeof(IViewFlipDecider); _exportService = exportService ??
var types = AppDomain.CurrentDomain.GetAssemblies() throw new ArgumentNullException(nameof(exportService));
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) && p.IsClass)
.ToList();
comboBox1.DataSource = GetItems(); InitializeViewFlipDeciders();
comboBox1.DisplayMember = "Name";
} }
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); base.OnLoad(e);
button1.Enabled = false; button1.Enabled = false;
var task = new Task(ConnectToSolidWorks); await InitializeAsync();
task.ContinueWith((t) =>
{
Invoke(new MethodInvoker(() =>
{
SetActiveDocName();
button1.Enabled = true;
}));
});
task.Start();
} }
private List<ViewFlipDeciderComboboxItem> GetItems() private async Task InitializeAsync()
{ {
var types = AppDomain.CurrentDomain.GetAssemblies() try
.SelectMany(s => s.GetTypes()) {
.Where(p => typeof(IViewFlipDecider).IsAssignableFrom(p) && p.IsClass) 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(); .ToList();
var items = new List<ViewFlipDeciderComboboxItem>(); // Move "Automatic" to the top if it exists
foreach (var type in types)
{
var obj = (IViewFlipDecider)Activator.CreateInstance(type);
items.Add(new ViewFlipDeciderComboboxItem
{
Name = obj.Name,
ViewFlipDecider = obj
});
}
var automatic = items.FirstOrDefault(i => i.Name == "Automatic"); var automatic = items.FirstOrDefault(i => i.Name == "Automatic");
if (automatic != null) if (automatic != null)
{ {
items.Remove(automatic); items.Remove(automatic);
items.Insert(0, 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; CancelExport();
worker.CancelAsync();
return;
} }
else else
{ {
worker.RunWorkerAsync(); await StartExportAsync();
} }
} }
private void Worker_DoWork(object sender, DoWorkEventArgs e) private async Task StartExportAsync()
{
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)
{ {
try try
{ {
var itemExtractor = new BomItemExtractor(bom); _cancellationTokenSource = new CancellationTokenSource();
itemExtractor.SkipHiddenRows = true; var token = _cancellationTokenSource.Token;
Print($"Fetching components from {bom.BomFeature.Name}"); UpdateUIForExportStart();
return itemExtractor.GetItems(); var activeDoc = _solidWorksService.GetActiveDocument();
} if (activeDoc == null)
catch (Exception ex) {
{ LogMessage("No active document.", Color.Red);
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)
return; 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 exportContext = new ExportContext
var sheetMetalData = sheetMetal?.GetDefinition() as SheetMetalFeatureData;
if (sheetMetalData != null)
{ {
item.Thickness = sheetMetalData.Thickness.FromSldWorks(); ActiveDocument = activeDoc,
item.KFactor = sheetMetalData.KFactor; ViewFlipDecider = viewFlipDecider,
item.BendRadius = sheetMetalData.BendRadius.FromSldWorks(); FilePrefix = prefix,
} CancellationToken = token,
ProgressCallback = LogMessage
};
if (item.Description == null) LogMessage($"Started at {DateTime.Now:t}");
item.Description = model.Extension.CustomPropertyManager[config].Get("Description");
if (item.Description == null) await Task.Run(() => _exportService.Export(exportContext), token);
item.Description = model.Extension.CustomPropertyManager[""].Get("Description");
item.Description = RemoveFontXml(item.Description); LogMessage("Done.", Color.Green);
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();
} }
catch (OperationCanceledException)
try
{ {
var drawingInfo = DrawingInfo.Parse(prefix); LogMessage("Export canceled.", Color.Red);
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());
} }
catch (Exception ex) 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) button1.Enabled = false;
return null; _cancellationTokenSource?.Cancel();
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;
} }
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; richTextBox1.AppendText("\n\n");
}
else
{
return prefix + item.ItemNo.PadLeft(2, '0');
} }
} }
private bool SavePartToDXF(PartDoc part, string savePath) private void UpdateUIForExportComplete()
{ {
var partModel = part as ModelDoc2; activeDocTitleBox.Enabled = true;
var config = partModel.ConfigurationManager.ActiveConfiguration.Name; prefixTextBox.Enabled = true;
comboBox1.Enabled = true;
return SavePartToDXF(part, config, savePath); 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"); prefixTextBox.Text = $"{drawingInfo.JobNo} {drawingInfo.DrawingNo} PT";
return false; 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 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) private void LogMessage(string message, Color? color = null)
return; {
if (InvokeRequired)
var isDrawing = model is DrawingDoc;
var title = model.GetTitle();
if (isDrawing)
{ {
var drawingInfo = DrawingInfo.Parse(title); Invoke(new Action(() => LogMessage(message, color)));
return;
}
if (drawingInfo == null) if (color.HasValue)
return; {
richTextBox1.AppendText(message + System.Environment.NewLine, color.Value);
prefixTextBox.Text = $"{drawingInfo.JobNo} {drawingInfo.DrawingNo} PT";
prefixTextBox.SelectionStart = prefixTextBox.Text.Length;
} }
else 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; 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 SolidWorks.Interop.swconst;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -52,7 +55,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.ItemNumber != -1) if (columnIndices.ItemNumber != -1)
{ {
var x = table.DisplayedText[rowIndex, columnIndices.ItemNumber]; var x = table.DisplayedText[rowIndex, columnIndices.ItemNumber];
x = Helper.RemoveXmlTags(x); x = TextHelper.RemoveXmlTags(x);
double d; double d;
@@ -69,7 +72,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.PartNumber != -1) if (columnIndices.PartNumber != -1)
{ {
var x = table.DisplayedText[rowIndex, columnIndices.PartNumber]; var x = table.DisplayedText[rowIndex, columnIndices.PartNumber];
x = Helper.RemoveXmlTags(x); x = TextHelper.RemoveXmlTags(x);
item.PartName = x; item.PartName = x;
@@ -78,7 +81,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.Description != -1) if (columnIndices.Description != -1)
{ {
var x = table.DisplayedText[rowIndex, columnIndices.Description]; var x = table.DisplayedText[rowIndex, columnIndices.Description];
x = Helper.RemoveXmlTags(x); x = TextHelper.RemoveXmlTags(x);
item.Description = x; item.Description = x;
} }
@@ -86,7 +89,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.Quantity != -1) if (columnIndices.Quantity != -1)
{ {
var qtyString = table.DisplayedText[rowIndex, columnIndices.Quantity]; var qtyString = table.DisplayedText[rowIndex, columnIndices.Quantity];
qtyString = Helper.RemoveXmlTags(qtyString); qtyString = TextHelper.RemoveXmlTags(qtyString);
int qty = 0; int qty = 0;
int.TryParse(qtyString, out qty); 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 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; using System.Windows.Forms;
namespace ExportDXF namespace ExportDXF
{ {
internal static class Program static class Program
{ {
/// <summary> /// <summary>
/// The main entry point for the application. /// The main entry point for the application.
/// </summary> /// </summary>
[STAThread] [STAThread]
private static void Main() static void Main()
{ {
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); 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 SolidWorks.Interop.swconst;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace ExportDXF namespace ExportDXF.Utilities
{ {
internal static class ViewHelper internal static class ViewHelper
{ {

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
using System.Linq; using ExportDXF.Utilities;
using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
namespace ExportDXF.ViewFlipDeciders 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;
}
}
}