diff --git a/ExportDXF/Forms/MainForm.Designer.cs b/ExportDXF/Forms/MainForm.Designer.cs
index f3344c2..2e25fd5 100644
--- a/ExportDXF/Forms/MainForm.Designer.cs
+++ b/ExportDXF/Forms/MainForm.Designer.cs
@@ -54,10 +54,10 @@ namespace ExportDXF.Forms
// runButton
//
runButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
- runButton.Location = new System.Drawing.Point(514, 13);
+ runButton.Location = new System.Drawing.Point(821, 13);
runButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
runButton.Name = "runButton";
- runButton.Size = new System.Drawing.Size(100, 30);
+ runButton.Size = new System.Drawing.Size(100, 55);
runButton.TabIndex = 11;
runButton.Text = "Start";
runButton.UseVisualStyleBackColor = true;
@@ -91,7 +91,7 @@ namespace ExportDXF.Forms
mainTabControl.Name = "mainTabControl";
mainTabControl.Padding = new System.Drawing.Point(20, 5);
mainTabControl.SelectedIndex = 0;
- mainTabControl.Size = new System.Drawing.Size(599, 330);
+ mainTabControl.Size = new System.Drawing.Size(910, 441);
mainTabControl.TabIndex = 12;
//
// logEventsTab
@@ -100,7 +100,7 @@ namespace ExportDXF.Forms
logEventsTab.Location = new System.Drawing.Point(4, 30);
logEventsTab.Name = "logEventsTab";
logEventsTab.Padding = new System.Windows.Forms.Padding(3);
- logEventsTab.Size = new System.Drawing.Size(591, 296);
+ logEventsTab.Size = new System.Drawing.Size(902, 407);
logEventsTab.TabIndex = 0;
logEventsTab.Text = "Log Events";
logEventsTab.UseVisualStyleBackColor = true;
@@ -112,7 +112,7 @@ namespace ExportDXF.Forms
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
logEventsDataGrid.Name = "logEventsDataGrid";
- logEventsDataGrid.Size = new System.Drawing.Size(579, 282);
+ logEventsDataGrid.Size = new System.Drawing.Size(890, 391);
logEventsDataGrid.TabIndex = 0;
//
// bomTab
@@ -121,7 +121,7 @@ namespace ExportDXF.Forms
bomTab.Location = new System.Drawing.Point(4, 30);
bomTab.Name = "bomTab";
bomTab.Padding = new System.Windows.Forms.Padding(3);
- bomTab.Size = new System.Drawing.Size(982, 549);
+ bomTab.Size = new System.Drawing.Size(902, 407);
bomTab.TabIndex = 1;
bomTab.Text = "Bill Of Materials";
bomTab.UseVisualStyleBackColor = true;
@@ -133,30 +133,30 @@ namespace ExportDXF.Forms
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
bomDataGrid.Location = new System.Drawing.Point(6, 6);
bomDataGrid.Name = "bomDataGrid";
- bomDataGrid.Size = new System.Drawing.Size(970, 535);
+ bomDataGrid.Size = new System.Drawing.Size(1281, 644);
bomDataGrid.TabIndex = 1;
- //
+ //
// cutTemplatesTab
- //
+ //
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
cutTemplatesTab.Location = new System.Drawing.Point(4, 30);
cutTemplatesTab.Name = "cutTemplatesTab";
cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3);
- cutTemplatesTab.Size = new System.Drawing.Size(982, 549);
+ cutTemplatesTab.Size = new System.Drawing.Size(902, 407);
cutTemplatesTab.TabIndex = 2;
cutTemplatesTab.Text = "Cut Templates";
cutTemplatesTab.UseVisualStyleBackColor = true;
- //
+ //
// cutTemplatesDataGrid
- //
+ //
cutTemplatesDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
cutTemplatesDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6);
cutTemplatesDataGrid.Name = "cutTemplatesDataGrid";
- cutTemplatesDataGrid.Size = new System.Drawing.Size(970, 535);
+ cutTemplatesDataGrid.Size = new System.Drawing.Size(1281, 644);
cutTemplatesDataGrid.TabIndex = 2;
- //
+ //
// equipmentBox
//
equipmentBox.FormattingEnabled = true;
@@ -194,7 +194,7 @@ namespace ExportDXF.Forms
// MainForm
//
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
- ClientSize = new System.Drawing.Size(626, 416);
+ ClientSize = new System.Drawing.Size(937, 527);
Controls.Add(drawingNoBox);
Controls.Add(equipmentBox);
Controls.Add(mainTabControl);
diff --git a/ExportDXF/Forms/MainForm.cs b/ExportDXF/Forms/MainForm.cs
index f7d149b..d6a2411 100644
--- a/ExportDXF/Forms/MainForm.cs
+++ b/ExportDXF/Forms/MainForm.cs
@@ -390,6 +390,8 @@ namespace ExportDXF.Forms
ActiveDocument = activeDoc,
ViewFlipDecider = viewFlipDecider,
FilePrefix = filePrefix,
+ Equipment = equipment,
+ DrawingNo = drawingNo,
EquipmentId = null,
CancellationToken = token,
ProgressCallback = (msg, level, file) => LogMessage(msg, level, file),
@@ -468,21 +470,58 @@ namespace ExportDXF.Forms
var docTitle = activeDoc?.Title ?? "No Document Open";
this.Text = $"ExportDXF - {docTitle}";
- // Parse the file name and fill Equipment/Drawing dropdowns
- if (activeDoc != null)
- {
- var drawingInfo = DrawingInfo.Parse(activeDoc.Title);
- if (drawingInfo != null)
- {
- if (!equipmentBox.Items.Contains(drawingInfo.EquipmentNo))
- equipmentBox.Items.Add(drawingInfo.EquipmentNo);
- equipmentBox.Text = drawingInfo.EquipmentNo;
+ if (activeDoc == null)
+ return;
- if (!drawingNoBox.Items.Contains(drawingInfo.DrawingNo))
- drawingNoBox.Items.Add(drawingInfo.DrawingNo);
- drawingNoBox.Text = drawingInfo.DrawingNo;
+ // Try database first: look up the most recent export for this file path
+ DrawingInfo drawingInfo = null;
+
+ if (!string.IsNullOrEmpty(activeDoc.FilePath))
+ {
+ drawingInfo = LookupDrawingInfoFromHistory(activeDoc.FilePath);
+ }
+
+ // Fall back to parsing the document title
+ if (drawingInfo == null)
+ {
+ drawingInfo = DrawingInfo.Parse(activeDoc.Title);
+ }
+
+ if (drawingInfo != null)
+ {
+ if (!equipmentBox.Items.Contains(drawingInfo.EquipmentNo))
+ equipmentBox.Items.Add(drawingInfo.EquipmentNo);
+ equipmentBox.Text = drawingInfo.EquipmentNo;
+
+ if (!drawingNoBox.Items.Contains(drawingInfo.DrawingNo))
+ drawingNoBox.Items.Add(drawingInfo.DrawingNo);
+ drawingNoBox.Text = drawingInfo.DrawingNo;
+ }
+ }
+
+ private DrawingInfo LookupDrawingInfoFromHistory(string filePath)
+ {
+ try
+ {
+ using (var db = _dbContextFactory())
+ {
+ var drawingNumber = db.ExportRecords
+ .Where(r => r.SourceFilePath.ToLower() == filePath.ToLower()
+ && !string.IsNullOrEmpty(r.DrawingNumber))
+ .OrderByDescending(r => r.Id)
+ .Select(r => r.DrawingNumber)
+ .FirstOrDefault();
+
+ if (drawingNumber != null)
+ return DrawingInfo.Parse(drawingNumber);
}
}
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to look up drawing info from history: {ex.Message}");
+ }
+
+ return null;
}
private void LogMessage(string message, LogLevel level = LogLevel.Info, string file = null)
diff --git a/ExportDXF/Migrations/20260214044511_InitialCreate.Designer.cs b/ExportDXF/Migrations/20260214044511_InitialCreate.Designer.cs
new file mode 100644
index 0000000..4c4858b
--- /dev/null
+++ b/ExportDXF/Migrations/20260214044511_InitialCreate.Designer.cs
@@ -0,0 +1,153 @@
+//
+using System;
+using ExportDXF.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ExportDXF.Migrations
+{
+ [DbContext(typeof(ExportDxfDbContext))]
+ [Migration("20260214044511_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ID"));
+
+ b.Property("ConfigurationName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ContentHash")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CutTemplateName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("DefaultBendRadius")
+ .HasColumnType("float");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("DxfFilePath")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ExportRecordId")
+ .HasColumnType("int");
+
+ b.Property("ItemNo")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("KFactor")
+ .HasColumnType("float");
+
+ b.Property("Material")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("PartName")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("PartNo")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Qty")
+ .HasColumnType("int");
+
+ b.Property("SortOrder")
+ .HasColumnType("int");
+
+ b.Property("Thickness")
+ .HasColumnType("float");
+
+ b.Property("TotalQty")
+ .HasColumnType("int");
+
+ b.HasKey("ID");
+
+ b.HasIndex("ExportRecordId");
+
+ b.ToTable("BomItems");
+ });
+
+ modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("DrawingNumber")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ExportedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ExportedBy")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("OutputFolder")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("PdfContentHash")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("SourceFilePath")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.HasKey("Id");
+
+ b.ToTable("ExportRecords");
+ });
+
+ modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
+ {
+ b.HasOne("ExportDXF.Models.ExportRecord", "ExportRecord")
+ .WithMany("BomItems")
+ .HasForeignKey("ExportRecordId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ExportRecord");
+ });
+
+ modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
+ {
+ b.Navigation("BomItems");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/ExportDXF/Migrations/20260214044511_InitialCreate.cs b/ExportDXF/Migrations/20260214044511_InitialCreate.cs
new file mode 100644
index 0000000..e88cc78
--- /dev/null
+++ b/ExportDXF/Migrations/20260214044511_InitialCreate.cs
@@ -0,0 +1,82 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ExportDXF.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "ExportRecords",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ DrawingNumber = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true),
+ SourceFilePath = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true),
+ OutputFolder = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true),
+ ExportedAt = table.Column(type: "datetime2", nullable: false),
+ ExportedBy = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true),
+ PdfContentHash = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ExportRecords", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "BomItems",
+ columns: table => new
+ {
+ ID = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ ItemNo = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true),
+ PartNo = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true),
+ SortOrder = table.Column(type: "int", nullable: false),
+ Qty = table.Column(type: "int", nullable: true),
+ TotalQty = table.Column(type: "int", nullable: true),
+ Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true),
+ PartName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true),
+ ConfigurationName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true),
+ Material = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true),
+ CutTemplateName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true),
+ DxfFilePath = table.Column(type: "nvarchar(max)", nullable: true),
+ ContentHash = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true),
+ Thickness = table.Column(type: "float", nullable: true),
+ KFactor = table.Column(type: "float", nullable: true),
+ DefaultBendRadius = table.Column(type: "float", nullable: true),
+ ExportRecordId = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BomItems", x => x.ID);
+ table.ForeignKey(
+ name: "FK_BomItems_ExportRecords_ExportRecordId",
+ column: x => x.ExportRecordId,
+ principalTable: "ExportRecords",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BomItems_ExportRecordId",
+ table: "BomItems",
+ column: "ExportRecordId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "BomItems");
+
+ migrationBuilder.DropTable(
+ name: "ExportRecords");
+ }
+ }
+}
diff --git a/ExportDXF/Models/ExportContext.cs b/ExportDXF/Models/ExportContext.cs
index 837db04..f04ee65 100644
--- a/ExportDXF/Models/ExportContext.cs
+++ b/ExportDXF/Models/ExportContext.cs
@@ -32,6 +32,16 @@ namespace ExportDXF.Services
///
public string FilePrefix { get; set; }
+ ///
+ /// Equipment number from the UI (e.g., "5028").
+ ///
+ public string Equipment { get; set; }
+
+ ///
+ /// Drawing number from the UI (e.g., "A02", "Misc").
+ ///
+ public string DrawingNo { get; set; }
+
///
/// Selected Equipment ID for API operations (optional).
///
diff --git a/ExportDXF/Models/ExportRecord.cs b/ExportDXF/Models/ExportRecord.cs
index 77c201c..8ada5f1 100644
--- a/ExportDXF/Models/ExportRecord.cs
+++ b/ExportDXF/Models/ExportRecord.cs
@@ -11,6 +11,7 @@ namespace ExportDXF.Models
public string OutputFolder { get; set; }
public DateTime ExportedAt { get; set; }
public string ExportedBy { get; set; }
+ public string PdfContentHash { get; set; }
public virtual ICollection BomItems { get; set; } = new List();
}
diff --git a/ExportDXF/Models/Item.cs b/ExportDXF/Models/Item.cs
index 6b92c68..656446f 100644
--- a/ExportDXF/Models/Item.cs
+++ b/ExportDXF/Models/Item.cs
@@ -61,5 +61,15 @@ namespace ExportDXF.Services
/// The SolidWorks component reference.
///
public Component2 Component { get; set; }
+
+ ///
+ /// SHA256 content hash of the exported DXF (transient, not persisted).
+ ///
+ public string ContentHash { get; set; }
+
+ ///
+ /// Path to the stashed (backed-up) previous DXF file (transient, not persisted).
+ ///
+ public string StashedFilePath { get; set; }
}
}
\ No newline at end of file
diff --git a/ExportDXF/Program.cs b/ExportDXF/Program.cs
index 4e68c80..e773f9d 100644
--- a/ExportDXF/Program.cs
+++ b/ExportDXF/Program.cs
@@ -39,9 +39,9 @@ namespace ExportDXF
{
var solidWorksService = new SolidWorksService();
var bomExtractor = new BomExtractor();
- var partExporter = new PartExporter();
- var drawingExporter = new DrawingExporter();
var fileExportService = new FileExportService(_outputFolder);
+ var partExporter = new PartExporter(fileExportService);
+ var drawingExporter = new DrawingExporter();
var exportService = new DxfExportService(
solidWorksService,
diff --git a/ExportDXF/Services/DxfExportService.cs b/ExportDXF/Services/DxfExportService.cs
index cd0e90d..791ead6 100644
--- a/ExportDXF/Services/DxfExportService.cs
+++ b/ExportDXF/Services/DxfExportService.cs
@@ -64,7 +64,7 @@ namespace ExportDXF.Services
var startTime = DateTime.Now;
var drawingNumber = ParseDrawingNumber(context);
- var outputFolder = _fileExportService.GetDrawingOutputFolder(drawingNumber);
+ var outputFolder = _fileExportService.GetDrawingOutputFolder(context.Equipment, context.DrawingNo);
try
{
@@ -73,11 +73,11 @@ namespace ExportDXF.Services
switch (context.ActiveDocument.DocumentType)
{
case DocumentType.Part:
- ExportPart(context, outputFolder);
+ ExportPart(context, outputFolder, drawingNumber);
break;
case DocumentType.Assembly:
- ExportAssembly(context, outputFolder);
+ ExportAssembly(context, outputFolder, drawingNumber);
break;
case DocumentType.Drawing:
@@ -101,7 +101,7 @@ namespace ExportDXF.Services
#region Export Methods by Document Type
- private void ExportPart(ExportContext context, string outputFolder)
+ private void ExportPart(ExportContext context, string outputFolder, string drawingNumber)
{
LogProgress(context, "Active document is a Part");
@@ -112,10 +112,52 @@ namespace ExportDXF.Services
return;
}
- _partExporter.ExportSinglePart(part, outputFolder, context);
+ var exportRecord = CreateExportRecord(context, drawingNumber, outputFolder);
+ var item = _partExporter.ExportSinglePart(part, outputFolder, context);
+
+ if (item != null)
+ {
+ // Assign auto-incremented item number
+ var nextItemNo = GetNextItemNumber(drawingNumber);
+ item.ItemNo = nextItemNo;
+
+ var bomItem = new BomItem
+ {
+ ExportRecordId = exportRecord?.Id ?? 0,
+ ItemNo = item.ItemNo,
+ PartNo = item.FileName ?? item.PartName ?? "",
+ SortOrder = 0,
+ Qty = item.Quantity,
+ TotalQty = item.Quantity,
+ Description = item.Description ?? "",
+ PartName = item.PartName ?? "",
+ ConfigurationName = item.Configuration ?? "",
+ Material = item.Material ?? ""
+ };
+
+ if (!string.IsNullOrEmpty(item.FileName))
+ {
+ var dxfPath = Path.Combine(outputFolder, item.FileName + ".dxf");
+ bomItem.CutTemplate = new CutTemplate
+ {
+ DxfFilePath = dxfPath,
+ ContentHash = item.ContentHash,
+ Thickness = item.Thickness > 0 ? item.Thickness : null,
+ KFactor = item.KFactor > 0 ? item.KFactor : null,
+ DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
+ };
+
+ HandleDxfVersioning(item, dxfPath, context);
+ }
+
+ context.BomItemCallback?.Invoke(bomItem);
+
+ if (exportRecord != null)
+ SaveBomItem(bomItem, context);
+ }
}
- private void ExportAssembly(ExportContext context, string outputFolder)
+ private void ExportAssembly(ExportContext context, string outputFolder, string drawingNumber)
{
LogProgress(context, "Active document is an Assembly");
LogProgress(context, "Fetching components...");
@@ -137,7 +179,20 @@ namespace ExportDXF.Services
LogProgress(context, $"Found {items.Count} item(s).");
- ExportItems(items, outputFolder, context);
+ var exportRecord = CreateExportRecord(context, drawingNumber, outputFolder);
+
+ // Auto-assign item numbers for items that don't have one
+ var nextNum = int.Parse(GetNextItemNumber(drawingNumber));
+ foreach (var item in items)
+ {
+ if (string.IsNullOrWhiteSpace(item.ItemNo))
+ {
+ item.ItemNo = nextNum.ToString();
+ nextNum++;
+ }
+ }
+
+ ExportItems(items, outputFolder, context, exportRecord?.Id);
}
private void ExportDrawing(ExportContext context, string drawingNumber, string drawingOutputFolder)
@@ -190,31 +245,24 @@ namespace ExportDXF.Services
}
// Create export record in database
- ExportRecord exportRecord = null;
- try
+ var exportRecord = CreateExportRecord(context, drawingNumber, drawingOutputFolder);
+
+ // Handle PDF versioning and update export record with hash
+ if (exportRecord != null && savedPdfPath != null)
{
- using (var db = _dbContextFactory())
+ try
{
- db.Database.Migrate();
- exportRecord = new ExportRecord
- {
- DrawingNumber = drawingNumber ?? context.ActiveDocument.Title,
- SourceFilePath = context.ActiveDocument.FilePath,
- OutputFolder = drawingOutputFolder,
- ExportedAt = DateTime.Now,
- ExportedBy = System.Environment.UserName
- };
+ HandlePdfVersioning(savedPdfPath, exportRecord.DrawingNumber, exportRecord, context);
- // Handle PDF versioning - compute hash and compare with previous
- if (savedPdfPath != null)
+ // Archive or discard old PDF based on hash comparison
+ if (pdfStashPath != null)
{
- HandlePdfVersioning(savedPdfPath, exportRecord.DrawingNumber, exportRecord, context);
-
- // Archive or discard old PDF based on hash comparison
- if (pdfStashPath != null)
+ using (var db = _dbContextFactory())
{
var previousRecord = db.ExportRecords
- .Where(r => r.DrawingNumber == exportRecord.DrawingNumber && r.PdfContentHash != null)
+ .Where(r => r.DrawingNumber == exportRecord.DrawingNumber
+ && r.PdfContentHash != null
+ && r.Id != exportRecord.Id)
.OrderByDescending(r => r.Id)
.FirstOrDefault();
@@ -229,16 +277,24 @@ namespace ExportDXF.Services
}
}
- db.ExportRecords.Add(exportRecord);
- db.SaveChanges();
- LogProgress(context, $"Created export record (ID: {exportRecord.Id})", LogLevel.Info);
+ // Update the record with the PDF hash
+ using (var db = _dbContextFactory())
+ {
+ db.ExportRecords.Attach(exportRecord);
+ db.Entry(exportRecord).Property(r => r.PdfContentHash).IsModified = true;
+ db.SaveChanges();
+ }
+ }
+ catch (Exception ex)
+ {
+ _fileExportService.DiscardStash(pdfStashPath);
+ LogProgress(context, $"PDF versioning error: {ex.Message}", LogLevel.Error);
}
}
- catch (Exception ex)
+ else if (pdfStashPath != null)
{
- // Clean up stash on error
+ // No export record - discard stash
_fileExportService.DiscardStash(pdfStashPath);
- LogProgress(context, $"Database error creating export record: {ex.Message}", LogLevel.Error);
}
// Export parts to DXF and save BOM items
@@ -478,6 +534,86 @@ namespace ExportDXF.Services
#endregion
+ #region Database Helpers
+
+ private ExportRecord CreateExportRecord(ExportContext context, string drawingNumber, string outputFolder)
+ {
+ try
+ {
+ using (var db = _dbContextFactory())
+ {
+ db.Database.Migrate();
+ var record = new ExportRecord
+ {
+ DrawingNumber = drawingNumber ?? context.ActiveDocument.Title,
+ SourceFilePath = context.ActiveDocument.FilePath,
+ OutputFolder = outputFolder,
+ ExportedAt = DateTime.Now,
+ ExportedBy = System.Environment.UserName
+ };
+
+ db.ExportRecords.Add(record);
+ db.SaveChanges();
+ LogProgress(context, $"Created export record (ID: {record.Id})", LogLevel.Info);
+ return record;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogProgress(context, $"Database error creating export record: {ex.Message}", LogLevel.Error);
+ return null;
+ }
+ }
+
+ private string GetNextItemNumber(string drawingNumber)
+ {
+ if (string.IsNullOrEmpty(drawingNumber))
+ return "1";
+
+ try
+ {
+ using (var db = _dbContextFactory())
+ {
+ var existingItems = db.ExportRecords
+ .Where(r => r.DrawingNumber == drawingNumber)
+ .SelectMany(r => r.BomItems)
+ .Select(b => b.ItemNo)
+ .ToList();
+
+ int maxNum = 0;
+ foreach (var itemNo in existingItems)
+ {
+ if (int.TryParse(itemNo, out var num) && num > maxNum)
+ maxNum = num;
+ }
+
+ return (maxNum + 1).ToString();
+ }
+ }
+ catch
+ {
+ return "1";
+ }
+ }
+
+ private void SaveBomItem(BomItem bomItem, ExportContext context)
+ {
+ try
+ {
+ using (var db = _dbContextFactory())
+ {
+ db.BomItems.Add(bomItem);
+ db.SaveChanges();
+ }
+ }
+ catch (Exception dbEx)
+ {
+ LogProgress(context, $"Database error saving BOM item: {dbEx.Message}", LogLevel.Error);
+ }
+ }
+
+ #endregion
+
#region Helper Methods
private string CreateTempWorkDir()
@@ -489,7 +625,11 @@ namespace ExportDXF.Services
private string ParseDrawingNumber(ExportContext context)
{
- // Prefer prefix (e.g., "5007 A02 PT"), fallback to active document title
+ // Use explicit Equipment/DrawingNo from the UI when available
+ if (!string.IsNullOrWhiteSpace(context?.Equipment) && !string.IsNullOrWhiteSpace(context?.DrawingNo))
+ return $"{context.Equipment} {context.DrawingNo}";
+
+ // Fallback: parse from prefix or document title
var candidate = context?.FilePrefix;
var info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate);
if (info == null)
diff --git a/ExportDXF/Services/FileExportService.cs b/ExportDXF/Services/FileExportService.cs
index 9e42f6d..80482ee 100644
--- a/ExportDXF/Services/FileExportService.cs
+++ b/ExportDXF/Services/FileExportService.cs
@@ -6,9 +6,13 @@ namespace ExportDXF.Services
public interface IFileExportService
{
string OutputFolder { get; }
+ string GetDrawingOutputFolder(string equipment, string drawingNo);
string SaveDxfFile(string sourcePath, string drawingNumber, string itemNo);
- string SavePdfFile(string sourcePath, string drawingNumber);
+ string SavePdfFile(string sourcePath, string drawingNumber, string outputFolder = null);
void EnsureOutputFolderExists();
+ string StashFile(string filePath);
+ void ArchiveFile(string stashPath, string originalPath);
+ void DiscardStash(string stashPath);
}
public class FileExportService : IFileExportService
@@ -29,6 +33,18 @@ namespace ExportDXF.Services
}
}
+ public string GetDrawingOutputFolder(string equipment, string drawingNo)
+ {
+ if (string.IsNullOrEmpty(equipment) || string.IsNullOrEmpty(drawingNo))
+ return OutputFolder;
+
+ var folder = Path.Combine(OutputFolder, equipment, drawingNo);
+ if (!Directory.Exists(folder))
+ Directory.CreateDirectory(folder);
+
+ return folder;
+ }
+
public string SaveDxfFile(string sourcePath, string drawingNumber, string itemNo)
{
if (string.IsNullOrEmpty(sourcePath))
@@ -49,16 +65,17 @@ namespace ExportDXF.Services
return destPath;
}
- public string SavePdfFile(string sourcePath, string drawingNumber)
+ public string SavePdfFile(string sourcePath, string drawingNumber, string outputFolder = null)
{
if (string.IsNullOrEmpty(sourcePath))
throw new ArgumentNullException(nameof(sourcePath));
+ var folder = outputFolder ?? OutputFolder;
var fileName = !string.IsNullOrEmpty(drawingNumber)
? $"{drawingNumber}.pdf"
: Path.GetFileName(sourcePath);
- var destPath = Path.Combine(OutputFolder, fileName);
+ var destPath = Path.Combine(folder, fileName);
// If source and dest are the same, skip copy
if (!string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase))
@@ -68,5 +85,40 @@ namespace ExportDXF.Services
return destPath;
}
+
+ public string StashFile(string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
+ return null;
+
+ var stashPath = filePath + ".bak";
+ File.Move(filePath, stashPath, overwrite: true);
+ return stashPath;
+ }
+
+ public void ArchiveFile(string stashPath, string originalPath)
+ {
+ if (string.IsNullOrEmpty(stashPath) || !File.Exists(stashPath))
+ return;
+
+ var fileDir = Path.GetDirectoryName(originalPath) ?? OutputFolder;
+ var archiveDir = Path.Combine(fileDir, "_archive");
+ if (!Directory.Exists(archiveDir))
+ Directory.CreateDirectory(archiveDir);
+
+ var originalName = Path.GetFileNameWithoutExtension(originalPath);
+ var ext = Path.GetExtension(originalPath);
+ var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
+ var archiveName = $"{originalName} [{timestamp}]{ext}";
+ var archivePath = Path.Combine(archiveDir, archiveName);
+
+ File.Move(stashPath, archivePath, overwrite: true);
+ }
+
+ public void DiscardStash(string stashPath)
+ {
+ if (!string.IsNullOrEmpty(stashPath) && File.Exists(stashPath))
+ File.Delete(stashPath);
+ }
}
}
diff --git a/ExportDXF/Services/PartExporter.cs b/ExportDXF/Services/PartExporter.cs
index 3683599..9fecf6e 100644
--- a/ExportDXF/Services/PartExporter.cs
+++ b/ExportDXF/Services/PartExporter.cs
@@ -15,11 +15,12 @@ namespace ExportDXF.Services
{
///
/// Exports a single part document to DXF.
+ /// Returns an Item with export metadata (filename, hash, sheet metal properties), or null if export failed.
///
/// The part document to export.
/// The directory where the DXF file will be saved.
/// The export context.
- void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
+ Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
///
/// Exports an item (component from BOM or assembly) to DXF.
@@ -39,7 +40,7 @@ namespace ExportDXF.Services
_fileExportService = fileExportService ?? throw new ArgumentNullException(nameof(fileExportService));
}
- public void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context)
+ public Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context)
{
if (part == null)
throw new ArgumentNullException(nameof(part));
@@ -59,9 +60,57 @@ namespace ExportDXF.Services
var fileName = GetSinglePartFileName(model, context.FilePrefix);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
+ // Build result item with metadata
+ var item = new Item
+ {
+ PartName = model.GetTitle()?.Replace(".SLDPRT", "") ?? "",
+ Configuration = originalConfigName ?? "",
+ Quantity = 1
+ };
+
+ // Enrich with sheet metal properties and description
+ 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 configPropMgr = model.Extension.CustomPropertyManager[originalConfigName];
+ item.Description = configPropMgr?.Get("Description");
+ if (string.IsNullOrEmpty(item.Description))
+ {
+ var docPropMgr = model.Extension.CustomPropertyManager[""];
+ item.Description = docPropMgr?.Get("Description");
+ }
+ item.Description = TextHelper.RemoveXmlTags(item.Description);
+
+ // Get material
+ item.Material = part.GetMaterialPropertyName2(originalConfigName, out _);
+
+ // Stash existing file before overwriting
+ item.StashedFilePath = _fileExportService.StashFile(savePath);
+
context.GetOrCreateTemplateDrawing();
- ExportPartToDxf(part, originalConfigName, savePath, context);
+ if (ExportPartToDxf(part, originalConfigName, savePath, context))
+ {
+ item.FileName = Path.GetFileNameWithoutExtension(savePath);
+ item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath);
+ return item;
+ }
+ else
+ {
+ // Export failed - restore stashed file
+ if (item.StashedFilePath != null && File.Exists(item.StashedFilePath))
+ {
+ File.Move(item.StashedFilePath, savePath, overwrite: true);
+ item.StashedFilePath = null;
+ }
+ return null;
+ }
}
finally
{
@@ -290,18 +339,15 @@ namespace ExportDXF.Services
var config = model.ConfigurationManager.ActiveConfiguration.Name;
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
- var name = isDefaultConfig ? title : $"{title} [{config}]";
-
- return PrependPrefix(name, prefix);
+ return isDefaultConfig ? title : $"{title} [{config}]";
}
private string GetItemFileName(Item item, string prefix)
{
- prefix = prefix?.Replace("\"", "''") ?? string.Empty;
-
if (string.IsNullOrWhiteSpace(item.ItemNo))
- return PrependPrefix(item.PartName, prefix);
+ return item.PartName;
+ prefix = prefix?.Replace("\"", "''") ?? string.Empty;
var num = item.ItemNo.PadLeft(2, '0');
// Expected format: {DrawingNo} PT{ItemNo}
return string.IsNullOrWhiteSpace(prefix)
@@ -309,17 +355,6 @@ namespace ExportDXF.Services
: $"{prefix} PT{num}";
}
- private string PrependPrefix(string name, string prefix)
- {
- if (string.IsNullOrEmpty(prefix))
- return name;
-
- if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
- return name;
-
- return prefix + name;
- }
-
private void LogExportFailure(Item item, ExportContext context)
{
var desc = item.Description?.ToLower() ?? string.Empty;
diff --git a/ExportDXF/Utilities/ContentHasher.cs b/ExportDXF/Utilities/ContentHasher.cs
new file mode 100644
index 0000000..0be2612
--- /dev/null
+++ b/ExportDXF/Utilities/ContentHasher.cs
@@ -0,0 +1,118 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace ExportDXF.Utilities
+{
+ public static class ContentHasher
+ {
+ ///
+ /// Computes a SHA256 hash of DXF file content, skipping the HEADER section
+ /// which contains timestamps that change on every save.
+ ///
+ public static string ComputeDxfContentHash(string filePath)
+ {
+ var text = File.ReadAllText(filePath);
+ var contentStart = FindEndOfHeader(text);
+ var content = contentStart >= 0 ? text.Substring(contentStart) : text;
+
+ using (var sha = SHA256.Create())
+ {
+ var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content));
+ return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
+ }
+ }
+
+ ///
+ /// Computes a SHA256 hash of the entire file contents (for PDFs and other binary files).
+ ///
+ public static string ComputeFileHash(string filePath)
+ {
+ using (var sha = SHA256.Create())
+ using (var stream = File.OpenRead(filePath))
+ {
+ var bytes = sha.ComputeHash(stream);
+ return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
+ }
+ }
+
+ ///
+ /// Finds the position immediately after the HEADER section's ENDSEC marker.
+ /// DXF HEADER format:
+ /// 0\nSECTION\n2\nHEADER\n...variables...\n0\nENDSEC\n
+ /// Returns -1 if no HEADER section is found.
+ ///
+ private static int FindEndOfHeader(string text)
+ {
+ // Find the HEADER section start
+ var headerIndex = FindGroupCode(text, 0, "2", "HEADER");
+ if (headerIndex < 0)
+ return -1;
+
+ // Advance past the HEADER value line so pair scanning stays aligned
+ var headerLineEnd = text.IndexOf('\n', headerIndex);
+ if (headerLineEnd < 0)
+ return -1;
+
+ // Find the ENDSEC that closes the HEADER section
+ var pos = headerLineEnd + 1;
+ while (pos < text.Length)
+ {
+ var endsecIndex = FindGroupCode(text, pos, "0", "ENDSEC");
+ if (endsecIndex < 0)
+ return -1;
+
+ // Move past the ENDSEC line
+ var lineEnd = text.IndexOf('\n', endsecIndex);
+ return lineEnd >= 0 ? lineEnd + 1 : text.Length;
+ }
+
+ return -1;
+ }
+
+ ///
+ /// Finds a DXF group code pair (code line followed by value line) starting from the given position.
+ /// Returns the position of the value line, or -1 if not found.
+ ///
+ private static int FindGroupCode(string text, int startIndex, string groupCode, string value)
+ {
+ var pos = startIndex;
+ while (pos < text.Length)
+ {
+ // Skip whitespace/newlines to find the group code
+ while (pos < text.Length && (text[pos] == '\r' || text[pos] == '\n' || text[pos] == ' '))
+ pos++;
+
+ if (pos >= text.Length)
+ break;
+
+ // Read the group code line
+ var codeLineEnd = text.IndexOf('\n', pos);
+ if (codeLineEnd < 0)
+ break;
+
+ var codeLine = text.Substring(pos, codeLineEnd - pos).Trim();
+
+ // Move to the value line
+ var valueStart = codeLineEnd + 1;
+ if (valueStart >= text.Length)
+ break;
+
+ var valueLineEnd = text.IndexOf('\n', valueStart);
+ if (valueLineEnd < 0)
+ valueLineEnd = text.Length;
+
+ var valueLine = text.Substring(valueStart, valueLineEnd - valueStart).Trim();
+
+ if (codeLine == groupCode && string.Equals(valueLine, value, StringComparison.OrdinalIgnoreCase))
+ return valueStart;
+
+ // Move to the next pair
+ pos = valueLineEnd + 1;
+ }
+
+ return -1;
+ }
+ }
+}