feat: add export history auto-fill, fix filename prefixes, persist records for all doc types
- Add database-first lookup for equipment/drawing number auto-fill when reopening previously exported files - Remove prefix prepending for named parts (only use prefix for PT## BOM items) - Create ExportRecord/BomItem/CutTemplate chains for Part and Assembly exports, not just Drawings - Add auto-incrementing item numbers across drawing numbers - Add content hashing (SHA256) for DXF and PDF versioning with stash/archive pattern - Add EF Core initial migration for ExportDxfDb Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
30
ExportDXF/Forms/MainForm.Designer.cs
generated
30
ExportDXF/Forms/MainForm.Designer.cs
generated
@@ -54,10 +54,10 @@ namespace ExportDXF.Forms
|
|||||||
// runButton
|
// runButton
|
||||||
//
|
//
|
||||||
runButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
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.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||||
runButton.Name = "runButton";
|
runButton.Name = "runButton";
|
||||||
runButton.Size = new System.Drawing.Size(100, 30);
|
runButton.Size = new System.Drawing.Size(100, 55);
|
||||||
runButton.TabIndex = 11;
|
runButton.TabIndex = 11;
|
||||||
runButton.Text = "Start";
|
runButton.Text = "Start";
|
||||||
runButton.UseVisualStyleBackColor = true;
|
runButton.UseVisualStyleBackColor = true;
|
||||||
@@ -91,7 +91,7 @@ namespace ExportDXF.Forms
|
|||||||
mainTabControl.Name = "mainTabControl";
|
mainTabControl.Name = "mainTabControl";
|
||||||
mainTabControl.Padding = new System.Drawing.Point(20, 5);
|
mainTabControl.Padding = new System.Drawing.Point(20, 5);
|
||||||
mainTabControl.SelectedIndex = 0;
|
mainTabControl.SelectedIndex = 0;
|
||||||
mainTabControl.Size = new System.Drawing.Size(599, 330);
|
mainTabControl.Size = new System.Drawing.Size(910, 441);
|
||||||
mainTabControl.TabIndex = 12;
|
mainTabControl.TabIndex = 12;
|
||||||
//
|
//
|
||||||
// logEventsTab
|
// logEventsTab
|
||||||
@@ -100,7 +100,7 @@ namespace ExportDXF.Forms
|
|||||||
logEventsTab.Location = new System.Drawing.Point(4, 30);
|
logEventsTab.Location = new System.Drawing.Point(4, 30);
|
||||||
logEventsTab.Name = "logEventsTab";
|
logEventsTab.Name = "logEventsTab";
|
||||||
logEventsTab.Padding = new System.Windows.Forms.Padding(3);
|
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.TabIndex = 0;
|
||||||
logEventsTab.Text = "Log Events";
|
logEventsTab.Text = "Log Events";
|
||||||
logEventsTab.UseVisualStyleBackColor = true;
|
logEventsTab.UseVisualStyleBackColor = true;
|
||||||
@@ -112,7 +112,7 @@ namespace ExportDXF.Forms
|
|||||||
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||||
logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
|
logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||||
logEventsDataGrid.Name = "logEventsDataGrid";
|
logEventsDataGrid.Name = "logEventsDataGrid";
|
||||||
logEventsDataGrid.Size = new System.Drawing.Size(579, 282);
|
logEventsDataGrid.Size = new System.Drawing.Size(890, 391);
|
||||||
logEventsDataGrid.TabIndex = 0;
|
logEventsDataGrid.TabIndex = 0;
|
||||||
//
|
//
|
||||||
// bomTab
|
// bomTab
|
||||||
@@ -121,7 +121,7 @@ namespace ExportDXF.Forms
|
|||||||
bomTab.Location = new System.Drawing.Point(4, 30);
|
bomTab.Location = new System.Drawing.Point(4, 30);
|
||||||
bomTab.Name = "bomTab";
|
bomTab.Name = "bomTab";
|
||||||
bomTab.Padding = new System.Windows.Forms.Padding(3);
|
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.TabIndex = 1;
|
||||||
bomTab.Text = "Bill Of Materials";
|
bomTab.Text = "Bill Of Materials";
|
||||||
bomTab.UseVisualStyleBackColor = true;
|
bomTab.UseVisualStyleBackColor = true;
|
||||||
@@ -133,30 +133,30 @@ namespace ExportDXF.Forms
|
|||||||
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||||
bomDataGrid.Location = new System.Drawing.Point(6, 6);
|
bomDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||||
bomDataGrid.Name = "bomDataGrid";
|
bomDataGrid.Name = "bomDataGrid";
|
||||||
bomDataGrid.Size = new System.Drawing.Size(970, 535);
|
bomDataGrid.Size = new System.Drawing.Size(1281, 644);
|
||||||
bomDataGrid.TabIndex = 1;
|
bomDataGrid.TabIndex = 1;
|
||||||
//
|
//
|
||||||
// cutTemplatesTab
|
// cutTemplatesTab
|
||||||
//
|
//
|
||||||
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
|
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
|
||||||
cutTemplatesTab.Location = new System.Drawing.Point(4, 30);
|
cutTemplatesTab.Location = new System.Drawing.Point(4, 30);
|
||||||
cutTemplatesTab.Name = "cutTemplatesTab";
|
cutTemplatesTab.Name = "cutTemplatesTab";
|
||||||
cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3);
|
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.TabIndex = 2;
|
||||||
cutTemplatesTab.Text = "Cut Templates";
|
cutTemplatesTab.Text = "Cut Templates";
|
||||||
cutTemplatesTab.UseVisualStyleBackColor = true;
|
cutTemplatesTab.UseVisualStyleBackColor = true;
|
||||||
//
|
//
|
||||||
// cutTemplatesDataGrid
|
// cutTemplatesDataGrid
|
||||||
//
|
//
|
||||||
cutTemplatesDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
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.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||||
cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||||
cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6);
|
cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||||
cutTemplatesDataGrid.Name = "cutTemplatesDataGrid";
|
cutTemplatesDataGrid.Name = "cutTemplatesDataGrid";
|
||||||
cutTemplatesDataGrid.Size = new System.Drawing.Size(970, 535);
|
cutTemplatesDataGrid.Size = new System.Drawing.Size(1281, 644);
|
||||||
cutTemplatesDataGrid.TabIndex = 2;
|
cutTemplatesDataGrid.TabIndex = 2;
|
||||||
//
|
//
|
||||||
// equipmentBox
|
// equipmentBox
|
||||||
//
|
//
|
||||||
equipmentBox.FormattingEnabled = true;
|
equipmentBox.FormattingEnabled = true;
|
||||||
@@ -194,7 +194,7 @@ namespace ExportDXF.Forms
|
|||||||
// MainForm
|
// MainForm
|
||||||
//
|
//
|
||||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
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(drawingNoBox);
|
||||||
Controls.Add(equipmentBox);
|
Controls.Add(equipmentBox);
|
||||||
Controls.Add(mainTabControl);
|
Controls.Add(mainTabControl);
|
||||||
|
|||||||
@@ -390,6 +390,8 @@ namespace ExportDXF.Forms
|
|||||||
ActiveDocument = activeDoc,
|
ActiveDocument = activeDoc,
|
||||||
ViewFlipDecider = viewFlipDecider,
|
ViewFlipDecider = viewFlipDecider,
|
||||||
FilePrefix = filePrefix,
|
FilePrefix = filePrefix,
|
||||||
|
Equipment = equipment,
|
||||||
|
DrawingNo = drawingNo,
|
||||||
EquipmentId = null,
|
EquipmentId = null,
|
||||||
CancellationToken = token,
|
CancellationToken = token,
|
||||||
ProgressCallback = (msg, level, file) => LogMessage(msg, level, file),
|
ProgressCallback = (msg, level, file) => LogMessage(msg, level, file),
|
||||||
@@ -468,21 +470,58 @@ namespace ExportDXF.Forms
|
|||||||
var docTitle = activeDoc?.Title ?? "No Document Open";
|
var docTitle = activeDoc?.Title ?? "No Document Open";
|
||||||
this.Text = $"ExportDXF - {docTitle}";
|
this.Text = $"ExportDXF - {docTitle}";
|
||||||
|
|
||||||
// Parse the file name and fill Equipment/Drawing dropdowns
|
if (activeDoc == null)
|
||||||
if (activeDoc != null)
|
return;
|
||||||
{
|
|
||||||
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 (!drawingNoBox.Items.Contains(drawingInfo.DrawingNo))
|
// Try database first: look up the most recent export for this file path
|
||||||
drawingNoBox.Items.Add(drawingInfo.DrawingNo);
|
DrawingInfo drawingInfo = null;
|
||||||
drawingNoBox.Text = drawingInfo.DrawingNo;
|
|
||||||
|
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)
|
private void LogMessage(string message, LogLevel level = LogLevel.Info, string file = null)
|
||||||
|
|||||||
153
ExportDXF/Migrations/20260214044511_InitialCreate.Designer.cs
generated
Normal file
153
ExportDXF/Migrations/20260214044511_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
|
||||||
|
|
||||||
|
b.Property<string>("ConfigurationName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("ContentHash")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("CutTemplateName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<double?>("DefaultBendRadius")
|
||||||
|
.HasColumnType("float");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("DxfFilePath")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("ExportRecordId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ItemNo")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<double?>("KFactor")
|
||||||
|
.HasColumnType("float");
|
||||||
|
|
||||||
|
b.Property<string>("Material")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("PartName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("PartNo")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int?>("Qty")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<double?>("Thickness")
|
||||||
|
.HasColumnType("float");
|
||||||
|
|
||||||
|
b.Property<int?>("TotalQty")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("ExportRecordId");
|
||||||
|
|
||||||
|
b.ToTable("BomItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DrawingNumber")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExportedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("ExportedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("OutputFolder")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("PdfContentHash")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
ExportDXF/Migrations/20260214044511_InitialCreate.cs
Normal file
82
ExportDXF/Migrations/20260214044511_InitialCreate.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ExportDXF.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ExportRecords",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
DrawingNumber = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
SourceFilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
OutputFolder = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
ExportedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
ExportedBy = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
PdfContentHash = table.Column<string>(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<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
ItemNo = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||||
|
PartNo = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
SortOrder = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Qty = table.Column<int>(type: "int", nullable: true),
|
||||||
|
TotalQty = table.Column<int>(type: "int", nullable: true),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
PartName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||||
|
ConfigurationName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
Material = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
CutTemplateName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
DxfFilePath = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
ContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||||
|
Thickness = table.Column<double>(type: "float", nullable: true),
|
||||||
|
KFactor = table.Column<double>(type: "float", nullable: true),
|
||||||
|
DefaultBendRadius = table.Column<double>(type: "float", nullable: true),
|
||||||
|
ExportRecordId = table.Column<int>(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "BomItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ExportRecords");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,16 @@ namespace ExportDXF.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string FilePrefix { get; set; }
|
public string FilePrefix { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Equipment number from the UI (e.g., "5028").
|
||||||
|
/// </summary>
|
||||||
|
public string Equipment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drawing number from the UI (e.g., "A02", "Misc").
|
||||||
|
/// </summary>
|
||||||
|
public string DrawingNo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Selected Equipment ID for API operations (optional).
|
/// Selected Equipment ID for API operations (optional).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace ExportDXF.Models
|
|||||||
public string OutputFolder { get; set; }
|
public string OutputFolder { get; set; }
|
||||||
public DateTime ExportedAt { get; set; }
|
public DateTime ExportedAt { get; set; }
|
||||||
public string ExportedBy { get; set; }
|
public string ExportedBy { get; set; }
|
||||||
|
public string PdfContentHash { get; set; }
|
||||||
|
|
||||||
public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
|
public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,5 +61,15 @@ namespace ExportDXF.Services
|
|||||||
/// The SolidWorks component reference.
|
/// The SolidWorks component reference.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Component2 Component { get; set; }
|
public Component2 Component { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA256 content hash of the exported DXF (transient, not persisted).
|
||||||
|
/// </summary>
|
||||||
|
public string ContentHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path to the stashed (backed-up) previous DXF file (transient, not persisted).
|
||||||
|
/// </summary>
|
||||||
|
public string StashedFilePath { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,9 +39,9 @@ namespace ExportDXF
|
|||||||
{
|
{
|
||||||
var solidWorksService = new SolidWorksService();
|
var solidWorksService = new SolidWorksService();
|
||||||
var bomExtractor = new BomExtractor();
|
var bomExtractor = new BomExtractor();
|
||||||
var partExporter = new PartExporter();
|
|
||||||
var drawingExporter = new DrawingExporter();
|
|
||||||
var fileExportService = new FileExportService(_outputFolder);
|
var fileExportService = new FileExportService(_outputFolder);
|
||||||
|
var partExporter = new PartExporter(fileExportService);
|
||||||
|
var drawingExporter = new DrawingExporter();
|
||||||
|
|
||||||
var exportService = new DxfExportService(
|
var exportService = new DxfExportService(
|
||||||
solidWorksService,
|
solidWorksService,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ namespace ExportDXF.Services
|
|||||||
var startTime = DateTime.Now;
|
var startTime = DateTime.Now;
|
||||||
|
|
||||||
var drawingNumber = ParseDrawingNumber(context);
|
var drawingNumber = ParseDrawingNumber(context);
|
||||||
var outputFolder = _fileExportService.GetDrawingOutputFolder(drawingNumber);
|
var outputFolder = _fileExportService.GetDrawingOutputFolder(context.Equipment, context.DrawingNo);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -73,11 +73,11 @@ namespace ExportDXF.Services
|
|||||||
switch (context.ActiveDocument.DocumentType)
|
switch (context.ActiveDocument.DocumentType)
|
||||||
{
|
{
|
||||||
case DocumentType.Part:
|
case DocumentType.Part:
|
||||||
ExportPart(context, outputFolder);
|
ExportPart(context, outputFolder, drawingNumber);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DocumentType.Assembly:
|
case DocumentType.Assembly:
|
||||||
ExportAssembly(context, outputFolder);
|
ExportAssembly(context, outputFolder, drawingNumber);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DocumentType.Drawing:
|
case DocumentType.Drawing:
|
||||||
@@ -101,7 +101,7 @@ namespace ExportDXF.Services
|
|||||||
|
|
||||||
#region Export Methods by Document Type
|
#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");
|
LogProgress(context, "Active document is a Part");
|
||||||
|
|
||||||
@@ -112,10 +112,52 @@ namespace ExportDXF.Services
|
|||||||
return;
|
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, "Active document is an Assembly");
|
||||||
LogProgress(context, "Fetching components...");
|
LogProgress(context, "Fetching components...");
|
||||||
@@ -137,7 +179,20 @@ namespace ExportDXF.Services
|
|||||||
|
|
||||||
LogProgress(context, $"Found {items.Count} item(s).");
|
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)
|
private void ExportDrawing(ExportContext context, string drawingNumber, string drawingOutputFolder)
|
||||||
@@ -190,31 +245,24 @@ namespace ExportDXF.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create export record in database
|
// Create export record in database
|
||||||
ExportRecord exportRecord = null;
|
var exportRecord = CreateExportRecord(context, drawingNumber, drawingOutputFolder);
|
||||||
try
|
|
||||||
|
// Handle PDF versioning and update export record with hash
|
||||||
|
if (exportRecord != null && savedPdfPath != null)
|
||||||
{
|
{
|
||||||
using (var db = _dbContextFactory())
|
try
|
||||||
{
|
{
|
||||||
db.Database.Migrate();
|
HandlePdfVersioning(savedPdfPath, exportRecord.DrawingNumber, exportRecord, context);
|
||||||
exportRecord = new ExportRecord
|
|
||||||
{
|
|
||||||
DrawingNumber = drawingNumber ?? context.ActiveDocument.Title,
|
|
||||||
SourceFilePath = context.ActiveDocument.FilePath,
|
|
||||||
OutputFolder = drawingOutputFolder,
|
|
||||||
ExportedAt = DateTime.Now,
|
|
||||||
ExportedBy = System.Environment.UserName
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle PDF versioning - compute hash and compare with previous
|
// Archive or discard old PDF based on hash comparison
|
||||||
if (savedPdfPath != null)
|
if (pdfStashPath != null)
|
||||||
{
|
{
|
||||||
HandlePdfVersioning(savedPdfPath, exportRecord.DrawingNumber, exportRecord, context);
|
using (var db = _dbContextFactory())
|
||||||
|
|
||||||
// Archive or discard old PDF based on hash comparison
|
|
||||||
if (pdfStashPath != null)
|
|
||||||
{
|
{
|
||||||
var previousRecord = db.ExportRecords
|
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)
|
.OrderByDescending(r => r.Id)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
@@ -229,16 +277,24 @@ namespace ExportDXF.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db.ExportRecords.Add(exportRecord);
|
// Update the record with the PDF hash
|
||||||
db.SaveChanges();
|
using (var db = _dbContextFactory())
|
||||||
LogProgress(context, $"Created export record (ID: {exportRecord.Id})", LogLevel.Info);
|
{
|
||||||
|
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);
|
_fileExportService.DiscardStash(pdfStashPath);
|
||||||
LogProgress(context, $"Database error creating export record: {ex.Message}", LogLevel.Error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export parts to DXF and save BOM items
|
// Export parts to DXF and save BOM items
|
||||||
@@ -478,6 +534,86 @@ namespace ExportDXF.Services
|
|||||||
|
|
||||||
#endregion
|
#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
|
#region Helper Methods
|
||||||
|
|
||||||
private string CreateTempWorkDir()
|
private string CreateTempWorkDir()
|
||||||
@@ -489,7 +625,11 @@ namespace ExportDXF.Services
|
|||||||
|
|
||||||
private string ParseDrawingNumber(ExportContext context)
|
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 candidate = context?.FilePrefix;
|
||||||
var info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate);
|
var info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate);
|
||||||
if (info == null)
|
if (info == null)
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ namespace ExportDXF.Services
|
|||||||
public interface IFileExportService
|
public interface IFileExportService
|
||||||
{
|
{
|
||||||
string OutputFolder { get; }
|
string OutputFolder { get; }
|
||||||
|
string GetDrawingOutputFolder(string equipment, string drawingNo);
|
||||||
string SaveDxfFile(string sourcePath, string drawingNumber, string itemNo);
|
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();
|
void EnsureOutputFolderExists();
|
||||||
|
string StashFile(string filePath);
|
||||||
|
void ArchiveFile(string stashPath, string originalPath);
|
||||||
|
void DiscardStash(string stashPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FileExportService : IFileExportService
|
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)
|
public string SaveDxfFile(string sourcePath, string drawingNumber, string itemNo)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(sourcePath))
|
if (string.IsNullOrEmpty(sourcePath))
|
||||||
@@ -49,16 +65,17 @@ namespace ExportDXF.Services
|
|||||||
return destPath;
|
return destPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string SavePdfFile(string sourcePath, string drawingNumber)
|
public string SavePdfFile(string sourcePath, string drawingNumber, string outputFolder = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(sourcePath))
|
if (string.IsNullOrEmpty(sourcePath))
|
||||||
throw new ArgumentNullException(nameof(sourcePath));
|
throw new ArgumentNullException(nameof(sourcePath));
|
||||||
|
|
||||||
|
var folder = outputFolder ?? OutputFolder;
|
||||||
var fileName = !string.IsNullOrEmpty(drawingNumber)
|
var fileName = !string.IsNullOrEmpty(drawingNumber)
|
||||||
? $"{drawingNumber}.pdf"
|
? $"{drawingNumber}.pdf"
|
||||||
: Path.GetFileName(sourcePath);
|
: 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 source and dest are the same, skip copy
|
||||||
if (!string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -68,5 +85,40 @@ namespace ExportDXF.Services
|
|||||||
|
|
||||||
return destPath;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ namespace ExportDXF.Services
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exports a single part document to DXF.
|
/// Exports a single part document to DXF.
|
||||||
|
/// Returns an Item with export metadata (filename, hash, sheet metal properties), or null if export failed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="part">The part document to export.</param>
|
/// <param name="part">The part document to export.</param>
|
||||||
/// <param name="saveDirectory">The directory where the DXF file will be saved.</param>
|
/// <param name="saveDirectory">The directory where the DXF file will be saved.</param>
|
||||||
/// <param name="context">The export context.</param>
|
/// <param name="context">The export context.</param>
|
||||||
void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
|
Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exports an item (component from BOM or assembly) to DXF.
|
/// Exports an item (component from BOM or assembly) to DXF.
|
||||||
@@ -39,7 +40,7 @@ namespace ExportDXF.Services
|
|||||||
_fileExportService = fileExportService ?? throw new ArgumentNullException(nameof(fileExportService));
|
_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)
|
if (part == null)
|
||||||
throw new ArgumentNullException(nameof(part));
|
throw new ArgumentNullException(nameof(part));
|
||||||
@@ -59,9 +60,57 @@ namespace ExportDXF.Services
|
|||||||
var fileName = GetSinglePartFileName(model, context.FilePrefix);
|
var fileName = GetSinglePartFileName(model, context.FilePrefix);
|
||||||
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
|
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();
|
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
|
finally
|
||||||
{
|
{
|
||||||
@@ -290,18 +339,15 @@ namespace ExportDXF.Services
|
|||||||
var config = model.ConfigurationManager.ActiveConfiguration.Name;
|
var config = model.ConfigurationManager.ActiveConfiguration.Name;
|
||||||
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
|
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var name = isDefaultConfig ? title : $"{title} [{config}]";
|
return isDefaultConfig ? title : $"{title} [{config}]";
|
||||||
|
|
||||||
return PrependPrefix(name, prefix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetItemFileName(Item item, string prefix)
|
private string GetItemFileName(Item item, string prefix)
|
||||||
{
|
{
|
||||||
prefix = prefix?.Replace("\"", "''") ?? string.Empty;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(item.ItemNo))
|
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');
|
var num = item.ItemNo.PadLeft(2, '0');
|
||||||
// Expected format: {DrawingNo} PT{ItemNo}
|
// Expected format: {DrawingNo} PT{ItemNo}
|
||||||
return string.IsNullOrWhiteSpace(prefix)
|
return string.IsNullOrWhiteSpace(prefix)
|
||||||
@@ -309,17 +355,6 @@ namespace ExportDXF.Services
|
|||||||
: $"{prefix} PT{num}";
|
: $"{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)
|
private void LogExportFailure(Item item, ExportContext context)
|
||||||
{
|
{
|
||||||
var desc = item.Description?.ToLower() ?? string.Empty;
|
var desc = item.Description?.ToLower() ?? string.Empty;
|
||||||
|
|||||||
118
ExportDXF/Utilities/ContentHasher.cs
Normal file
118
ExportDXF/Utilities/ContentHasher.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace ExportDXF.Utilities
|
||||||
|
{
|
||||||
|
public static class ContentHasher
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Computes a SHA256 hash of DXF file content, skipping the HEADER section
|
||||||
|
/// which contains timestamps that change on every save.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes a SHA256 hash of the entire file contents (for PDFs and other binary files).
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user