Compare commits

..

28 Commits

Author SHA1 Message Date
14fa1e6906 chore: add docs/superpowers to gitignore and remove from tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:34:11 -04:00
41022a93cc docs: update README for Excel export and template-based naming
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:33:46 -04:00
0fd117da92 merge: reconcile GitHub history before mirror setup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:31:21 -04:00
5f28a6ce2b chore: remove netDxf project from solution
netDxf was replaced by ACadSharp in the EtchBendLines submodule.
The netDxf directory was already removed from git tracking;
this removes the stale solution reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:26:16 -04:00
a4db71f074 fix: update EtchBendLines submodule to ACadSharp version
Advances submodule from netDxf (89d987f) to ACadSharp (da4d322),
picking up all fixes from the fabworks-api branch:
- Switch from netDxf to ACadSharp for DXF operations
- Fix ACAD_GROUP dictionary, upgrade ACadSharp to 3.4.9
- Fix bend line detection and ByLayer color
- Fix degree symbol encoding (reverted in submodule, handled by caller)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:23:18 -04:00
4d01b2654d refactor: rewire Program.cs DI — remove API/DB, add Excel and log services
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:18:31 -04:00
1d3b6b8f0f refactor: replace equipment/drawing dropdowns with filename template textbox
- Remove equipmentBox, drawingNoBox, titleBox and related controls
- Add txtFilenameTemplate text box with auto-fill via IDrawingInfoExtractor
- Validate template before export (must contain {item_no})
- Output to Templates/ folder next to source file
- Remove all API client references from MainForm

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:18:15 -04:00
9bc29e98c8 refactor: rewrite DxfExportService for local file export with revision tracking
- Replace API client with ExcelExportService and LogFileService
- Export DXFs to Templates folder with template-based naming
- Content hash comparison against existing xlsx for revision tracking
- Changed DXFs get revision suffix (e.g., PT03 Rev2.dxf)
- Unchanged DXFs are skipped
- Raw BOM table copied from SolidWorks drawing to Excel
- PDF exported directly to output folder
- Update PartExporter to remove FilePrefix dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:16:47 -04:00
c6dde6e217 feat: add ExcelExportService, LogFileService, and RawBomTableReader
- ExcelExportService: read/write BOM and Cut Templates xlsx with ClosedXML
- LogFileService: per-export and app-level log file writing
- RawBomTableReader: copy visible SolidWorks BOM table data for Excel output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:14:43 -04:00
cf17e71b80 refactor: update models — add CutTemplate.Revision, remove ExportRecord, simplify ExportContext
- CutTemplate: add Revision property, remove EF Core navigation properties
- BomItem: remove EF Core navigation properties and ExportRecordId
- ExportContext: replace Equipment/DrawingNo/Title/FilePrefix with
  FilenameTemplate and OutputFolder
- Delete ExportRecord.cs (replaced by log file)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:13:40 -04:00
742d86ab8a feat: add FilenameTemplateParser with placeholder evaluation and validation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:12:58 -04:00
ba782b99db feat: add IDrawingInfoExtractor with equipment and default implementations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:12:38 -04:00
9a33d405e2 refactor: remove API client, database, and EF Core; add ClosedXML
- Delete ApiClient/ directory (FabWorksApiClient, DTOs, interface)
- Delete Data/ and Migrations/ directories
- Add ClosedXML 0.104.2 for Excel output
- Replace FabWorksApiUrl with DefaultSuffix in app.config
- Remove .NET Framework startup config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:11:57 -04:00
e0d4563cc6 chore: remove FabWorks.Core, FabWorks.Api, and FabWorks.Tests after merge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:10:51 -04:00
a4f6dffe12 merge: bring fabworks-api structural improvements into master 2026-04-13 22:10:36 -04:00
b7d35bbe78 docs: add implementation plan for removing API and switching to Excel export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:09:35 -04:00
5e5c6ab72f docs: add design spec for removing API and switching to Excel export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:04:29 -04:00
036ab2a55a docs: add context to FixDegreeSymbol workaround
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:42:54 -05:00
f9e7ace35d fix: repair double-encoded degree symbol in DXF output
ACadSharp misreads UTF-8 degree symbol (C2 B0) as two ANSI_1252
characters (°) then writes that back out. Post-process the saved
DXF to replace ° with ° so bend notes display correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:31:57 -05:00
622cbf1170 fix: update EtchBendLines submodule with degree symbol fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:28:45 -05:00
4a3f33db33 fix: update EtchBendLines submodule with bend line ByLayer color
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:51:59 -05:00
77d0157370 fix: update EtchBendLines submodule with bend detection fixes
Fixes missing etch lines and incorrect bend layer assignment after
the ACadSharp migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:34:05 -05:00
26e9233b30 fix: update EtchBendLines submodule with ACadSharp 3.4.9 upgrade
Fixes DXF files failing to open with "GroupTable dictionary was not
defined in NamedObject dictionary" error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:20:57 -05:00
e59584a5c0 fix: update EtchBendLines submodule with ACAD_GROUP dictionary fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:13:12 -05:00
dcc508d479 feat: update EtchBendLines submodule with ACadSharp migration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 12:52:48 -05:00
1266378b51 fix: update EtchBendLines submodule with etch line fix
Updates submodule to include the yield break -> return lines fix
that was causing etch lines to be silently discarded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 12:37:29 -05:00
5de40ebafd feat: add delete button to exports list and detail pages
Add DELETE /api/exports/{id} endpoint with cascade delete, trash icon
buttons on both the exports list and export detail pages, and disable
browser caching for static files to prevent stale JS issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:34:59 -05:00
AJ
f418573908 Move project to github 2022-09-16 09:59:44 -04:00
70 changed files with 878 additions and 6190 deletions

3
.gitignore vendored
View File

@@ -245,3 +245,6 @@ ModelManifest.xml
# Test documents
TestDocs/
# Superpowers specs and plans
docs/superpowers/

View File

@@ -7,14 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportDXF", "ExportDXF\Expo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EtchBendLines", "EtchBendLines\EtchBendLines\EtchBendLines.csproj", "{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "netDxf", "EtchBendLines\netDxf\netDxf\netDxf.csproj", "{785380E0-CEB9-4C34-82E5-60D0E33E848E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Core", "FabWorks.Core\FabWorks.Core.csproj", "{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Tests", "FabWorks.Tests\FabWorks.Tests.csproj", "{6DD89774-D86B-47E9-B982-2794BD95616A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Api", "FabWorks.Api\FabWorks.Api.csproj", "{9BD571FA-52D8-430D-8843-FEB6EABD421C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -49,54 +41,6 @@ Global
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x64.Build.0 = Release|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.ActiveCfg = Release|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.Build.0 = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|x64.ActiveCfg = Debug|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|x64.Build.0 = Debug|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|x86.ActiveCfg = Debug|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|x86.Build.0 = Debug|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|Any CPU.Build.0 = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|x64.ActiveCfg = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|x64.Build.0 = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|x86.ActiveCfg = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|x86.Build.0 = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x64.ActiveCfg = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x64.Build.0 = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x86.ActiveCfg = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x86.Build.0 = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|Any CPU.Build.0 = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x64.ActiveCfg = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x64.Build.0 = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x86.ActiveCfg = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x86.Build.0 = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x64.ActiveCfg = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x64.Build.0 = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x86.ActiveCfg = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x86.Build.0 = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|Any CPU.Build.0 = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x64.ActiveCfg = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x64.Build.0 = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x86.ActiveCfg = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x86.Build.0 = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x64.ActiveCfg = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x64.Build.0 = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x86.ActiveCfg = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x86.Build.0 = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|Any CPU.Build.0 = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x64.ActiveCfg = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x64.Build.0 = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x86.ActiveCfg = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -1,157 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace ExportDXF.ApiClient
{
public class FabWorksApiClient : IFabWorksApiClient
{
private readonly HttpClient _http;
public FabWorksApiClient(HttpClient httpClient)
{
_http = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task<ApiExportDetail> CreateExportAsync(string drawingNumber, string equipmentNo, string drawingNo, string sourceFilePath, string outputFolder, string title = null)
{
var request = new ApiCreateExportRequest
{
DrawingNumber = drawingNumber,
Title = title,
EquipmentNo = equipmentNo,
DrawingNo = drawingNo,
SourceFilePath = sourceFilePath,
OutputFolder = outputFolder
};
var response = await _http.PostAsJsonAsync("api/exports", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiExportDetail>();
}
public async Task<ApiExportDetail> GetExportBySourceFileAsync(string filePath)
{
var response = await _http.GetAsync($"api/exports/by-source?path={Uri.EscapeDataString(filePath)}");
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiExportDetail>();
}
public async Task<List<string>> GetDrawingNumbersAsync()
{
var response = await _http.GetAsync("api/exports/drawing-numbers");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<string>>();
}
public async Task<List<string>> GetEquipmentNumbersAsync()
{
var response = await _http.GetAsync("api/exports/equipment-numbers");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<string>>();
}
public async Task<List<string>> GetDrawingNumbersByEquipmentAsync(string equipmentNo = null)
{
var url = "api/exports/drawing-numbers-by-equipment";
if (!string.IsNullOrEmpty(equipmentNo))
url += $"?equipmentNo={Uri.EscapeDataString(equipmentNo)}";
var response = await _http.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<string>>();
}
public async Task<string> GetNextItemNumberAsync(string drawingNumber)
{
var response = await _http.GetAsync($"api/exports/next-item-number?drawingNumber={Uri.EscapeDataString(drawingNumber)}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task UpdatePdfHashAsync(int exportId, string pdfContentHash)
{
var request = new ApiUpdatePdfHashRequest { PdfContentHash = pdfContentHash };
var response = await _http.PatchAsJsonAsync($"api/exports/{exportId}/pdf-hash", request);
response.EnsureSuccessStatusCode();
}
public async Task<string> GetPreviousPdfHashAsync(string drawingNumber, int? excludeId = null)
{
var url = $"api/exports/previous-pdf-hash?drawingNumber={Uri.EscapeDataString(drawingNumber)}";
if (excludeId.HasValue)
url += $"&excludeId={excludeId.Value}";
var response = await _http.GetAsync(url);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task<ApiBomItem> FindExistingBomItemAsync(int exportId, string partName, string configurationName)
{
var url = $"api/exports/{exportId}/bom-items/find?partName={Uri.EscapeDataString(partName ?? "")}&configurationName={Uri.EscapeDataString(configurationName ?? "")}";
var response = await _http.GetAsync(url);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiBomItem>();
}
public async Task<ApiBomItem> CreateBomItemAsync(int exportId, ApiBomItem bomItem)
{
var response = await _http.PostAsJsonAsync($"api/exports/{exportId}/bom-items", bomItem);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiBomItem>();
}
public async Task<ApiCutTemplate> GetPreviousCutTemplateAsync(string drawingNumber, string itemNo)
{
var response = await _http.GetAsync($"api/exports/previous-cut-template?drawingNumber={Uri.EscapeDataString(drawingNumber)}&itemNo={Uri.EscapeDataString(itemNo)}");
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiCutTemplate>();
}
public async Task<ApiFileUploadResponse> UploadDxfAsync(string localFilePath, string equipment, string drawingNo, string itemNo, string contentHash)
{
using var content = new MultipartFormDataContent();
using var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read);
var fileContent = new StreamContent(fileStream);
content.Add(fileContent, "file", Path.GetFileName(localFilePath));
content.Add(new StringContent(equipment ?? ""), "equipment");
content.Add(new StringContent(drawingNo ?? ""), "drawingNo");
content.Add(new StringContent(itemNo ?? ""), "itemNo");
content.Add(new StringContent(contentHash ?? ""), "contentHash");
var response = await _http.PostAsync("api/files/dxf", content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiFileUploadResponse>();
}
public async Task<ApiFileUploadResponse> UploadPdfAsync(string localFilePath, string equipment, string drawingNo, string contentHash, int? exportRecordId = null)
{
using var content = new MultipartFormDataContent();
using var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read);
var fileContent = new StreamContent(fileStream);
content.Add(fileContent, "file", Path.GetFileName(localFilePath));
content.Add(new StringContent(equipment ?? ""), "equipment");
content.Add(new StringContent(drawingNo ?? ""), "drawingNo");
content.Add(new StringContent(contentHash ?? ""), "contentHash");
if (exportRecordId.HasValue)
content.Add(new StringContent(exportRecordId.Value.ToString()), "exportRecordId");
var response = await _http.PostAsync("api/files/pdf", content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiFileUploadResponse>();
}
}
}

View File

@@ -1,86 +0,0 @@
using System;
using System.Collections.Generic;
namespace ExportDXF.ApiClient
{
public class ApiExportDetail
{
public int Id { get; set; }
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
public DateTime ExportedAt { get; set; }
public string ExportedBy { get; set; }
public string PdfContentHash { get; set; }
public List<ApiBomItem> BomItems { get; set; } = new();
}
public class ApiBomItem
{
public int ID { get; set; }
public string ItemNo { get; set; }
public string PartNo { get; set; }
public int SortOrder { get; set; }
public int? Qty { get; set; }
public int? TotalQty { get; set; }
public string Description { get; set; }
public string PartName { get; set; }
public string ConfigurationName { get; set; }
public string Material { get; set; }
public ApiCutTemplate CutTemplate { get; set; }
public ApiFormProgram FormProgram { get; set; }
}
public class ApiCutTemplate
{
public int Id { get; set; }
public string DxfFilePath { get; set; }
public string ContentHash { get; set; }
public int Revision { get; set; }
public double? Thickness { get; set; }
public double? KFactor { get; set; }
public double? DefaultBendRadius { get; set; }
}
public class ApiFormProgram
{
public int Id { get; set; }
public string ProgramFilePath { get; set; }
public string ContentHash { get; set; }
public string ProgramName { get; set; }
public double? Thickness { get; set; }
public string MaterialType { get; set; }
public double? KFactor { get; set; }
public int BendCount { get; set; }
public string UpperToolNames { get; set; }
public string LowerToolNames { get; set; }
public string SetupNotes { get; set; }
}
public class ApiCreateExportRequest
{
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
}
public class ApiUpdatePdfHashRequest
{
public string PdfContentHash { get; set; }
}
public class ApiFileUploadResponse
{
public string StoredFilePath { get; set; }
public string ContentHash { get; set; }
public string FileName { get; set; }
public bool WasUnchanged { get; set; }
public bool IsNewFile { get; set; }
}
}

View File

@@ -1,22 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ExportDXF.ApiClient
{
public interface IFabWorksApiClient
{
Task<ApiExportDetail> CreateExportAsync(string drawingNumber, string equipmentNo, string drawingNo, string sourceFilePath, string outputFolder, string title = null);
Task<ApiExportDetail> GetExportBySourceFileAsync(string filePath);
Task<List<string>> GetDrawingNumbersAsync();
Task<List<string>> GetEquipmentNumbersAsync();
Task<List<string>> GetDrawingNumbersByEquipmentAsync(string equipmentNo = null);
Task<string> GetNextItemNumberAsync(string drawingNumber);
Task UpdatePdfHashAsync(int exportId, string pdfContentHash);
Task<string> GetPreviousPdfHashAsync(string drawingNumber, int? excludeId = null);
Task<ApiBomItem> FindExistingBomItemAsync(int exportId, string partName, string configurationName);
Task<ApiBomItem> CreateBomItemAsync(int exportId, ApiBomItem bomItem);
Task<ApiCutTemplate> GetPreviousCutTemplateAsync(string drawingNumber, string itemNo);
Task<ApiFileUploadResponse> UploadDxfAsync(string localFilePath, string equipment, string drawingNo, string itemNo, string contentHash);
Task<ApiFileUploadResponse> UploadPdfAsync(string localFilePath, string equipment, string drawingNo, string contentHash, int? exportRecordId = null);
}
}

View File

@@ -14,6 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.104.2" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
</ItemGroup>

View File

@@ -38,12 +38,8 @@ namespace ExportDXF.Forms
bomDataGrid = new System.Windows.Forms.DataGridView();
cutTemplatesTab = new System.Windows.Forms.TabPage();
cutTemplatesDataGrid = new System.Windows.Forms.DataGridView();
equipmentBox = new System.Windows.Forms.ComboBox();
label1 = new System.Windows.Forms.Label();
label2 = new System.Windows.Forms.Label();
drawingNoBox = new System.Windows.Forms.ComboBox();
titleLabel = new System.Windows.Forms.Label();
titleBox = new System.Windows.Forms.TextBox();
templateLabel = new System.Windows.Forms.Label();
txtFilenameTemplate = new System.Windows.Forms.TextBox();
mainTabControl.SuspendLayout();
logEventsTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)logEventsDataGrid).BeginInit();
@@ -52,175 +48,137 @@ namespace ExportDXF.Forms
cutTemplatesTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)cutTemplatesDataGrid).BeginInit();
SuspendLayout();
//
//
// runButton
//
//
runButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
runButton.Location = new System.Drawing.Point(508, 12);
runButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
runButton.Name = "runButton";
runButton.Size = new System.Drawing.Size(65, 87);
runButton.Size = new System.Drawing.Size(65, 57);
runButton.TabIndex = 11;
runButton.Text = "Start";
runButton.UseVisualStyleBackColor = true;
runButton.Click += button1_Click;
//
//
// templateLabel
//
templateLabel.AutoSize = true;
templateLabel.Location = new System.Drawing.Point(15, 15);
templateLabel.Name = "templateLabel";
templateLabel.Size = new System.Drawing.Size(116, 17);
templateLabel.TabIndex = 2;
templateLabel.Text = "Filename Template";
//
// txtFilenameTemplate
//
txtFilenameTemplate.Location = new System.Drawing.Point(137, 12);
txtFilenameTemplate.Name = "txtFilenameTemplate";
txtFilenameTemplate.Size = new System.Drawing.Size(365, 25);
txtFilenameTemplate.TabIndex = 1;
//
// label3
//
//
label3.AutoSize = true;
label3.Location = new System.Drawing.Point(26, 77);
label3.Location = new System.Drawing.Point(26, 46);
label3.Name = "label3";
label3.Size = new System.Drawing.Size(105, 17);
label3.TabIndex = 2;
label3.Text = "View flip decider";
//
//
// viewFlipDeciderBox
//
//
viewFlipDeciderBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
viewFlipDeciderBox.FormattingEnabled = true;
viewFlipDeciderBox.Location = new System.Drawing.Point(137, 74);
viewFlipDeciderBox.Location = new System.Drawing.Point(137, 43);
viewFlipDeciderBox.Name = "viewFlipDeciderBox";
viewFlipDeciderBox.Size = new System.Drawing.Size(365, 25);
viewFlipDeciderBox.TabIndex = 3;
//
//
// mainTabControl
//
//
mainTabControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
mainTabControl.Controls.Add(logEventsTab);
mainTabControl.Controls.Add(bomTab);
mainTabControl.Controls.Add(cutTemplatesTab);
mainTabControl.Location = new System.Drawing.Point(15, 105);
mainTabControl.Location = new System.Drawing.Point(15, 75);
mainTabControl.Name = "mainTabControl";
mainTabControl.Padding = new System.Drawing.Point(20, 5);
mainTabControl.SelectedIndex = 0;
mainTabControl.Size = new System.Drawing.Size(910, 492);
mainTabControl.Size = new System.Drawing.Size(910, 522);
mainTabControl.TabIndex = 12;
//
//
// logEventsTab
//
//
logEventsTab.Controls.Add(logEventsDataGrid);
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(902, 458);
logEventsTab.Size = new System.Drawing.Size(902, 488);
logEventsTab.TabIndex = 0;
logEventsTab.Text = "Log Events";
logEventsTab.UseVisualStyleBackColor = true;
//
//
// logEventsDataGrid
//
//
logEventsDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
logEventsDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
logEventsDataGrid.Name = "logEventsDataGrid";
logEventsDataGrid.Size = new System.Drawing.Size(890, 440);
logEventsDataGrid.Size = new System.Drawing.Size(890, 476);
logEventsDataGrid.TabIndex = 0;
//
//
// bomTab
//
//
bomTab.Controls.Add(bomDataGrid);
bomTab.Location = new System.Drawing.Point(4, 28);
bomTab.Name = "bomTab";
bomTab.Padding = new System.Windows.Forms.Padding(3);
bomTab.Size = new System.Drawing.Size(902, 409);
bomTab.Size = new System.Drawing.Size(902, 490);
bomTab.TabIndex = 1;
bomTab.Text = "Bill Of Materials";
bomTab.UseVisualStyleBackColor = true;
//
//
// bomDataGrid
//
//
bomDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
bomDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
bomDataGrid.Location = new System.Drawing.Point(6, 6);
bomDataGrid.Name = "bomDataGrid";
bomDataGrid.Size = new System.Drawing.Size(1281, 644);
bomDataGrid.Size = new System.Drawing.Size(890, 478);
bomDataGrid.TabIndex = 1;
//
//
// cutTemplatesTab
//
//
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
cutTemplatesTab.Location = new System.Drawing.Point(4, 28);
cutTemplatesTab.Name = "cutTemplatesTab";
cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3);
cutTemplatesTab.Size = new System.Drawing.Size(902, 409);
cutTemplatesTab.Size = new System.Drawing.Size(902, 490);
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(1281, 644);
cutTemplatesDataGrid.Size = new System.Drawing.Size(890, 478);
cutTemplatesDataGrid.TabIndex = 2;
//
// equipmentBox
//
equipmentBox.FormattingEnabled = true;
equipmentBox.Location = new System.Drawing.Point(137, 12);
equipmentBox.Name = "equipmentBox";
equipmentBox.Size = new System.Drawing.Size(166, 25);
equipmentBox.TabIndex = 13;
//
// label1
//
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(61, 15);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(70, 17);
label1.TabIndex = 2;
label1.Text = "Equipment";
//
// label2
//
label2.AutoSize = true;
label2.Location = new System.Drawing.Point(321, 15);
label2.Name = "label2";
label2.Size = new System.Drawing.Size(56, 17);
label2.TabIndex = 2;
label2.Text = "Drawing";
//
// drawingNoBox
//
drawingNoBox.FormattingEnabled = true;
drawingNoBox.Location = new System.Drawing.Point(383, 12);
drawingNoBox.Name = "drawingNoBox";
drawingNoBox.Size = new System.Drawing.Size(119, 25);
drawingNoBox.TabIndex = 13;
//
// titleLabel
//
titleLabel.AutoSize = true;
titleLabel.Location = new System.Drawing.Point(99, 46);
titleLabel.Name = "titleLabel";
titleLabel.Size = new System.Drawing.Size(32, 17);
titleLabel.TabIndex = 14;
titleLabel.Text = "Title";
//
// titleBox
//
titleBox.Location = new System.Drawing.Point(137, 43);
titleBox.Name = "titleBox";
titleBox.Size = new System.Drawing.Size(365, 25);
titleBox.TabIndex = 15;
//
//
// MainForm
//
//
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
ClientSize = new System.Drawing.Size(937, 609);
Controls.Add(titleBox);
Controls.Add(titleLabel);
Controls.Add(drawingNoBox);
Controls.Add(equipmentBox);
Controls.Add(txtFilenameTemplate);
Controls.Add(mainTabControl);
Controls.Add(viewFlipDeciderBox);
Controls.Add(label2);
Controls.Add(label1);
Controls.Add(templateLabel);
Controls.Add(label3);
Controls.Add(runButton);
Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
@@ -253,11 +211,7 @@ namespace ExportDXF.Forms
private System.Windows.Forms.DataGridView bomDataGrid;
private System.Windows.Forms.TabPage cutTemplatesTab;
private System.Windows.Forms.DataGridView cutTemplatesDataGrid;
private System.Windows.Forms.ComboBox equipmentBox;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.ComboBox drawingNoBox;
private System.Windows.Forms.Label titleLabel;
private System.Windows.Forms.TextBox titleBox;
private System.Windows.Forms.Label templateLabel;
private System.Windows.Forms.TextBox txtFilenameTemplate;
}
}

View File

@@ -1,12 +1,11 @@
using ExportDXF.ApiClient;
using ExportDXF.Extensions;
using ExportDXF.Models;
using ExportDXF.Services;
using ExportDXF.ViewFlipDeciders;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -18,14 +17,13 @@ namespace ExportDXF.Forms
{
private readonly ISolidWorksService _solidWorksService;
private readonly IDxfExportService _exportService;
private readonly IFabWorksApiClient _apiClient;
private readonly IDrawingInfoExtractor[] _extractors;
private CancellationTokenSource _cancellationTokenSource;
private readonly BindingList<LogEvent> _logEvents;
private readonly BindingList<BomItem> _bomItems;
private readonly BindingList<CutTemplate> _cutTemplates;
private List<DrawingInfo> _allDrawings;
public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IFabWorksApiClient apiClient)
public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IDrawingInfoExtractor[] extractors)
{
InitializeComponent();
_solidWorksService = solidWorksService ??
@@ -33,17 +31,15 @@ namespace ExportDXF.Forms
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
_exportService = exportService ??
throw new ArgumentNullException(nameof(exportService));
_apiClient = apiClient ??
throw new ArgumentNullException(nameof(apiClient));
_extractors = extractors ??
throw new ArgumentNullException(nameof(extractors));
_logEvents = new BindingList<LogEvent>();
_bomItems = new BindingList<BomItem>();
_cutTemplates = new BindingList<CutTemplate>();
_allDrawings = new List<DrawingInfo>();
InitializeViewFlipDeciders();
InitializeLogEventsGrid();
InitializeBomGrid();
InitializeCutTemplatesGrid();
InitializeDrawingDropdowns();
}
~MainForm()
@@ -68,10 +64,8 @@ namespace ExportDXF.Forms
LogMessage("Connecting to SolidWorks, this may take a minute...");
await _solidWorksService.ConnectAsync();
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
LogMessage("Files will be uploaded to FabWorks API");
await LoadDrawingDropdownsAsync();
LogMessage("Ready");
await UpdateActiveDocumentDisplayAsync();
UpdateActiveDocumentDisplay();
runButton.Enabled = true;
}
catch (Exception ex)
@@ -91,7 +85,7 @@ namespace ExportDXF.Forms
ViewFlipDecider = d
})
.ToList();
// Move "Automatic" to the top if it exists
var automatic = items.FirstOrDefault(i => i.Name == "Automatic");
if (automatic != null)
{
@@ -104,17 +98,13 @@ namespace ExportDXF.Forms
private void InitializeLogEventsGrid()
{
// Clear any existing columns first
logEventsDataGrid.Columns.Clear();
// Configure grid settings
logEventsDataGrid.AutoGenerateColumns = false;
logEventsDataGrid.AllowUserToAddRows = false;
logEventsDataGrid.AllowUserToDeleteRows = false;
logEventsDataGrid.ReadOnly = true;
logEventsDataGrid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
// Add columns
logEventsDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(LogEvent.Time),
@@ -144,26 +134,19 @@ namespace ExportDXF.Forms
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
});
// Add row coloring based on log level
logEventsDataGrid.CellFormatting += LogEventsDataGrid_CellFormatting;
// Set the data source AFTER adding columns
logEventsDataGrid.DataSource = _logEvents;
}
private void InitializeBomGrid()
{
// Clear any existing columns first
bomDataGrid.Columns.Clear();
// Configure grid settings
bomDataGrid.AutoGenerateColumns = false;
bomDataGrid.AllowUserToAddRows = false;
bomDataGrid.AllowUserToDeleteRows = false;
bomDataGrid.ReadOnly = true;
bomDataGrid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
// Add columns
bomDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(BomItem.ItemNo),
@@ -213,14 +196,12 @@ namespace ExportDXF.Forms
Width = 120
});
// Set the data source AFTER adding columns
bomDataGrid.DataSource = _bomItems;
}
private void InitializeCutTemplatesGrid()
{
cutTemplatesDataGrid.Columns.Clear();
cutTemplatesDataGrid.AutoGenerateColumns = false;
cutTemplatesDataGrid.AllowUserToAddRows = false;
cutTemplatesDataGrid.AllowUserToDeleteRows = false;
@@ -241,6 +222,13 @@ namespace ExportDXF.Forms
Width = 250
});
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.Revision),
HeaderText = "Rev",
Width = 50
});
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.Thickness),
@@ -272,69 +260,6 @@ namespace ExportDXF.Forms
cutTemplatesDataGrid.DataSource = _cutTemplates;
}
private void InitializeDrawingDropdowns()
{
// Wire up event handler; actual data loading happens in LoadDrawingDropdownsAsync
equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged;
}
private async Task LoadDrawingDropdownsAsync()
{
try
{
var equipmentNumbers = await _apiClient.GetEquipmentNumbersAsync();
equipmentBox.Items.Clear();
equipmentBox.Items.Add("");
foreach (var eq in equipmentNumbers)
{
equipmentBox.Items.Add(eq);
}
// Clear _allDrawings — drawing list is now loaded on equipment selection
_allDrawings = new List<DrawingInfo>();
await UpdateDrawingDropdownAsync();
}
catch (Exception ex)
{
// API might not be available yet - that's OK
System.Diagnostics.Debug.WriteLine($"Failed to load equipment numbers from API: {ex.Message}");
}
}
private async void EquipmentBox_SelectedIndexChanged(object sender, EventArgs e)
{
await UpdateDrawingDropdownAsync();
}
private async Task UpdateDrawingDropdownAsync()
{
var selectedEquipment = equipmentBox.SelectedItem?.ToString();
drawingNoBox.Items.Clear();
drawingNoBox.Items.Add("");
try
{
var drawingNumbers = await _apiClient.GetDrawingNumbersByEquipmentAsync(
string.IsNullOrEmpty(selectedEquipment) ? null : selectedEquipment);
foreach (var dn in drawingNumbers)
{
drawingNoBox.Items.Add(dn);
}
}
catch
{
// API might not be available
}
if (drawingNoBox.Items.Count > 0)
{
drawingNoBox.SelectedIndex = 0;
}
}
private async void button1_Click(object sender, EventArgs e)
{
if (_cancellationTokenSource != null)
@@ -351,6 +276,13 @@ namespace ExportDXF.Forms
{
try
{
var template = txtFilenameTemplate.Text.Trim();
if (!FilenameTemplateParser.Validate(template, out var error))
{
MessageBox.Show(error, "Invalid Template", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
_cancellationTokenSource = new CancellationTokenSource();
var token = _cancellationTokenSource.Token;
UpdateUIForExportStart();
@@ -362,36 +294,27 @@ namespace ExportDXF.Forms
return;
}
// Use equipment/drawing values from the UI dropdowns
var equipment = equipmentBox.Text?.Trim();
var drawingNo = drawingNoBox.Text?.Trim();
var filePrefix = !string.IsNullOrEmpty(equipment)
? (!string.IsNullOrEmpty(drawingNo) ? $"{equipment} {drawingNo}" : equipment)
: activeDoc.Title;
var sourceDir = Path.GetDirectoryName(activeDoc.FilePath);
var outputFolder = Path.Combine(sourceDir, "Templates");
var viewFlipDecider = GetSelectedViewFlipDecider();
var title = titleBox.Text?.Trim();
var exportContext = new ExportContext
{
ActiveDocument = activeDoc,
ViewFlipDecider = viewFlipDecider,
FilePrefix = filePrefix,
Equipment = equipment,
DrawingNo = drawingNo,
Title = string.IsNullOrEmpty(title) ? null : title,
EquipmentId = null,
FilenameTemplate = template,
OutputFolder = outputFolder,
CancellationToken = token,
ProgressCallback = (msg, level, file) => LogMessage(msg, level, file),
BomItemCallback = AddBomItem
};
// Clear previous BOM items and cut templates
_bomItems.Clear();
_cutTemplates.Clear();
LogMessage($"Started at {DateTime.Now:t}");
LogMessage("Exporting (files will be uploaded to API)...");
LogMessage($"Output: {outputFolder}");
_solidWorksService.SetCommandInProgress(true);
@@ -432,27 +355,29 @@ namespace ExportDXF.Forms
private void UpdateUIForExportStart()
{
viewFlipDeciderBox.Enabled = false;
txtFilenameTemplate.Enabled = false;
runButton.Text = "Stop";
}
private void UpdateUIForExportComplete()
{
viewFlipDeciderBox.Enabled = true;
txtFilenameTemplate.Enabled = true;
runButton.Text = "Start";
runButton.Enabled = true;
}
private async void OnActiveDocumentChanged(object sender, EventArgs e)
private void OnActiveDocumentChanged(object sender, EventArgs e)
{
if (InvokeRequired)
{
Invoke(new Action(async () => await UpdateActiveDocumentDisplayAsync()));
Invoke(new Action(() => UpdateActiveDocumentDisplay()));
return;
}
await UpdateActiveDocumentDisplayAsync();
UpdateActiveDocumentDisplay();
}
private async Task UpdateActiveDocumentDisplayAsync()
private void UpdateActiveDocumentDisplay()
{
var activeDoc = _solidWorksService.GetActiveDocument();
var docTitle = activeDoc?.Title ?? "No Document Open";
@@ -461,66 +386,17 @@ namespace ExportDXF.Forms
if (activeDoc == null)
return;
// Try API first: look up the most recent export for this file path
DrawingInfo drawingInfo = null;
if (!string.IsNullOrEmpty(activeDoc.FilePath))
// Try each extractor to auto-fill the template
foreach (var extractor in _extractors)
{
drawingInfo = await LookupDrawingInfoFromHistoryAsync(activeDoc.FilePath);
}
// Fall back to parsing the document title
if (drawingInfo == null)
{
drawingInfo = DrawingInfo.Parse(activeDoc.Title);
}
if (drawingInfo != null)
{
// Detach event to prevent async race when setting equipment
equipmentBox.SelectedIndexChanged -= EquipmentBox_SelectedIndexChanged;
if (!string.IsNullOrEmpty(drawingInfo.EquipmentNo))
if (extractor.TryExtract(activeDoc.Title, out var info))
{
if (!equipmentBox.Items.Contains(drawingInfo.EquipmentNo))
equipmentBox.Items.Add(drawingInfo.EquipmentNo);
equipmentBox.Text = drawingInfo.EquipmentNo;
}
// Load drawings for the selected equipment, then set drawing number
await UpdateDrawingDropdownAsync();
equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged;
if (!string.IsNullOrEmpty(drawingInfo.DrawingNo))
{
if (!drawingNoBox.Items.Contains(drawingInfo.DrawingNo))
drawingNoBox.Items.Add(drawingInfo.DrawingNo);
drawingNoBox.Text = drawingInfo.DrawingNo;
txtFilenameTemplate.Text = info.DefaultTemplate;
return;
}
}
}
private async Task<DrawingInfo> LookupDrawingInfoFromHistoryAsync(string filePath)
{
try
{
var dto = await _apiClient.GetExportBySourceFileAsync(filePath);
if (dto != null && !string.IsNullOrEmpty(dto.DrawingNumber))
{
if (!string.IsNullOrEmpty(dto.Title))
titleBox.Text = dto.Title;
return DrawingInfo.Parse(dto.DrawingNumber);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to look up drawing info from API: {ex.Message}");
}
return null;
}
private void LogMessage(string message, LogLevel level = LogLevel.Info, string file = null)
{
AddLogEvent(level, LogAction.Start, message, part: file);
@@ -550,7 +426,6 @@ namespace ExportDXF.Forms
_logEvents.Add(logEvent);
// Auto-scroll to the last row
if (logEventsDataGrid.Rows.Count > 0)
{
logEventsDataGrid.FirstDisplayedScrollingRowIndex = logEventsDataGrid.Rows.Count - 1;

View File

@@ -2,7 +2,6 @@ namespace ExportDXF.Models
{
public class BomItem
{
public int ID { get; set; }
public string ItemNo { get; set; } = "";
public string PartNo { get; set; } = "";
public int SortOrder { get; set; }
@@ -12,13 +11,7 @@ namespace ExportDXF.Models
public string PartName { get; set; } = "";
public string ConfigurationName { get; set; } = "";
public string Material { get; set; } = "";
// EF Core relationship to ExportRecord
public int ExportRecordId { get; set; }
public virtual ExportRecord ExportRecord { get; set; }
// Optional 1:1 relationship to CutTemplate (only for sheet metal parts)
public virtual CutTemplate CutTemplate { get; set; }
public CutTemplate CutTemplate { get; set; }
}
public struct Size

View File

@@ -4,12 +4,11 @@ namespace ExportDXF.Models
{
public class CutTemplate
{
public int Id { get; set; }
public string DxfFilePath { get; set; } = "";
public string ContentHash { get; set; }
public string CutTemplateName { get; set; } = "";
public int Revision { get; set; } = 1;
// Sheet metal properties (moved from BomItem)
private double? _thickness;
public double? Thickness
{
@@ -25,9 +24,5 @@ namespace ExportDXF.Models
get => _defaultBendRadius;
set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null;
}
// FK back to BomItem
public int BomItemId { get; set; }
public virtual BomItem BomItem { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
using ExportDXF.Models;
using ExportDXF.Models;
using ExportDXF.ViewFlipDeciders;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
@@ -28,29 +28,14 @@ namespace ExportDXF.Services
public IViewFlipDecider ViewFlipDecider { get; set; }
/// <summary>
/// Prefix to prepend to exported filenames.
/// Filename template with placeholders (e.g., "4321 A01 PT{item_no:2}").
/// </summary>
public string FilePrefix { get; set; }
public string FilenameTemplate { get; set; }
/// <summary>
/// Equipment number from the UI (e.g., "5028").
/// Output folder for DXF files and Excel workbook.
/// </summary>
public string Equipment { get; set; }
/// <summary>
/// Drawing number from the UI (e.g., "A02", "Misc").
/// </summary>
public string DrawingNo { get; set; }
/// <summary>
/// Optional title/label for the export.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Selected Equipment ID for API operations (optional).
/// </summary>
public int? EquipmentId { get; set; }
public string OutputFolder { get; set; }
/// <summary>
/// Cancellation token for canceling the export operation.
@@ -82,8 +67,8 @@ namespace ExportDXF.Services
get
{
return Path.Combine(
Application.StartupPath,
DRAWING_TEMPLATE_FOLDER,
Application.StartupPath,
DRAWING_TEMPLATE_FOLDER,
DRAWING_TEMPLATE_FILE);
}
}
@@ -123,23 +108,17 @@ namespace ExportDXF.Services
if (!string.IsNullOrEmpty(title))
{
// Close the document without saving
SolidWorksApp.CloseDoc(title);
ProgressCallback?.Invoke("Closed template drawing", LogLevel.Info, null);
}
}
// Clear the reference regardless of success/failure
TemplateDrawing = null;
}
catch (Exception ex)
{
ProgressCallback?.Invoke($"Failed to close template drawing: {ex.Message}", LogLevel.Error, null);
// Still clear the reference to prevent further issues
TemplateDrawing = null;
// Don't throw here as this is cleanup code - log the error but continue
}
}

View File

@@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
namespace ExportDXF.Models
{
public class ExportRecord
{
public int Id { get; set; }
public string DrawingNumber { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
public DateTime ExportedAt { get; set; }
public string ExportedBy { get; set; }
public string PdfContentHash { get; set; }
public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
}
}

View File

@@ -1,18 +1,12 @@
using ExportDXF.ApiClient;
using ExportDXF.Forms;
using ExportDXF.Services;
using System;
using System.Configuration;
using System.Net.Http;
using System.Windows.Forms;
namespace ExportDXF
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
@@ -25,40 +19,32 @@ namespace ExportDXF
}
}
/// <summary>
/// Simple dependency injection container.
/// </summary>
public class ServiceContainer
{
private readonly string _apiBaseUrl;
public ServiceContainer()
{
_apiBaseUrl = ConfigurationManager.AppSettings["FabWorksApiUrl"] ?? "http://localhost:5206";
}
public MainForm ResolveMainForm()
{
var solidWorksService = new SolidWorksService();
var bomExtractor = new BomExtractor();
var partExporter = new PartExporter();
var drawingExporter = new DrawingExporter();
var httpClient = new HttpClient
{
BaseAddress = new Uri(_apiBaseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
var apiClient = new FabWorksApiClient(httpClient);
var excelExportService = new ExcelExportService();
var logFileService = new LogFileService();
var exportService = new DxfExportService(
solidWorksService,
bomExtractor,
partExporter,
drawingExporter,
apiClient);
excelExportService,
logFileService);
return new MainForm(solidWorksService, exportService, apiClient);
var extractors = new IDrawingInfoExtractor[]
{
new EquipmentDrawingInfoExtractor(),
new DefaultDrawingInfoExtractor()
};
return new MainForm(solidWorksService, exportService, extractors);
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Configuration;
using System.IO;
namespace ExportDXF.Services
{
/// <summary>
/// Fallback extractor that uses the document name as prefix
/// and appends the configured default suffix.
/// Always returns true — this is the catch-all.
/// </summary>
public class DefaultDrawingInfoExtractor : IDrawingInfoExtractor
{
public bool TryExtract(string documentName, out DrawingInfoResult info)
{
var name = Path.GetFileNameWithoutExtension(documentName);
var suffix = ConfigurationManager.AppSettings["DefaultSuffix"] ?? "PT{item_no:2}";
info = new DrawingInfoResult
{
EquipmentNumber = null,
DrawingNumber = null,
DefaultTemplate = $"{name} {suffix}"
};
return true;
}
}
}

View File

@@ -1,9 +1,7 @@
using ExportDXF.ApiClient;
using ExportDXF.Extensions;
using ExportDXF.ItemExtractors;
using ExportDXF.Models;
using ExportDXF.Utilities;
using ExportDXF;
using SolidWorks.Interop.sldworks;
using System;
using System.Collections.Generic;
@@ -15,42 +13,34 @@ 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>
Task ExportAsync(ExportContext context);
}
/// <summary>
/// Service responsible for orchestrating the export of SolidWorks documents to DXF format.
/// Files are generated locally in a temp directory, then uploaded to the API for storage and versioning.
/// </summary>
public class DxfExportService : IDxfExportService
{
private readonly ISolidWorksService _solidWorksService;
private readonly IBomExtractor _bomExtractor;
private readonly IPartExporter _partExporter;
private readonly IDrawingExporter _drawingExporter;
private readonly IFabWorksApiClient _apiClient;
private readonly ExcelExportService _excelExportService;
private readonly LogFileService _logFileService;
public DxfExportService(
ISolidWorksService solidWorksService,
IBomExtractor bomExtractor,
IPartExporter partExporter,
IDrawingExporter drawingExporter,
IFabWorksApiClient apiClient)
ExcelExportService excelExportService,
LogFileService logFileService)
{
_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));
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
_excelExportService = excelExportService ?? throw new ArgumentNullException(nameof(excelExportService));
_logFileService = logFileService ?? throw new ArgumentNullException(nameof(logFileService));
}
/// <summary>
/// Exports the document specified in the context to DXF format.
/// </summary>
public async Task ExportAsync(ExportContext context)
{
if (context == null)
@@ -59,6 +49,28 @@ namespace ExportDXF.Services
ValidateContext(context);
SetupExportContext(context);
var outputFolder = context.OutputFolder;
if (!Directory.Exists(outputFolder))
Directory.CreateDirectory(outputFolder);
var prefix = FilenameTemplateParser.GetPrefix(
context.FilenameTemplate,
context.ActiveDocument.Title);
var xlsxPath = Path.Combine(outputFolder, $"{prefix}.xlsx");
var logPath = Path.Combine(outputFolder, $"{prefix}.log");
_logFileService.StartExportLog(logPath);
_logFileService.LogInfo($"Export started: {context.ActiveDocument.FilePath}");
_logFileService.LogInfo($"Template: {context.FilenameTemplate}");
_logFileService.LogInfo($"Output: {outputFolder}");
// Read existing cut templates for revision comparison
var existingTemplates = _excelExportService.ReadExistingCutTemplates(xlsxPath);
var bomItems = new List<BomItem>();
List<Dictionary<string, string>> rawBomTable = null;
var startTime = DateTime.Now;
var tempDir = CreateTempWorkDir();
@@ -66,26 +78,39 @@ namespace ExportDXF.Services
{
_solidWorksService.EnableUserControl(false);
var drawingNumber = ParseDrawingNumber(context);
switch (context.ActiveDocument.DocumentType)
{
case DocumentType.Part:
await ExportPartAsync(context, tempDir, drawingNumber);
await ExportPartAsync(context, tempDir, outputFolder, existingTemplates, bomItems);
break;
case DocumentType.Assembly:
await ExportAssemblyAsync(context, tempDir, drawingNumber);
await ExportAssemblyAsync(context, tempDir, outputFolder, existingTemplates, bomItems);
break;
case DocumentType.Drawing:
await ExportDrawingAsync(context, drawingNumber, tempDir);
rawBomTable = await ExportDrawingAsync(context, tempDir, outputFolder, existingTemplates, bomItems);
break;
default:
LogProgress(context, "Unknown document type.", LogLevel.Error);
break;
}
// Write Excel file
_excelExportService.Write(xlsxPath, rawBomTable, bomItems);
_logFileService.LogInfo($"Wrote {Path.GetFileName(xlsxPath)}");
LogProgress(context, $"Saved {Path.GetFileName(xlsxPath)}");
}
catch (OperationCanceledException)
{
_logFileService.LogWarning("Export cancelled by user");
throw;
}
catch (Exception ex)
{
_logFileService.LogError($"Export failed: {ex.Message}");
throw;
}
finally
{
@@ -94,13 +119,19 @@ namespace ExportDXF.Services
CleanupTempDir(tempDir);
var duration = DateTime.Now - startTime;
_logFileService.LogInfo($"Run time: {duration.ToReadableFormat()}");
LogProgress(context, $"Run time: {duration.ToReadableFormat()}");
}
}
#region Export Methods by Document Type
private async Task ExportPartAsync(ExportContext context, string tempDir, string drawingNumber)
private async Task ExportPartAsync(
ExportContext context,
string tempDir,
string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
List<BomItem> bomItems)
{
LogProgress(context, "Active document is a Part");
@@ -111,54 +142,25 @@ namespace ExportDXF.Services
return;
}
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
var item = _partExporter.ExportSinglePart(part, tempDir, context);
if (item != null)
{
// Check if this part+config already has a BOM item for this drawing
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
item.ItemNo = existingItemNo ?? await GetNextItemNumberAsync(drawingNumber);
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 ?? ""
};
// Upload DXF to API and get stored path
if (!string.IsNullOrEmpty(item.LocalTempPath))
{
var uploadResult = await UploadDxfAsync(item, context);
if (uploadResult != null)
{
bomItem.CutTemplate = new CutTemplate
{
DxfFilePath = uploadResult.StoredFilePath,
ContentHash = item.ContentHash,
Thickness = item.Thickness > 0 ? item.Thickness : null,
KFactor = item.KFactor > 0 ? item.KFactor : null,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
};
}
}
item.ItemNo = "1";
var bomItem = CreateBomItem(item);
PlaceDxfFile(item, context, outputFolder, existingTemplates, bomItem);
bomItems.Add(bomItem);
context.BomItemCallback?.Invoke(bomItem);
if (exportRecord != null)
await SaveBomItemAsync(exportRecord.Id, bomItem, context);
}
}
private async Task ExportAssemblyAsync(ExportContext context, string tempDir, string drawingNumber)
private async Task ExportAssemblyAsync(
ExportContext context,
string tempDir,
string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
List<BomItem> bomItems)
{
LogProgress(context, "Active document is an Assembly");
LogProgress(context, "Fetching components...");
@@ -180,31 +182,26 @@ namespace ExportDXF.Services
LogProgress(context, $"Found {items.Count} item(s).");
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
// Check existing BOM items and reuse item numbers, or assign new ones
var nextNum = int.Parse(await GetNextItemNumberAsync(drawingNumber));
// Assign item numbers
int nextNum = 1;
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item.ItemNo))
{
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
if (existingItemNo != null)
{
item.ItemNo = existingItemNo;
}
else
{
item.ItemNo = nextNum.ToString();
nextNum++;
}
item.ItemNo = nextNum.ToString();
nextNum++;
}
}
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
await ExportItemsAsync(items, tempDir, outputFolder, context, existingTemplates, bomItems);
}
private async Task ExportDrawingAsync(ExportContext context, string drawingNumber, string tempDir)
private async Task<List<Dictionary<string, string>>> ExportDrawingAsync(
ExportContext context,
string tempDir,
string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
List<BomItem> bomItems)
{
LogProgress(context, "Active document is a Drawing");
LogProgress(context, "Finding BOM tables...");
@@ -213,59 +210,35 @@ namespace ExportDXF.Services
if (drawing == null)
{
LogProgress(context, "Failed to get drawing document.", LogLevel.Error);
return;
return null;
}
// Read raw BOM table for Excel output
var rawBomTable = new List<Dictionary<string, string>>();
var bomTables = drawing.GetBomTables();
foreach (var table in bomTables)
{
var rows = RawBomTableReader.Read(table);
rawBomTable.AddRange(rows);
}
// Extract items for DXF export
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
if (items == null || items.Count == 0)
{
LogProgress(context, "Error: Bill of materials not found.", LogLevel.Error);
return;
return rawBomTable;
}
LogProgress(context, $"Found {items.Count} component(s)");
// Export drawing to PDF in temp dir
_drawingExporter.ExportToPdf(drawing, tempDir, context);
// Export drawing to PDF in output folder
_drawingExporter.ExportToPdf(drawing, outputFolder, context);
// Create export record via API
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
await ExportItemsAsync(items, tempDir, outputFolder, context, existingTemplates, bomItems);
// Upload PDF to API with versioning
try
{
var pdfs = Directory.GetFiles(tempDir, "*.pdf");
if (pdfs.Length > 0)
{
var pdfTempPath = pdfs[0];
var pdfHash = ContentHasher.ComputeFileHash(pdfTempPath);
var uploadResult = await _apiClient.UploadPdfAsync(
pdfTempPath,
context.Equipment,
context.DrawingNo,
pdfHash,
exportRecord?.Id);
if (uploadResult != null)
{
if (uploadResult.WasUnchanged)
LogProgress(context, $"PDF unchanged: {uploadResult.FileName}", LogLevel.Info);
else if (uploadResult.IsNewFile)
LogProgress(context, $"Saved PDF: {uploadResult.FileName}", LogLevel.Info);
else
LogProgress(context, $"PDF updated: {uploadResult.FileName}", LogLevel.Info);
}
}
}
catch (Exception ex)
{
LogProgress(context, $"PDF upload error: {ex.Message}", LogLevel.Error);
}
// Export parts to DXF and save BOM items
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
return rawBomTable;
}
#endregion
@@ -274,17 +247,12 @@ namespace ExportDXF.Services
private void SetupExportContext(ExportContext context)
{
// Set up SolidWorks application reference
context.SolidWorksApp = _solidWorksService.GetNativeSldWorks();
if (context.SolidWorksApp == null)
{
throw new InvalidOperationException("SolidWorks service is not connected.");
}
// Set up drawing template path
context.TemplateDrawing = null;
LogProgress(context, "Export context initialized");
}
@@ -292,13 +260,11 @@ namespace ExportDXF.Services
{
try
{
// Clean up template drawing if it was created
context.CleanupTemplateDrawing();
}
catch (Exception ex)
{
LogProgress(context, $"Warning: Failed to cleanup template drawing: {ex.Message}", LogLevel.Warning);
// Don't throw - this is cleanup code
}
}
@@ -314,7 +280,6 @@ namespace ExportDXF.Services
{
TopLevelOnly = false
};
return extractor.GetItems();
}
catch (Exception ex)
@@ -324,10 +289,17 @@ namespace ExportDXF.Services
}
}
private async Task ExportItemsAsync(List<Item> items, string tempDir, ExportContext context, int? exportRecordId = null)
private async Task ExportItemsAsync(
List<Item> items,
string tempDir,
string outputFolder,
ExportContext context,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
List<BomItem> bomItems)
{
int successCount = 0;
int skippedCount = 0;
int unchangedCount = 0;
int failureCount = 0;
int sortOrder = 0;
@@ -341,215 +313,150 @@ namespace ExportDXF.Services
try
{
// PartExporter will handle template drawing creation through context
_partExporter.ExportItem(item, tempDir, context);
// Always create BomItem for every item (sheet metal or not)
var bomItem = new BomItem
{
ExportRecordId = exportRecordId ?? 0,
ItemNo = item.ItemNo ?? "",
PartNo = item.FileName ?? item.PartName ?? "",
SortOrder = sortOrder++,
Qty = item.Quantity,
TotalQty = item.Quantity,
Description = item.Description ?? "",
PartName = item.PartName ?? "",
ConfigurationName = item.Configuration ?? "",
Material = item.Material ?? ""
};
var bomItem = CreateBomItem(item);
bomItem.SortOrder = sortOrder++;
// Only upload and create CutTemplate if DXF was exported successfully
if (!string.IsNullOrEmpty(item.LocalTempPath))
{
successCount++;
var uploadResult = await UploadDxfAsync(item, context);
if (uploadResult != null)
{
bomItem.CutTemplate = new CutTemplate
{
DxfFilePath = uploadResult.StoredFilePath,
ContentHash = item.ContentHash,
Thickness = item.Thickness > 0 ? item.Thickness : null,
KFactor = item.KFactor > 0 ? item.KFactor : null,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
};
}
var wasPlaced = PlaceDxfFile(item, context, outputFolder, existingTemplates, bomItem);
if (wasPlaced)
successCount++;
else
unchangedCount++;
}
else
{
skippedCount++;
}
// Add to UI
bomItems.Add(bomItem);
context.BomItemCallback?.Invoke(bomItem);
// Save BOM item via API if we have an export record
if (exportRecordId.HasValue)
{
await SaveBomItemAsync(exportRecordId.Value, bomItem, context);
}
}
catch (Exception ex)
{
LogProgress(context, $"Error exporting item {item.ItemNo}: {ex.Message}", LogLevel.Error);
_logFileService.LogError($"Item {item.ItemNo}: {ex.Message}");
failureCount++;
}
}
var summary = $"Export complete: {successCount} exported, {skippedCount} skipped";
var summary = $"Export complete: {successCount} exported";
if (unchangedCount > 0)
summary += $", {unchangedCount} unchanged";
if (skippedCount > 0)
summary += $", {skippedCount} skipped (non-sheet-metal)";
if (failureCount > 0)
summary += $", {failureCount} failed";
LogProgress(context, summary, failureCount > 0 ? LogLevel.Warning : LogLevel.Info);
if (exportRecordId.HasValue)
{
LogProgress(context, $"BOM items saved (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info);
}
_logFileService.LogInfo(summary);
}
#endregion
#region File Upload
private async Task<ApiFileUploadResponse> UploadDxfAsync(Item item, ExportContext context)
/// <summary>
/// Places a DXF file in the output folder with revision tracking.
/// Returns true if a new file was written, false if unchanged.
/// </summary>
private bool PlaceDxfFile(
Item item,
ExportContext context,
string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
BomItem bomItem)
{
try
var baseName = FilenameTemplateParser.Evaluate(context.FilenameTemplate, item);
var contentHash = item.ContentHash;
int revision = 1;
string dxfFileName;
// Check existing templates for revision comparison
if (existingTemplates.TryGetValue(item.ItemNo, out var existing))
{
var result = await _apiClient.UploadDxfAsync(
item.LocalTempPath,
context.Equipment,
context.DrawingNo,
item.ItemNo,
item.ContentHash);
if (result.WasUnchanged)
LogProgress(context, $"DXF unchanged: {result.FileName}", LogLevel.Info);
else if (result.IsNewFile)
LogProgress(context, $"Exported: {result.FileName}", LogLevel.Info);
else
LogProgress(context, $"DXF updated: {result.FileName}", LogLevel.Info);
return result;
}
catch (Exception ex)
{
LogProgress(context, $"DXF upload failed for {item.FileName}: {ex.Message}", LogLevel.Warning);
return null;
}
}
#endregion
#region API Helpers
private async Task<ExportRecord> CreateExportRecordAsync(ExportContext context, string drawingNumber)
{
try
{
var dto = await _apiClient.CreateExportAsync(
drawingNumber ?? context.ActiveDocument.Title,
context.Equipment ?? "",
context.DrawingNo ?? "",
context.ActiveDocument.FilePath,
"", // Output folder is now managed by the API
context.Title);
var record = new ExportRecord
if (existing.ContentHash == contentHash)
{
Id = dto.Id,
DrawingNumber = dto.DrawingNumber,
EquipmentNo = dto.EquipmentNo,
DrawingNo = dto.DrawingNo,
SourceFilePath = dto.SourceFilePath,
OutputFolder = dto.OutputFolder,
ExportedAt = dto.ExportedAt,
ExportedBy = dto.ExportedBy
};
// Unchanged — skip file write, keep existing
dxfFileName = existing.FileName;
revision = existing.Revision;
LogProgress(context, $"Created export record (ID: {record.Id})", LogLevel.Info);
return record;
}
catch (Exception ex)
{
LogProgress(context, $"API error creating export record: {ex.Message}", LogLevel.Error);
return null;
}
}
LogProgress(context, $"Unchanged: {dxfFileName}", LogLevel.Info, item.PartName);
_logFileService.LogInfo($"Unchanged: {dxfFileName}");
private async Task<string> FindExistingItemNoAsync(int? exportRecordId, string partName, string configurationName)
{
if (!exportRecordId.HasValue)
return null;
try
{
var existing = await _apiClient.FindExistingBomItemAsync(exportRecordId.Value, partName, configurationName);
return existing?.ItemNo;
}
catch
{
return null;
}
}
private async Task<string> GetNextItemNumberAsync(string drawingNumber)
{
if (string.IsNullOrEmpty(drawingNumber))
return "1";
try
{
return await _apiClient.GetNextItemNumberAsync(drawingNumber);
}
catch
{
return "1";
}
}
private async Task SaveBomItemAsync(int exportRecordId, BomItem bomItem, ExportContext context)
{
try
{
var apiBomItem = new ApiBomItem
{
ItemNo = bomItem.ItemNo,
PartNo = bomItem.PartNo,
SortOrder = bomItem.SortOrder,
Qty = bomItem.Qty,
TotalQty = bomItem.TotalQty,
Description = bomItem.Description,
PartName = bomItem.PartName,
ConfigurationName = bomItem.ConfigurationName,
Material = bomItem.Material
};
if (bomItem.CutTemplate != null)
{
apiBomItem.CutTemplate = new ApiCutTemplate
bomItem.CutTemplate = new CutTemplate
{
DxfFilePath = bomItem.CutTemplate.DxfFilePath,
ContentHash = bomItem.CutTemplate.ContentHash,
Thickness = bomItem.CutTemplate.Thickness,
KFactor = bomItem.CutTemplate.KFactor,
DefaultBendRadius = bomItem.CutTemplate.DefaultBendRadius
DxfFilePath = dxfFileName,
ContentHash = contentHash,
Revision = revision,
Thickness = item.Thickness > 0 ? item.Thickness : (double?)null,
KFactor = item.KFactor > 0 ? item.KFactor : (double?)null,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : (double?)null
};
}
await _apiClient.CreateBomItemAsync(exportRecordId, apiBomItem);
return false;
}
else
{
// Changed — increment revision
revision = existing.Revision + 1;
dxfFileName = GetRevisionFileName(baseName, revision);
LogProgress(context, $"Updated: {dxfFileName} (Rev{revision})", LogLevel.Info, item.PartName);
_logFileService.LogInfo($"Updated: {dxfFileName} (was {existing.FileName})");
}
}
catch (Exception ex)
else
{
LogProgress(context, $"API error saving BOM item: {ex.Message}", LogLevel.Error);
// New item
dxfFileName = $"{baseName}.dxf";
LogProgress(context, $"Exported: {dxfFileName}", LogLevel.Info, item.PartName);
_logFileService.LogInfo($"Exported: {dxfFileName}");
}
// Copy from temp to output
var destPath = Path.Combine(outputFolder, dxfFileName);
File.Copy(item.LocalTempPath, destPath, overwrite: true);
bomItem.CutTemplate = new CutTemplate
{
DxfFilePath = Path.GetFileNameWithoutExtension(dxfFileName),
ContentHash = contentHash,
Revision = revision,
Thickness = item.Thickness > 0 ? item.Thickness : (double?)null,
KFactor = item.KFactor > 0 ? item.KFactor : (double?)null,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : (double?)null
};
return true;
}
private BomItem CreateBomItem(Item item)
{
return new BomItem
{
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 ?? ""
};
}
#endregion
#region Helper Methods
private string GetRevisionFileName(string baseName, int revision)
{
if (revision <= 1)
return $"{baseName}.dxf";
return $"{baseName} Rev{revision}.dxf";
}
private string CreateTempWorkDir()
{
var path = Path.Combine(Path.GetTempPath(), "ExportDXF-" + Guid.NewGuid().ToString("N"));
@@ -570,27 +477,6 @@ namespace ExportDXF.Services
}
}
private string ParseDrawingNumber(ExportContext context)
{
// Use explicit Equipment/DrawingNo from the UI when available
if (!string.IsNullOrWhiteSpace(context?.Equipment))
{
return !string.IsNullOrWhiteSpace(context?.DrawingNo)
? $"{context.Equipment} {context.DrawingNo}"
: context.Equipment;
}
// Fallback: parse from prefix or document title
var candidate = context?.FilePrefix;
var info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate);
if (info == null)
{
var title = context?.ActiveDocument?.Title;
info = string.IsNullOrWhiteSpace(title) ? null : DrawingInfo.Parse(title);
}
return info?.ToString();
}
private void ValidateContext(ExportContext context)
{
if (context.ActiveDocument == null)
@@ -598,6 +484,12 @@ namespace ExportDXF.Services
if (context.ProgressCallback == null)
throw new ArgumentException("ProgressCallback cannot be null.", nameof(context));
if (string.IsNullOrWhiteSpace(context.FilenameTemplate))
throw new ArgumentException("FilenameTemplate cannot be null or empty.", nameof(context));
if (string.IsNullOrWhiteSpace(context.OutputFolder))
throw new ArgumentException("OutputFolder cannot be null or empty.", nameof(context));
}
private void LogProgress(ExportContext context, string message, LogLevel level = LogLevel.Info, string file = null)

View File

@@ -0,0 +1,36 @@
using System.IO;
namespace ExportDXF.Services
{
/// <summary>
/// Extracts equipment/drawing numbers from document names matching the
/// workplace format (e.g., "4321 A01.SLDDRW" → equipment "4321", drawing "A01").
/// Uses the existing DrawingInfo.Parse() logic.
/// </summary>
public class EquipmentDrawingInfoExtractor : IDrawingInfoExtractor
{
public bool TryExtract(string documentName, out DrawingInfoResult info)
{
info = null;
var name = Path.GetFileNameWithoutExtension(documentName);
var parsed = DrawingInfo.Parse(name);
if (parsed == null || string.IsNullOrEmpty(parsed.EquipmentNo))
return false;
var template = !string.IsNullOrEmpty(parsed.DrawingNo)
? $"{parsed.EquipmentNo} {parsed.DrawingNo} PT{{item_no:2}}"
: $"{parsed.EquipmentNo} PT{{item_no:2}}";
info = new DrawingInfoResult
{
EquipmentNumber = parsed.EquipmentNo,
DrawingNumber = parsed.DrawingNo,
DefaultTemplate = template
};
return true;
}
}
}

View File

@@ -0,0 +1,139 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ClosedXML.Excel;
using ExportDXF.Models;
namespace ExportDXF.Services
{
public class ExcelExportService
{
/// <summary>
/// Reads existing Cut Templates from an xlsx file to compare content hashes.
/// Returns empty dictionary if file doesn't exist or has no Cut Templates sheet.
/// Key = Item #, Value = (ContentHash, Revision, FileName)
/// </summary>
public Dictionary<string, (string ContentHash, int Revision, string FileName)> ReadExistingCutTemplates(string xlsxPath)
{
var result = new Dictionary<string, (string, int, string)>();
if (!File.Exists(xlsxPath))
return result;
using (var workbook = new XLWorkbook(xlsxPath))
{
if (!workbook.TryGetWorksheet("Cut Templates", out var ws))
return result;
var lastCol = ws.LastColumnUsed()?.ColumnNumber() ?? 0;
var lastRow = ws.LastRowUsed()?.RowNumber() ?? 1;
if (lastCol == 0 || lastRow <= 1)
return result;
var headers = new Dictionary<string, int>();
for (int col = 1; col <= lastCol; col++)
{
var header = ws.Cell(1, col).GetString();
if (!string.IsNullOrEmpty(header))
headers[header] = col;
}
if (!headers.ContainsKey("Item #") || !headers.ContainsKey("Content Hash"))
return result;
for (int row = 2; row <= lastRow; row++)
{
var itemNo = ws.Cell(row, headers["Item #"]).GetString();
var hash = ws.Cell(row, headers["Content Hash"]).GetString();
var revision = headers.ContainsKey("Revision")
? ws.Cell(row, headers["Revision"]).GetValue<int>()
: 1;
var fileName = headers.ContainsKey("File Name")
? ws.Cell(row, headers["File Name"]).GetString()
: "";
if (!string.IsNullOrEmpty(itemNo))
result[itemNo] = (hash, revision, fileName);
}
}
return result;
}
/// <summary>
/// Writes or updates the xlsx file with BOM and Cut Templates sheets.
/// rawBomTable: list of rows where each row is a dictionary of column name → value.
/// If null or empty, the BOM sheet is not written (Part/Assembly exports).
/// </summary>
public void Write(
string xlsxPath,
List<Dictionary<string, string>> rawBomTable,
List<BomItem> bomItems)
{
using (var workbook = File.Exists(xlsxPath)
? new XLWorkbook(xlsxPath)
: new XLWorkbook())
{
WriteBomSheet(workbook, rawBomTable);
WriteCutTemplatesSheet(workbook, bomItems);
workbook.SaveAs(xlsxPath);
}
}
private void WriteBomSheet(XLWorkbook workbook, List<Dictionary<string, string>> rawBomTable)
{
if (rawBomTable == null || rawBomTable.Count == 0)
return;
if (workbook.TryGetWorksheet("BOM", out _))
workbook.Worksheets.Delete("BOM");
var sheet = workbook.Worksheets.Add("BOM");
var columns = rawBomTable[0].Keys.ToList();
for (int col = 0; col < columns.Count; col++)
sheet.Cell(1, col + 1).Value = columns[col];
for (int row = 0; row < rawBomTable.Count; row++)
{
for (int col = 0; col < columns.Count; col++)
{
string value;
rawBomTable[row].TryGetValue(columns[col], out value);
sheet.Cell(row + 2, col + 1).Value = value ?? "";
}
}
sheet.Columns().AdjustToContents();
}
private void WriteCutTemplatesSheet(XLWorkbook workbook, List<BomItem> bomItems)
{
if (workbook.TryGetWorksheet("Cut Templates", out _))
workbook.Worksheets.Delete("Cut Templates");
var sheet = workbook.Worksheets.Add("Cut Templates");
var headers = new[] { "Item #", "File Name", "Revision", "Thickness", "K-Factor", "Bend Radius", "Content Hash" };
for (int col = 0; col < headers.Length; col++)
sheet.Cell(1, col + 1).Value = headers[col];
int row = 2;
foreach (var item in bomItems.Where(b => b.CutTemplate != null).OrderBy(b => b.ItemNo))
{
var ct = item.CutTemplate;
sheet.Cell(row, 1).Value = item.ItemNo;
sheet.Cell(row, 2).Value = ct.DxfFilePath;
sheet.Cell(row, 3).Value = ct.Revision;
sheet.Cell(row, 4).Value = ct.Thickness ?? 0;
sheet.Cell(row, 5).Value = ct.KFactor ?? 0;
sheet.Cell(row, 6).Value = ct.DefaultBendRadius ?? 0;
sheet.Cell(row, 7).Value = ct.ContentHash ?? "";
row++;
}
sheet.Columns().AdjustToContents();
}
}
}

View File

@@ -0,0 +1,96 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace ExportDXF.Services
{
public static class FilenameTemplateParser
{
private static readonly Regex PlaceholderPattern = new Regex(
@"\{(?<name>\w+)(?::(?<pad>\d+))?\}",
RegexOptions.Compiled);
/// <summary>
/// Evaluates a template string for a given item.
/// e.g. "4321 A01 PT{item_no:2}" with item_no=3 → "4321 A01 PT03"
/// </summary>
public static string Evaluate(string template, Item item)
{
return PlaceholderPattern.Replace(template, match =>
{
var name = match.Groups["name"].Value.ToLowerInvariant();
var padStr = match.Groups["pad"].Value;
int pad = string.IsNullOrEmpty(padStr) ? 0 : int.Parse(padStr);
string value;
switch (name)
{
case "item_no":
value = item.ItemNo ?? "0";
if (pad > 0)
value = value.PadLeft(pad, '0');
break;
case "part_name":
value = item.PartName ?? "";
break;
case "config":
value = item.Configuration ?? "";
break;
case "material":
value = item.Material ?? "";
break;
default:
value = match.Value;
break;
}
return value;
});
}
/// <summary>
/// Extracts the literal prefix before the first placeholder.
/// Used for naming the xlsx and log files.
/// Falls back to documentName if prefix is empty.
/// </summary>
public static string GetPrefix(string template, string documentName)
{
var match = PlaceholderPattern.Match(template);
if (!match.Success)
return template.Trim();
var prefix = template.Substring(0, match.Index).Trim();
if (string.IsNullOrEmpty(prefix))
return Path.GetFileNameWithoutExtension(documentName);
return prefix;
}
/// <summary>
/// Validates that the template contains {item_no} to prevent filename collisions.
/// </summary>
public static bool Validate(string template, out string error)
{
error = null;
if (string.IsNullOrWhiteSpace(template))
{
error = "Template cannot be empty.";
return false;
}
var hasItemNo = PlaceholderPattern.Matches(template)
.Cast<Match>()
.Any(m => m.Groups["name"].Value.Equals("item_no", StringComparison.OrdinalIgnoreCase));
if (!hasItemNo)
{
error = "Template must contain {item_no} or {item_no:N} to avoid filename collisions.";
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,14 @@
namespace ExportDXF.Services
{
public interface IDrawingInfoExtractor
{
bool TryExtract(string documentName, out DrawingInfoResult info);
}
public class DrawingInfoResult
{
public string EquipmentNumber { get; set; }
public string DrawingNumber { get; set; }
public string DefaultTemplate { get; set; }
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.IO;
namespace ExportDXF.Services
{
public class LogFileService : IDisposable
{
private StreamWriter _exportLog;
private static readonly string AppLogPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ExportDXF", "ExportDXF.log");
public void StartExportLog(string logFilePath)
{
_exportLog?.Dispose();
var dir = Path.GetDirectoryName(logFilePath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
_exportLog = new StreamWriter(logFilePath, append: true);
}
public void Log(string level, string message)
{
var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] {message}";
_exportLog?.WriteLine(line);
_exportLog?.Flush();
WriteAppLog(line);
}
public void LogInfo(string message) => Log("INFO", message);
public void LogWarning(string message) => Log("WARNING", message);
public void LogError(string message) => Log("ERROR", message);
private void WriteAppLog(string line)
{
try
{
var dir = Path.GetDirectoryName(AppLogPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
File.AppendAllText(AppLogPath, line + Environment.NewLine);
}
catch
{
// Best-effort app log — don't fail exports if log write fails
}
}
public void Dispose()
{
_exportLog?.Dispose();
_exportLog = null;
}
}
}

View File

@@ -54,7 +54,7 @@ namespace ExportDXF.Services
try
{
var fileName = GetSinglePartFileName(model, context.FilePrefix);
var fileName = GetSinglePartFileName(model);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
// Build result item with metadata
@@ -139,7 +139,7 @@ namespace ExportDXF.Services
EnrichItemWithMetadata(item, model, part);
var fileName = GetItemFileName(item, context.FilePrefix);
var fileName = GetItemFileName(item);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
var templateDrawing = context.GetOrCreateTemplateDrawing();
@@ -307,6 +307,7 @@ namespace ExportDXF.Services
{
var etcher = new EtchBendLines.Etcher();
etcher.AddEtchLines(dxfPath);
FixDegreeSymbol(dxfPath);
}
catch (Exception)
{
@@ -314,7 +315,24 @@ namespace ExportDXF.Services
}
}
private string GetSinglePartFileName(ModelDoc2 model, string prefix)
/// <summary>
/// Workaround for ACadSharp encoding bug (no upstream fix as of v3.4.9).
/// ACadSharp's DxfReader uses $DWGCODEPAGE (ANSI_1252) to decode text, but
/// AC1018+ DXF files use UTF-8. The degree symbol ° (UTF-8: C2 B0) gets
/// misread as two ANSI_1252 characters: Â (C2) and ° (B0).
/// See: https://github.com/DomCR/ACadSharp/issues?q=encoding
/// </summary>
private static void FixDegreeSymbol(string path)
{
var text = System.IO.File.ReadAllText(path);
if (text.Contains("\u00C2\u00B0"))
{
text = text.Replace("\u00C2\u00B0", "\u00B0");
System.IO.File.WriteAllText(path, text);
}
}
private string GetSinglePartFileName(ModelDoc2 model)
{
var title = model.GetTitle().Replace(".SLDPRT", "");
var config = model.ConfigurationManager.ActiveConfiguration.Name;
@@ -323,17 +341,13 @@ namespace ExportDXF.Services
return isDefaultConfig ? title : $"{title} [{config}]";
}
private string GetItemFileName(Item item, string prefix)
private string GetItemFileName(Item item)
{
if (string.IsNullOrWhiteSpace(item.ItemNo))
return item.PartName;
return item.PartName ?? "unknown";
prefix = prefix?.Replace("\"", "''") ?? string.Empty;
var num = item.ItemNo.PadLeft(2, '0');
// Expected format: {DrawingNo} PT{ItemNo}
return string.IsNullOrWhiteSpace(prefix)
? $"PT{num}"
: $"{prefix} PT{num}";
return $"PT{num}";
}
private void LogExportFailure(Item item, ExportContext context)

View File

@@ -0,0 +1,48 @@
using System.Collections.Generic;
using SolidWorks.Interop.sldworks;
namespace ExportDXF.Services
{
/// <summary>
/// Reads all visible columns and rows from a SolidWorks BOM table annotation
/// as raw string data for direct copy into an Excel sheet.
/// </summary>
public static class RawBomTableReader
{
public static List<Dictionary<string, string>> Read(BomTableAnnotation bomTable)
{
var table = (TableAnnotation)bomTable;
var rows = new List<Dictionary<string, string>>();
int colCount = table.ColumnCount;
int rowCount = table.RowCount;
// Build visible column headers
var columns = new List<(int Index, string Header)>();
for (int col = 0; col < colCount; col++)
{
if (table.ColumnHidden[col])
continue;
var header = table.get_Text(0, col)?.Trim() ?? $"Column{col}";
columns.Add((col, header));
}
// Read data rows (skip header row 0, skip hidden rows)
for (int row = 1; row < rowCount; row++)
{
if (table.RowHidden[row])
continue;
var rowData = new Dictionary<string, string>();
foreach (var (colIdx, header) in columns)
{
rowData[header] = table.get_Text(row, colIdx)?.Trim() ?? "";
}
rows.Add(rowData);
}
return rows;
}
}
}

View File

@@ -1,10 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
</startup>
<appSettings>
<add key="MaxBendRadius" value="2.0"/>
<add key="FabWorksApiUrl" value="http://localhost:5206"/>
<add key="DefaultSuffix" value="PT{item_no:2}"/>
</appSettings>
</configuration>

View File

@@ -1,9 +0,0 @@
namespace FabWorks.Api.Configuration
{
public class FileStorageOptions
{
public const string SectionName = "FileStorage";
public string OutputFolder { get; set; } = @"C:\ExportDXF\Output";
}
}

View File

@@ -1,259 +0,0 @@
using FabWorks.Api.DTOs;
using FabWorks.Core.Data;
using FabWorks.Core.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FabWorks.Api.Controllers
{
[ApiController]
[Route("api/exports/{exportId}/bom-items")]
public class BomItemsController : ControllerBase
{
private readonly FabWorksDbContext _db;
public BomItemsController(FabWorksDbContext db) => _db = db;
[HttpGet("find")]
public async Task<ActionResult<BomItemDto>> FindExisting(int exportId, [FromQuery] string partName, [FromQuery] string configurationName)
{
var export = await _db.ExportRecords.FindAsync(exportId);
if (export == null) return NotFound();
var existing = await _db.BomItems
.Include(b => b.CutTemplate)
.Include(b => b.FormProgram)
.Include(b => b.ExportRecord)
.Where(b => b.ExportRecord.DrawingNumber == export.DrawingNumber
&& b.PartName == (partName ?? "")
&& b.ConfigurationName == (configurationName ?? ""))
.OrderByDescending(b => b.ID)
.FirstOrDefaultAsync();
if (existing == null) return NotFound();
return MapToDto(existing);
}
[HttpGet]
public async Task<ActionResult<List<BomItemDto>>> GetByExport(int exportId)
{
var items = await _db.BomItems
.Include(b => b.CutTemplate)
.Include(b => b.FormProgram)
.Where(b => b.ExportRecordId == exportId)
.OrderBy(b => b.SortOrder)
.ToListAsync();
return items.Select(MapToDto).ToList();
}
[HttpPost]
public async Task<ActionResult<BomItemDto>> Create(int exportId, BomItemDto dto)
{
var export = await _db.ExportRecords.FindAsync(exportId);
if (export == null) return NotFound("Export record not found");
// Look up the latest CutTemplate for this drawing+item across all previous exports
// to determine the revision number
var newContentHash = dto.CutTemplate?.ContentHash;
int revision = await ResolveRevisionAsync(export.DrawingNumber, dto.ItemNo, newContentHash);
// Look for existing BomItem with same PartName + ConfigurationName within this export record
var existing = await _db.BomItems
.Include(b => b.CutTemplate)
.Include(b => b.FormProgram)
.Where(b => b.ExportRecordId == exportId
&& b.PartName == (dto.PartName ?? "")
&& b.ConfigurationName == (dto.ConfigurationName ?? ""))
.OrderByDescending(b => b.ID)
.FirstOrDefaultAsync();
if (existing != null)
{
// Update existing fields
existing.PartNo = dto.PartNo ?? "";
existing.SortOrder = dto.SortOrder;
existing.Qty = dto.Qty;
existing.TotalQty = dto.TotalQty;
existing.Description = dto.Description ?? "";
existing.Material = dto.Material ?? "";
if (dto.CutTemplate != null)
{
if (existing.CutTemplate != null)
{
existing.CutTemplate.DxfFilePath = dto.CutTemplate.DxfFilePath ?? "";
existing.CutTemplate.ContentHash = dto.CutTemplate.ContentHash;
existing.CutTemplate.Revision = revision;
existing.CutTemplate.Thickness = dto.CutTemplate.Thickness;
existing.CutTemplate.KFactor = dto.CutTemplate.KFactor;
existing.CutTemplate.DefaultBendRadius = dto.CutTemplate.DefaultBendRadius;
}
else
{
existing.CutTemplate = new CutTemplate
{
DxfFilePath = dto.CutTemplate.DxfFilePath ?? "",
ContentHash = dto.CutTemplate.ContentHash,
Revision = revision,
Thickness = dto.CutTemplate.Thickness,
KFactor = dto.CutTemplate.KFactor,
DefaultBendRadius = dto.CutTemplate.DefaultBendRadius
};
}
}
if (dto.FormProgram != null)
{
if (existing.FormProgram != null)
{
existing.FormProgram.ProgramFilePath = dto.FormProgram.ProgramFilePath ?? "";
existing.FormProgram.ContentHash = dto.FormProgram.ContentHash;
existing.FormProgram.ProgramName = dto.FormProgram.ProgramName ?? "";
existing.FormProgram.Thickness = dto.FormProgram.Thickness;
existing.FormProgram.MaterialType = dto.FormProgram.MaterialType ?? "";
existing.FormProgram.KFactor = dto.FormProgram.KFactor;
existing.FormProgram.BendCount = dto.FormProgram.BendCount;
existing.FormProgram.UpperToolNames = dto.FormProgram.UpperToolNames ?? "";
existing.FormProgram.LowerToolNames = dto.FormProgram.LowerToolNames ?? "";
existing.FormProgram.SetupNotes = dto.FormProgram.SetupNotes ?? "";
}
else
{
existing.FormProgram = new FormProgram
{
ProgramFilePath = dto.FormProgram.ProgramFilePath ?? "",
ContentHash = dto.FormProgram.ContentHash,
ProgramName = dto.FormProgram.ProgramName ?? "",
Thickness = dto.FormProgram.Thickness,
MaterialType = dto.FormProgram.MaterialType ?? "",
KFactor = dto.FormProgram.KFactor,
BendCount = dto.FormProgram.BendCount,
UpperToolNames = dto.FormProgram.UpperToolNames ?? "",
LowerToolNames = dto.FormProgram.LowerToolNames ?? "",
SetupNotes = dto.FormProgram.SetupNotes ?? ""
};
}
}
await _db.SaveChangesAsync();
return Ok(MapToDto(existing));
}
// No existing match — create new
var item = new BomItem
{
ExportRecordId = exportId,
ItemNo = dto.ItemNo ?? "",
PartNo = dto.PartNo ?? "",
SortOrder = dto.SortOrder,
Qty = dto.Qty,
TotalQty = dto.TotalQty,
Description = dto.Description ?? "",
PartName = dto.PartName ?? "",
ConfigurationName = dto.ConfigurationName ?? "",
Material = dto.Material ?? ""
};
if (dto.CutTemplate != null)
{
item.CutTemplate = new CutTemplate
{
DxfFilePath = dto.CutTemplate.DxfFilePath ?? "",
ContentHash = dto.CutTemplate.ContentHash,
Revision = revision,
Thickness = dto.CutTemplate.Thickness,
KFactor = dto.CutTemplate.KFactor,
DefaultBendRadius = dto.CutTemplate.DefaultBendRadius
};
}
if (dto.FormProgram != null)
{
item.FormProgram = new FormProgram
{
ProgramFilePath = dto.FormProgram.ProgramFilePath ?? "",
ContentHash = dto.FormProgram.ContentHash,
ProgramName = dto.FormProgram.ProgramName ?? "",
Thickness = dto.FormProgram.Thickness,
MaterialType = dto.FormProgram.MaterialType ?? "",
KFactor = dto.FormProgram.KFactor,
BendCount = dto.FormProgram.BendCount,
UpperToolNames = dto.FormProgram.UpperToolNames ?? "",
LowerToolNames = dto.FormProgram.LowerToolNames ?? "",
SetupNotes = dto.FormProgram.SetupNotes ?? ""
};
}
_db.BomItems.Add(item);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetByExport), new { exportId }, MapToDto(item));
}
/// <summary>
/// Determines the revision number for a CutTemplate by looking at the most recent
/// CutTemplate for the same drawing number and item number across all exports.
/// Returns 1 if no previous version exists, the same revision if the hash matches,
/// or previous revision + 1 if the hash changed.
/// </summary>
private async Task<int> ResolveRevisionAsync(string drawingNumber, string itemNo, string contentHash)
{
if (string.IsNullOrEmpty(drawingNumber) || string.IsNullOrEmpty(itemNo) || string.IsNullOrEmpty(contentHash))
return 1;
var previous = await _db.CutTemplates
.Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber
&& c.BomItem.ItemNo == itemNo
&& c.ContentHash != null)
.OrderByDescending(c => c.Id)
.Select(c => new { c.ContentHash, c.Revision })
.FirstOrDefaultAsync();
if (previous == null)
return 1;
return previous.ContentHash == contentHash
? previous.Revision
: previous.Revision + 1;
}
private static BomItemDto MapToDto(BomItem b) => new()
{
ID = b.ID,
ItemNo = b.ItemNo,
PartNo = b.PartNo,
SortOrder = b.SortOrder,
Qty = b.Qty,
TotalQty = b.TotalQty,
Description = b.Description,
PartName = b.PartName,
ConfigurationName = b.ConfigurationName,
Material = b.Material,
CutTemplate = b.CutTemplate == null ? null : new CutTemplateDto
{
Id = b.CutTemplate.Id,
DxfFilePath = b.CutTemplate.DxfFilePath,
ContentHash = b.CutTemplate.ContentHash,
Revision = b.CutTemplate.Revision,
Thickness = b.CutTemplate.Thickness,
KFactor = b.CutTemplate.KFactor,
DefaultBendRadius = b.CutTemplate.DefaultBendRadius
},
FormProgram = b.FormProgram == null ? null : new FormProgramDto
{
Id = b.FormProgram.Id,
ProgramFilePath = b.FormProgram.ProgramFilePath,
ContentHash = b.FormProgram.ContentHash,
ProgramName = b.FormProgram.ProgramName,
Thickness = b.FormProgram.Thickness,
MaterialType = b.FormProgram.MaterialType,
KFactor = b.FormProgram.KFactor,
BendCount = b.FormProgram.BendCount,
UpperToolNames = b.FormProgram.UpperToolNames,
LowerToolNames = b.FormProgram.LowerToolNames,
SetupNotes = b.FormProgram.SetupNotes
}
};
}
}

View File

@@ -1,350 +0,0 @@
using System.IO.Compression;
using FabWorks.Api.DTOs;
using FabWorks.Api.Services;
using FabWorks.Core.Data;
using FabWorks.Core.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FabWorks.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ExportsController : ControllerBase
{
private readonly FabWorksDbContext _db;
private readonly IFileStorageService _fileStorage;
public ExportsController(FabWorksDbContext db, IFileStorageService fileStorage)
{
_db = db;
_fileStorage = fileStorage;
}
[HttpGet]
public async Task<ActionResult<object>> List(
[FromQuery] string search = null,
[FromQuery] int skip = 0,
[FromQuery] int take = 50)
{
var query = _db.ExportRecords
.Include(r => r.BomItems)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim().ToLower();
query = query.Where(r =>
r.DrawingNumber.ToLower().Contains(term) ||
(r.Title != null && r.Title.ToLower().Contains(term)) ||
r.ExportedBy.ToLower().Contains(term) ||
r.BomItems.Any(b => b.PartName.ToLower().Contains(term) ||
b.Description.ToLower().Contains(term)));
}
var total = await query.CountAsync();
var records = await query
.OrderByDescending(r => r.ExportedAt)
.Skip(skip)
.Take(take)
.ToListAsync();
return new
{
total,
items = records.Select(r => new
{
r.Id,
r.DrawingNumber,
r.Title,
r.SourceFilePath,
r.ExportedAt,
r.ExportedBy,
BomItemCount = r.BomItems?.Count ?? 0
})
};
}
[HttpPost]
public async Task<ActionResult<ExportDetailDto>> Create(CreateExportRequest request)
{
var record = new ExportRecord
{
DrawingNumber = request.DrawingNumber,
Title = request.Title,
EquipmentNo = request.EquipmentNo,
DrawingNo = request.DrawingNo,
SourceFilePath = request.SourceFilePath,
OutputFolder = request.OutputFolder,
ExportedAt = DateTime.Now,
ExportedBy = Environment.UserName
};
_db.ExportRecords.Add(record);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = record.Id }, MapToDto(record));
}
[HttpGet("{id}")]
public async Task<ActionResult<ExportDetailDto>> GetById(int id)
{
var record = await _db.ExportRecords
.Include(r => r.BomItems).ThenInclude(b => b.CutTemplate)
.Include(r => r.BomItems).ThenInclude(b => b.FormProgram)
.FirstOrDefaultAsync(r => r.Id == id);
if (record == null) return NotFound();
return MapToDto(record);
}
[HttpGet("by-source")]
public async Task<ActionResult<ExportDetailDto>> GetBySourceFile([FromQuery] string path)
{
var record = await _db.ExportRecords
.Where(r => r.SourceFilePath.ToLower() == path.ToLower()
&& !string.IsNullOrEmpty(r.DrawingNumber))
.OrderByDescending(r => r.Id)
.FirstOrDefaultAsync();
if (record == null) return NotFound();
return MapToDto(record);
}
[HttpGet("by-drawing")]
public async Task<ActionResult<List<ExportDetailDto>>> GetByDrawing([FromQuery] string drawingNumber)
{
var records = await _db.ExportRecords
.Include(r => r.BomItems).ThenInclude(b => b.CutTemplate)
.Include(r => r.BomItems).ThenInclude(b => b.FormProgram)
.Where(r => r.DrawingNumber == drawingNumber)
.OrderByDescending(r => r.ExportedAt)
.ToListAsync();
return records.Select(MapToDto).ToList();
}
[HttpGet("next-item-number")]
public async Task<ActionResult<string>> GetNextItemNumber([FromQuery] string drawingNumber)
{
if (string.IsNullOrEmpty(drawingNumber)) return "1";
var existingItems = await _db.ExportRecords
.Where(r => r.DrawingNumber == drawingNumber)
.SelectMany(r => r.BomItems)
.Select(b => b.ItemNo)
.ToListAsync();
int maxNum = 0;
foreach (var itemNo in existingItems)
{
if (int.TryParse(itemNo, out var num) && num > maxNum)
maxNum = num;
}
return (maxNum + 1).ToString();
}
[HttpGet("drawing-numbers")]
public async Task<ActionResult<List<string>>> GetDrawingNumbers()
{
var numbers = await _db.ExportRecords
.Select(r => r.DrawingNumber)
.Where(d => !string.IsNullOrEmpty(d))
.Distinct()
.ToListAsync();
return numbers;
}
[HttpGet("equipment-numbers")]
public async Task<ActionResult<List<string>>> GetEquipmentNumbers()
{
var numbers = await _db.ExportRecords
.Select(r => r.EquipmentNo)
.Where(e => !string.IsNullOrEmpty(e))
.Distinct()
.OrderBy(e => e)
.ToListAsync();
return numbers;
}
[HttpGet("drawing-numbers-by-equipment")]
public async Task<ActionResult<List<string>>> GetDrawingNumbersByEquipment([FromQuery] string equipmentNo)
{
var query = _db.ExportRecords
.Where(r => !string.IsNullOrEmpty(r.DrawingNo));
if (!string.IsNullOrEmpty(equipmentNo))
query = query.Where(r => r.EquipmentNo == equipmentNo);
var numbers = await query
.Select(r => r.DrawingNo)
.Distinct()
.OrderBy(d => d)
.ToListAsync();
return numbers;
}
[HttpGet("previous-pdf-hash")]
public async Task<ActionResult<string>> GetPreviousPdfHash(
[FromQuery] string drawingNumber,
[FromQuery] int? excludeId = null)
{
var hash = await _db.ExportRecords
.Where(r => r.DrawingNumber == drawingNumber
&& r.PdfContentHash != null
&& (excludeId == null || r.Id != excludeId))
.OrderByDescending(r => r.Id)
.Select(r => r.PdfContentHash)
.FirstOrDefaultAsync();
if (hash == null) return NotFound();
return hash;
}
[HttpPatch("{id}/pdf-hash")]
public async Task<IActionResult> UpdatePdfHash(int id, [FromBody] UpdatePdfHashRequest request)
{
var record = await _db.ExportRecords.FindAsync(id);
if (record == null) return NotFound();
record.PdfContentHash = request.PdfContentHash;
await _db.SaveChangesAsync();
return NoContent();
}
[HttpGet("previous-cut-template")]
public async Task<ActionResult<CutTemplateDto>> GetPreviousCutTemplate(
[FromQuery] string drawingNumber,
[FromQuery] string itemNo)
{
if (string.IsNullOrEmpty(drawingNumber) || string.IsNullOrEmpty(itemNo))
return BadRequest("drawingNumber and itemNo are required.");
var ct = await _db.CutTemplates
.Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber
&& c.BomItem.ItemNo == itemNo
&& c.ContentHash != null)
.OrderByDescending(c => c.Id)
.FirstOrDefaultAsync();
if (ct == null) return NotFound();
return new CutTemplateDto
{
Id = ct.Id,
DxfFilePath = ct.DxfFilePath,
ContentHash = ct.ContentHash,
Revision = ct.Revision,
Thickness = ct.Thickness,
KFactor = ct.KFactor,
DefaultBendRadius = ct.DefaultBendRadius
};
}
[HttpGet("{id}/download-dxfs")]
public async Task<IActionResult> DownloadAllDxfs(int id)
{
var record = await _db.ExportRecords
.Include(r => r.BomItems).ThenInclude(b => b.CutTemplate)
.FirstOrDefaultAsync(r => r.Id == id);
if (record == null) return NotFound();
var dxfItems = record.BomItems
.Where(b => b.CutTemplate?.ContentHash != null)
.ToList();
if (dxfItems.Count == 0) return NotFound("No DXF files for this export.");
var ms = new MemoryStream();
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
{
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var b in dxfItems)
{
var ct = b.CutTemplate;
var fileName = ct.DxfFilePath?.Split(new[] { '/', '\\' }).LastOrDefault()
?? $"PT{(b.ItemNo ?? "").PadLeft(2, '0')}.dxf";
// Ensure unique names in zip
if (!usedNames.Add(fileName))
{
var baseName = Path.GetFileNameWithoutExtension(fileName);
var ext = Path.GetExtension(fileName);
var counter = 2;
do { fileName = $"{baseName}_{counter++}{ext}"; }
while (!usedNames.Add(fileName));
}
var blobStream = _fileStorage.OpenBlob(ct.ContentHash, "dxf");
if (blobStream == null) continue;
var entry = zip.CreateEntry(fileName, CompressionLevel.Fastest);
using var entryStream = entry.Open();
await blobStream.CopyToAsync(entryStream);
blobStream.Dispose();
}
}
ms.Position = 0;
var zipName = $"{record.DrawingNumber ?? $"Export-{id}"} DXFs.zip";
return File(ms, "application/zip", zipName);
}
private static ExportDetailDto MapToDto(ExportRecord r) => new()
{
Id = r.Id,
DrawingNumber = r.DrawingNumber,
Title = r.Title,
EquipmentNo = r.EquipmentNo,
DrawingNo = r.DrawingNo,
SourceFilePath = r.SourceFilePath,
OutputFolder = r.OutputFolder,
ExportedAt = r.ExportedAt,
ExportedBy = r.ExportedBy,
PdfContentHash = r.PdfContentHash,
BomItems = r.BomItems?.Select(b => new BomItemDto
{
ID = b.ID,
ItemNo = b.ItemNo,
PartNo = b.PartNo,
SortOrder = b.SortOrder,
Qty = b.Qty,
TotalQty = b.TotalQty,
Description = b.Description,
PartName = b.PartName,
ConfigurationName = b.ConfigurationName,
Material = b.Material,
CutTemplate = b.CutTemplate == null ? null : new CutTemplateDto
{
Id = b.CutTemplate.Id,
DxfFilePath = b.CutTemplate.DxfFilePath,
ContentHash = b.CutTemplate.ContentHash,
Revision = b.CutTemplate.Revision,
Thickness = b.CutTemplate.Thickness,
KFactor = b.CutTemplate.KFactor,
DefaultBendRadius = b.CutTemplate.DefaultBendRadius
},
FormProgram = b.FormProgram == null ? null : new FormProgramDto
{
Id = b.FormProgram.Id,
ProgramFilePath = b.FormProgram.ProgramFilePath,
ContentHash = b.FormProgram.ContentHash,
ProgramName = b.FormProgram.ProgramName,
Thickness = b.FormProgram.Thickness,
MaterialType = b.FormProgram.MaterialType,
KFactor = b.FormProgram.KFactor,
BendCount = b.FormProgram.BendCount,
UpperToolNames = b.FormProgram.UpperToolNames,
LowerToolNames = b.FormProgram.LowerToolNames,
SetupNotes = b.FormProgram.SetupNotes
}
}).ToList() ?? new()
};
}
}

View File

@@ -1,184 +0,0 @@
using FabWorks.Api.Services;
using FabWorks.Core.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
namespace FabWorks.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class FileBrowserController : ControllerBase
{
private readonly IFileStorageService _fileStorage;
private readonly FabWorksDbContext _db;
private readonly FileExtensionContentTypeProvider _contentTypeProvider = new();
public FileBrowserController(IFileStorageService fileStorage, FabWorksDbContext db)
{
_fileStorage = fileStorage;
_db = db;
}
[HttpGet("files")]
public async Task<ActionResult<FileListResult>> ListFiles(
[FromQuery] string search = null,
[FromQuery] string type = null)
{
var files = new List<StoredFileEntry>();
// Query DXF files from CutTemplates
if (type == null || type.Equals("dxf", StringComparison.OrdinalIgnoreCase))
{
var dxfQuery = _db.CutTemplates
.Where(c => c.ContentHash != null)
.Select(c => new
{
c.Id,
c.DxfFilePath,
c.ContentHash,
c.Thickness,
DrawingNumber = c.BomItem.ExportRecord.DrawingNumber,
CreatedAt = c.BomItem.ExportRecord.ExportedAt
});
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim().ToLower();
dxfQuery = dxfQuery.Where(c =>
c.DxfFilePath.ToLower().Contains(term) ||
c.DrawingNumber.ToLower().Contains(term));
}
var dxfResults = await dxfQuery
.OrderByDescending(c => c.CreatedAt)
.Take(500)
.ToListAsync();
// Deduplicate by content hash (keep latest)
var seenDxf = new HashSet<string>();
foreach (var c in dxfResults)
{
if (seenDxf.Contains(c.ContentHash)) continue;
seenDxf.Add(c.ContentHash);
var fileName = c.DxfFilePath?.Split(new[] { '/', '\\' }).LastOrDefault() ?? c.DxfFilePath;
files.Add(new StoredFileEntry
{
FileName = fileName,
ContentHash = c.ContentHash,
FileType = "dxf",
DrawingNumber = c.DrawingNumber,
Thickness = c.Thickness,
CreatedAt = c.CreatedAt
});
}
}
// Query PDF files from ExportRecords
if (type == null || type.Equals("pdf", StringComparison.OrdinalIgnoreCase))
{
var pdfQuery = _db.ExportRecords
.Where(r => r.PdfContentHash != null)
.Select(r => new
{
r.Id,
r.DrawingNumber,
r.PdfContentHash,
r.ExportedAt
});
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim().ToLower();
pdfQuery = pdfQuery.Where(r =>
r.DrawingNumber.ToLower().Contains(term));
}
var pdfResults = await pdfQuery
.OrderByDescending(r => r.ExportedAt)
.Take(500)
.ToListAsync();
// Deduplicate by content hash
var seenPdf = new HashSet<string>();
foreach (var r in pdfResults)
{
if (seenPdf.Contains(r.PdfContentHash)) continue;
seenPdf.Add(r.PdfContentHash);
files.Add(new StoredFileEntry
{
FileName = $"{r.DrawingNumber}.pdf",
ContentHash = r.PdfContentHash,
FileType = "pdf",
DrawingNumber = r.DrawingNumber,
CreatedAt = r.ExportedAt
});
}
}
return new FileListResult
{
Total = files.Count,
Files = files.OrderByDescending(f => f.CreatedAt).ToList()
};
}
[HttpGet("preview")]
public IActionResult PreviewFile([FromQuery] string hash, [FromQuery] string ext = "dxf")
{
if (string.IsNullOrEmpty(hash) || hash.Length < 4)
return BadRequest("Invalid hash.");
if (!_fileStorage.BlobExists(hash, ext))
return NotFound("File not found.");
var stream = _fileStorage.OpenBlob(hash, ext);
if (stream == null)
return NotFound("File not found.");
var virtualName = $"file.{ext}";
if (!_contentTypeProvider.TryGetContentType(virtualName, out var contentType))
contentType = "application/octet-stream";
return File(stream, contentType);
}
[HttpGet("download")]
public IActionResult DownloadFile([FromQuery] string hash, [FromQuery] string ext = "dxf", [FromQuery] string name = null)
{
if (string.IsNullOrEmpty(hash) || hash.Length < 4)
return BadRequest("Invalid hash.");
if (!_fileStorage.BlobExists(hash, ext))
return NotFound("File not found.");
var stream = _fileStorage.OpenBlob(hash, ext);
if (stream == null)
return NotFound("File not found.");
var fileName = name ?? $"{hash[..8]}.{ext}";
if (!_contentTypeProvider.TryGetContentType(fileName, out var contentType))
contentType = "application/octet-stream";
return File(stream, contentType, fileName);
}
}
public class FileListResult
{
public int Total { get; set; }
public List<StoredFileEntry> Files { get; set; }
}
public class StoredFileEntry
{
public string FileName { get; set; }
public string ContentHash { get; set; }
public string FileType { get; set; }
public string DrawingNumber { get; set; }
public double? Thickness { get; set; }
public DateTime CreatedAt { get; set; }
}
}

View File

@@ -1,93 +0,0 @@
using FabWorks.Api.DTOs;
using FabWorks.Api.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
namespace FabWorks.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
private readonly IFileStorageService _fileStorage;
private readonly FileExtensionContentTypeProvider _contentTypeProvider = new();
public FilesController(IFileStorageService fileStorage)
{
_fileStorage = fileStorage;
}
[HttpPost("dxf")]
[RequestSizeLimit(50_000_000)] // 50 MB
public async Task<ActionResult<FileUploadResponse>> UploadDxf(
IFormFile file,
[FromForm] string equipment,
[FromForm] string drawingNo,
[FromForm] string itemNo,
[FromForm] string contentHash)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded.");
using var stream = file.OpenReadStream();
var result = await _fileStorage.StoreDxfAsync(stream, equipment, drawingNo, itemNo, contentHash);
return Ok(new FileUploadResponse
{
StoredFilePath = result.FileName,
ContentHash = result.ContentHash,
FileName = result.FileName,
WasUnchanged = result.WasUnchanged,
IsNewFile = result.IsNewFile
});
}
[HttpPost("pdf")]
[RequestSizeLimit(100_000_000)] // 100 MB
public async Task<ActionResult<FileUploadResponse>> UploadPdf(
IFormFile file,
[FromForm] string equipment,
[FromForm] string drawingNo,
[FromForm] string contentHash,
[FromForm] int? exportRecordId = null)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded.");
using var stream = file.OpenReadStream();
var result = await _fileStorage.StorePdfAsync(stream, equipment, drawingNo, contentHash, exportRecordId);
return Ok(new FileUploadResponse
{
StoredFilePath = result.FileName,
ContentHash = result.ContentHash,
FileName = result.FileName,
WasUnchanged = result.WasUnchanged,
IsNewFile = result.IsNewFile
});
}
[HttpGet("blob/{hash}")]
public IActionResult GetBlob(string hash, [FromQuery] string ext = "dxf", [FromQuery] bool download = false, [FromQuery] string name = null)
{
if (string.IsNullOrEmpty(hash) || hash.Length < 4)
return BadRequest("Invalid hash.");
if (!_fileStorage.BlobExists(hash, ext))
return NotFound("Blob not found.");
var stream = _fileStorage.OpenBlob(hash, ext);
if (stream == null)
return NotFound("Blob not found.");
var fileName = !string.IsNullOrEmpty(name) ? name : $"{hash[..8]}.{ext}";
if (!_contentTypeProvider.TryGetContentType(fileName, out var contentType))
contentType = "application/octet-stream";
if (download)
return File(stream, contentType, fileName);
return File(stream, contentType);
}
}
}

View File

@@ -1,106 +0,0 @@
using FabWorks.Api.DTOs;
using FabWorks.Api.Services;
using FabWorks.Core.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FabWorks.Api.Controllers
{
[ApiController]
[Route("api/form-programs")]
public class FormProgramsController : ControllerBase
{
private readonly FabWorksDbContext _db;
private readonly FormProgramService _formService;
public FormProgramsController(FabWorksDbContext db, FormProgramService formService)
{
_db = db;
_formService = formService;
}
[HttpGet("by-drawing")]
public async Task<ActionResult<List<FormProgramDto>>> GetByDrawing([FromQuery] string drawingNumber)
{
var programs = await _db.FormPrograms
.Include(fp => fp.BomItem)
.ThenInclude(b => b.ExportRecord)
.Where(fp => fp.BomItem.ExportRecord.DrawingNumber == drawingNumber)
.ToListAsync();
return programs.Select(fp => new FormProgramDto
{
Id = fp.Id,
ProgramFilePath = fp.ProgramFilePath,
ContentHash = fp.ContentHash,
ProgramName = fp.ProgramName,
Thickness = fp.Thickness,
MaterialType = fp.MaterialType,
KFactor = fp.KFactor,
BendCount = fp.BendCount,
UpperToolNames = fp.UpperToolNames,
LowerToolNames = fp.LowerToolNames,
SetupNotes = fp.SetupNotes
}).ToList();
}
[HttpPost("parse")]
public ActionResult<FormProgramDto> Parse([FromQuery] string filePath)
{
if (!System.IO.File.Exists(filePath))
return NotFound($"File not found: {filePath}");
var fp = _formService.ParseFromFile(filePath);
return new FormProgramDto
{
ProgramFilePath = fp.ProgramFilePath,
ContentHash = fp.ContentHash,
ProgramName = fp.ProgramName,
Thickness = fp.Thickness,
MaterialType = fp.MaterialType,
KFactor = fp.KFactor,
BendCount = fp.BendCount,
UpperToolNames = fp.UpperToolNames,
LowerToolNames = fp.LowerToolNames,
SetupNotes = fp.SetupNotes
};
}
[HttpPost("{bomItemId}")]
public async Task<ActionResult<FormProgramDto>> AttachToItem(int bomItemId, [FromQuery] string filePath)
{
var bomItem = await _db.BomItems
.Include(b => b.FormProgram)
.FirstOrDefaultAsync(b => b.ID == bomItemId);
if (bomItem == null) return NotFound("BOM item not found");
if (!System.IO.File.Exists(filePath))
return NotFound($"File not found: {filePath}");
var fp = _formService.ParseFromFile(filePath);
fp.BomItemId = bomItemId;
if (bomItem.FormProgram != null)
_db.FormPrograms.Remove(bomItem.FormProgram);
bomItem.FormProgram = fp;
await _db.SaveChangesAsync();
return new FormProgramDto
{
Id = fp.Id,
ProgramFilePath = fp.ProgramFilePath,
ContentHash = fp.ContentHash,
ProgramName = fp.ProgramName,
Thickness = fp.Thickness,
MaterialType = fp.MaterialType,
KFactor = fp.KFactor,
BendCount = fp.BendCount,
UpperToolNames = fp.UpperToolNames,
LowerToolNames = fp.LowerToolNames,
SetupNotes = fp.SetupNotes
};
}
}
}

View File

@@ -1,17 +0,0 @@
namespace FabWorks.Api.DTOs
{
public class CreateExportRequest
{
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
}
public class UpdatePdfHashRequest
{
public string PdfContentHash { get; set; }
}
}

View File

@@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
namespace FabWorks.Api.DTOs
{
public class ExportDetailDto
{
public int Id { get; set; }
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
public DateTime ExportedAt { get; set; }
public string ExportedBy { get; set; }
public string PdfContentHash { get; set; }
public List<BomItemDto> BomItems { get; set; } = new();
}
public class BomItemDto
{
public int ID { get; set; }
public string ItemNo { get; set; }
public string PartNo { get; set; }
public int SortOrder { get; set; }
public int? Qty { get; set; }
public int? TotalQty { get; set; }
public string Description { get; set; }
public string PartName { get; set; }
public string ConfigurationName { get; set; }
public string Material { get; set; }
public CutTemplateDto CutTemplate { get; set; }
public FormProgramDto FormProgram { get; set; }
}
public class CutTemplateDto
{
public int Id { get; set; }
public string DxfFilePath { get; set; }
public string ContentHash { get; set; }
public int Revision { get; set; }
public double? Thickness { get; set; }
public double? KFactor { get; set; }
public double? DefaultBendRadius { get; set; }
}
public class FormProgramDto
{
public int Id { get; set; }
public string ProgramFilePath { get; set; }
public string ContentHash { get; set; }
public string ProgramName { get; set; }
public double? Thickness { get; set; }
public string MaterialType { get; set; }
public double? KFactor { get; set; }
public int BendCount { get; set; }
public string UpperToolNames { get; set; }
public string LowerToolNames { get; set; }
public string SetupNotes { get; set; }
}
}

View File

@@ -1,11 +0,0 @@
namespace FabWorks.Api.DTOs
{
public class FileUploadResponse
{
public string StoredFilePath { get; set; } // kept for client compat, contains logical filename
public string ContentHash { get; set; }
public string FileName { get; set; }
public bool WasUnchanged { get; set; }
public bool IsNewFile { get; set; }
}
}

View File

@@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\FabWorks.Core\FabWorks.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,22 +0,0 @@
using FabWorks.Api.Configuration;
using FabWorks.Api.Services;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<FabWorksDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("FabWorksDb")));
builder.Services.AddSingleton<FormProgramService>();
builder.Services.Configure<FileStorageOptions>(
builder.Configuration.GetSection(FileStorageOptions.SectionName));
builder.Services.AddScoped<IFileStorageService, FileStorageService>();
var app = builder.Build();
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapControllers();
app.Run();

View File

@@ -1,41 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:45483",
"sslPort": 44397
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "",
"applicationUrl": "http://localhost:5206",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "",
"applicationUrl": "https://localhost:7182;http://localhost:5206",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,159 +0,0 @@
using FabWorks.Api.Configuration;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace FabWorks.Api.Services
{
public class FileUploadResult
{
public string ContentHash { get; set; }
public string FileName { get; set; }
public bool WasUnchanged { get; set; }
public bool IsNewFile { get; set; }
}
public interface IFileStorageService
{
string OutputFolder { get; }
Task<FileUploadResult> StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash);
Task<FileUploadResult> StorePdfAsync(Stream stream, string equipment, string drawingNo, string contentHash, int? exportRecordId = null);
Stream OpenBlob(string contentHash, string extension);
bool BlobExists(string contentHash, string extension);
}
public class FileStorageService : IFileStorageService
{
private readonly FileStorageOptions _options;
private readonly FabWorksDbContext _db;
public string OutputFolder => _options.OutputFolder;
public FileStorageService(IOptions<FileStorageOptions> options, FabWorksDbContext db)
{
_options = options.Value;
_db = db;
var blobRoot = Path.Combine(_options.OutputFolder, "blobs");
if (!Directory.Exists(blobRoot))
Directory.CreateDirectory(blobRoot);
}
public async Task<FileUploadResult> StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash)
{
var fileName = BuildDxfFileName(drawingNo, equipment, itemNo);
// Look up previous hash by drawing number + item number
var drawingNumber = BuildDrawingNumber(equipment, drawingNo);
var previousHash = await _db.CutTemplates
.Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber
&& c.BomItem.ItemNo == itemNo
&& c.ContentHash != null)
.OrderByDescending(c => c.Id)
.Select(c => c.ContentHash)
.FirstOrDefaultAsync();
var wasUnchanged = previousHash != null && previousHash == contentHash;
var isNewFile = await StoreBlobAsync(stream, contentHash, "dxf");
return new FileUploadResult
{
ContentHash = contentHash,
FileName = fileName,
WasUnchanged = wasUnchanged,
IsNewFile = isNewFile
};
}
public async Task<FileUploadResult> StorePdfAsync(Stream stream, string equipment, string drawingNo, string contentHash, int? exportRecordId = null)
{
var drawingNumber = BuildDrawingNumber(equipment, drawingNo);
var fileName = $"{drawingNumber}.pdf";
// Look up previous PDF hash
var previousHash = await _db.ExportRecords
.Where(r => r.DrawingNumber == drawingNumber
&& r.PdfContentHash != null
&& (exportRecordId == null || r.Id != exportRecordId))
.OrderByDescending(r => r.Id)
.Select(r => r.PdfContentHash)
.FirstOrDefaultAsync();
var wasUnchanged = previousHash != null && previousHash == contentHash;
var isNewFile = await StoreBlobAsync(stream, contentHash, "pdf");
// Update the export record with the PDF content hash
if (exportRecordId.HasValue)
{
var record = await _db.ExportRecords.FindAsync(exportRecordId.Value);
if (record != null)
{
record.PdfContentHash = contentHash;
await _db.SaveChangesAsync();
}
}
return new FileUploadResult
{
ContentHash = contentHash,
FileName = fileName,
WasUnchanged = wasUnchanged,
IsNewFile = isNewFile
};
}
public Stream OpenBlob(string contentHash, string extension)
{
var path = GetBlobPath(contentHash, extension);
if (!File.Exists(path))
return null;
return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
}
public bool BlobExists(string contentHash, string extension)
{
return File.Exists(GetBlobPath(contentHash, extension));
}
private async Task<bool> StoreBlobAsync(Stream stream, string contentHash, string extension)
{
var blobPath = GetBlobPath(contentHash, extension);
if (File.Exists(blobPath))
return false; // blob already exists (dedup)
var dir = Path.GetDirectoryName(blobPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
using var fileStream = new FileStream(blobPath, FileMode.Create, FileAccess.Write);
await stream.CopyToAsync(fileStream);
return true; // new blob written
}
private string GetBlobPath(string contentHash, string extension)
{
var prefix1 = contentHash[..2];
var prefix2 = contentHash[2..4];
return Path.Combine(_options.OutputFolder, "blobs", prefix1, prefix2, $"{contentHash}.{extension}");
}
private static string BuildDrawingNumber(string equipment, string drawingNo)
{
if (!string.IsNullOrEmpty(equipment) && !string.IsNullOrEmpty(drawingNo))
return $"{equipment} {drawingNo}";
if (!string.IsNullOrEmpty(equipment))
return equipment;
return drawingNo ?? "";
}
private static string BuildDxfFileName(string drawingNo, string equipment, string itemNo)
{
var drawingNumber = BuildDrawingNumber(equipment, drawingNo);
var paddedItem = (itemNo ?? "").PadLeft(2, '0');
if (!string.IsNullOrEmpty(drawingNumber) && !string.IsNullOrEmpty(itemNo))
return $"{drawingNumber} PT{paddedItem}.dxf";
return $"PT{paddedItem}.dxf";
}
}
}

View File

@@ -1,39 +0,0 @@
using FabWorks.Core.Models;
using FabWorks.Core.PressBrake;
using System.Security.Cryptography;
namespace FabWorks.Api.Services
{
public class FormProgramService
{
public FormProgram ParseFromFile(string filePath)
{
var pgm = FabWorks.Core.PressBrake.Program.Load(filePath);
var hash = ComputeFileHash(filePath);
return new FormProgram
{
ProgramFilePath = filePath,
ContentHash = hash,
ProgramName = pgm.ProgName ?? "",
Thickness = pgm.MatThick > 0 ? pgm.MatThick : null,
MaterialType = pgm.MatType.ToString(),
KFactor = pgm.KFactor > 0 ? pgm.KFactor : null,
BendCount = pgm.Steps.Count,
UpperToolNames = string.Join(", ", pgm.UpperToolSets
.Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct()),
LowerToolNames = string.Join(", ", pgm.LowerToolSets
.Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct()),
SetupNotes = pgm.SetupNotes ?? ""
};
}
private 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();
}
}
}

View File

@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -1,15 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"FabWorksDb": "Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;"
},
"FileStorage": {
"OutputFolder": "C:\\ExportDXF\\Output"
}
}

View File

@@ -1,733 +0,0 @@
:root {
--bg-deep: #f0f1f3;
--bg: #f8f9fa;
--surface: #ffffff;
--surface-raised: #ffffff;
--border: #d0d5dd;
--border-subtle: #e4e7ec;
--text: #1a1a1a;
--text-secondary: #475467;
--text-dim: #667085;
--cyan: #0975b0;
--cyan-dim: rgba(9, 117, 176, 0.1);
--cyan-glow: rgba(9, 117, 176, 0.2);
--amber: #b54708;
--amber-dim: rgba(181, 71, 8, 0.08);
--green: #067647;
--green-dim: rgba(6, 118, 71, 0.08);
--red: #d92d20;
--sidebar-w: 64px;
--font-display: 'Outfit', sans-serif;
--font-body: 'IBM Plex Sans', sans-serif;
--font-mono: 'IBM Plex Mono', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-body);
background: var(--bg);
color: var(--text);
display: flex;
min-height: 100vh;
overflow-x: hidden;
}
/* ─── Sidebar ─── */
.sidebar {
width: var(--sidebar-w);
background: var(--bg-deep);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
position: fixed;
top: 0; left: 0; bottom: 0;
z-index: 50;
padding-top: 8px;
}
.sidebar-brand {
width: 40px; height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
position: relative;
}
.sidebar-brand::after {
content: '';
position: absolute;
bottom: -12px;
left: 8px; right: 8px;
height: 1px;
background: var(--border);
}
.sidebar-brand svg {
width: 26px; height: 26px;
color: var(--cyan);
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 4px;
padding-top: 16px;
width: 100%;
}
.nav-item {
display: flex;
align-items: center;
justify-content: center;
width: 44px; height: 44px;
margin: 0 auto;
color: var(--text-dim);
text-decoration: none;
cursor: pointer;
border-radius: 8px;
transition: all 0.2s;
position: relative;
}
.nav-item:hover {
color: var(--text-secondary);
background: var(--surface);
}
.nav-item.active {
color: var(--cyan);
background: var(--cyan-dim);
}
.nav-item.active::before {
content: '';
position: absolute;
left: -10px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 20px;
background: var(--cyan);
border-radius: 0 2px 2px 0;
}
.nav-item svg { width: 20px; height: 20px; }
.nav-tooltip {
position: absolute;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%);
background: var(--text);
border: 1px solid var(--border);
color: #fff;
padding: 4px 10px;
border-radius: 4px;
font-size: 13px;
font-family: var(--font-body);
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
z-index: 100;
}
.nav-item:hover .nav-tooltip { opacity: 1; }
/* ─── Main ─── */
.main {
margin-left: var(--sidebar-w);
flex: 1;
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
z-index: 1;
}
.topbar {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 0 32px;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 40;
}
.topbar-left {
display: flex;
align-items: center;
gap: 12px;
}
.topbar h2 {
font-family: var(--font-display);
font-size: 18px;
font-weight: 600;
letter-spacing: -0.01em;
}
.topbar-tag {
font-family: var(--font-mono);
font-size: 12px;
color: var(--cyan);
background: var(--cyan-dim);
padding: 2px 8px;
border-radius: 3px;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.page-content {
padding: 28px 32px;
flex: 1;
}
/* ─── Animations ─── */
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeSlideIn 0.3s ease forwards;
opacity: 0;
}
.animate-in:nth-child(1) { animation-delay: 0.04s; }
.animate-in:nth-child(2) { animation-delay: 0.08s; }
.animate-in:nth-child(3) { animation-delay: 0.12s; }
.animate-in:nth-child(4) { animation-delay: 0.16s; }
/* ─── Cards ─── */
.card {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 6px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.card-header {
padding: 14px 18px;
border-bottom: 1px solid var(--border-subtle);
font-family: var(--font-display);
font-weight: 600;
font-size: 14px;
letter-spacing: 0.02em;
display: flex;
align-items: center;
justify-content: space-between;
text-transform: uppercase;
color: var(--text-secondary);
}
.card-body { padding: 18px; }
/* ─── Stats ─── */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 24px;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 6px;
padding: 18px 20px;
position: relative;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.stat-card::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 100%; height: 3px;
background: linear-gradient(90deg, var(--cyan), transparent);
opacity: 0.5;
}
.stat-label {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1.5px;
}
.stat-value {
font-family: var(--font-display);
font-size: 32px;
font-weight: 700;
margin-top: 4px;
color: var(--text);
letter-spacing: -0.02em;
}
.stat-value.stat-sm {
font-size: 15px;
font-weight: 500;
font-family: var(--font-mono);
}
/* ─── Tables ─── */
table { width: 100%; border-collapse: collapse; }
th {
text-align: left;
padding: 10px 16px;
background: var(--bg);
border-bottom: 1px solid var(--border);
font-family: var(--font-mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-dim);
font-weight: 600;
white-space: nowrap;
}
td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-subtle);
font-size: 14px;
}
tbody tr { transition: background 0.1s; }
tbody tr:hover td { background: var(--cyan-dim); }
tbody tr:last-child td { border-bottom: none; }
/* ─── Badges ─── */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 3px;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.badge svg { width: 14px; height: 14px; flex-shrink: 0; }
.badge-cyan { background: var(--cyan-dim); color: var(--cyan); }
.badge-amber { background: var(--amber-dim); color: var(--amber); }
.badge-green { background: var(--green-dim); color: var(--green); }
.badge-count {
background: var(--bg);
color: var(--text-secondary);
border: 1px solid var(--border);
}
/* ─── Buttons ─── */
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
border-radius: 4px;
font-family: var(--font-body);
font-size: 13px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-secondary);
transition: all 0.15s;
white-space: nowrap;
}
.btn:hover {
background: var(--bg);
color: var(--text);
border-color: var(--text-dim);
}
.btn svg { width: 14px; height: 14px; }
.btn-cyan {
background: var(--cyan-dim);
color: var(--cyan);
border-color: rgba(9, 117, 176, 0.25);
}
.btn-cyan:hover {
background: rgba(9, 117, 176, 0.15);
border-color: rgba(9, 117, 176, 0.4);
color: var(--cyan);
}
.btn-amber {
background: var(--amber-dim);
color: var(--amber);
border-color: rgba(181, 71, 8, 0.25);
}
.btn-amber:hover {
background: rgba(181, 71, 8, 0.15);
border-color: rgba(181, 71, 8, 0.4);
color: var(--amber);
}
.btn-sm { padding: 4px 10px; font-size: 12px; }
/* ─── Search ─── */
.search-box {
display: flex;
align-items: center;
gap: 8px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0 12px;
height: 36px;
width: 300px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-box:focus-within {
border-color: var(--cyan);
box-shadow: 0 0 0 2px var(--cyan-dim);
}
.search-box svg {
width: 16px; height: 16px;
color: var(--text-dim);
flex-shrink: 0;
}
.search-box input {
border: none;
outline: none;
font-family: var(--font-body);
font-size: 14px;
width: 100%;
background: transparent;
color: var(--text);
}
.search-box input::placeholder { color: var(--text-dim); }
/* ─── Clickable ─── */
.clickable { cursor: pointer; }
/* ─── Detail sections ─── */
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}
.detail-field label {
display: block;
font-family: var(--font-mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text-dim);
margin-bottom: 4px;
}
.detail-field .value {
font-size: 15px;
font-weight: 500;
word-break: break-all;
}
.detail-field .value.mono {
font-family: var(--font-mono);
font-size: 13px;
color: var(--text-secondary);
}
/* ─── Back link ─── */
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--text-dim);
text-decoration: none;
font-size: 13px;
cursor: pointer;
margin-bottom: 20px;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.5px;
transition: color 0.15s;
}
.back-link:hover { color: var(--cyan); }
.back-link svg { width: 14px; height: 14px; }
/* ─── BOM Expansion ─── */
.bom-expand-row td {
padding: 0 !important;
background: var(--bg) !important;
}
.bom-expand-content {
padding: 16px 16px 16px 48px;
border-left: 3px solid var(--cyan-dim);
margin-left: 16px;
}
.bom-expand-content .info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 6px 24px;
}
.bom-expand-content .info-item {
font-size: 13px;
padding: 2px 0;
}
.bom-expand-content .info-item .lbl {
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-right: 6px;
}
.bom-expand-content .info-item .val {
font-family: var(--font-mono);
color: var(--text);
}
.bom-section-title {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--cyan);
margin: 14px 0 8px;
display: flex;
align-items: center;
gap: 8px;
}
.bom-section-title svg { width: 14px; height: 14px; flex-shrink: 0; }
.bom-section-title:first-child { margin-top: 0; }
.bom-section-title::after {
content: '';
flex: 1;
height: 1px;
background: var(--border-subtle);
}
/* ─── File Browser ─── */
.breadcrumb {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 13px;
margin-bottom: 16px;
flex-wrap: wrap;
padding: 8px 14px;
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 4px;
}
.breadcrumb a {
color: var(--cyan);
text-decoration: none;
cursor: pointer;
transition: opacity 0.15s;
}
.breadcrumb a:hover { opacity: 0.7; }
.breadcrumb .sep { color: var(--text-dim); font-size: 11px; }
.breadcrumb .current { color: var(--text); font-weight: 500; }
.file-name-cell {
display: flex;
align-items: center;
gap: 10px;
}
.file-name-cell svg { width: 18px; height: 18px; flex-shrink: 0; }
.file-name-cell a {
color: var(--text);
text-decoration: none;
cursor: pointer;
transition: color 0.15s;
}
.file-name-cell a:hover { color: var(--cyan); }
/* ─── Loading / Empty ─── */
.loading, .empty {
text-align: center;
padding: 60px 24px;
color: var(--text-dim);
font-size: 14px;
font-family: var(--font-mono);
}
.loading::before {
content: '';
display: block;
width: 24px;
height: 24px;
border: 2px solid var(--border);
border-top-color: var(--cyan);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ─── Chevron toggle ─── */
.chevron-toggle {
display: inline-flex;
width: 18px; height: 18px;
align-items: center;
justify-content: center;
transition: transform 0.2s;
color: var(--text-dim);
}
.chevron-toggle.open {
transform: rotate(90deg);
color: var(--cyan);
}
/* ─── Drawing cards grid ─── */
.drawings-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.drawing-card {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 6px;
padding: 18px 20px;
cursor: pointer;
transition: all 0.2s;
position: relative;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.drawing-card:hover {
border-color: var(--cyan);
background: var(--cyan-dim);
box-shadow: 0 2px 8px rgba(9, 117, 176, 0.1);
}
.drawing-card-title {
font-family: var(--font-display);
font-size: 16px;
font-weight: 600;
margin-bottom: 2px;
}
.drawing-card-sub {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
}
/* ─── Equipment Groups ─── */
.equip-group {
margin-bottom: 16px;
}
.equip-group:last-child { margin-bottom: 0; }
.equip-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 6px 6px 0 0;
cursor: pointer;
transition: all 0.15s;
user-select: none;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.equip-header:hover { background: var(--cyan-dim); }
.equip-header .chevron-toggle { flex-shrink: 0; }
.equip-header-title {
font-family: var(--font-display);
font-size: 16px;
font-weight: 600;
}
.equip-header-number {
font-family: var(--font-mono);
font-size: 15px;
color: var(--cyan);
font-weight: 600;
}
.equip-header-meta {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
}
.equip-header-stat {
font-family: var(--font-mono);
font-size: 13px;
color: var(--text-dim);
}
.equip-header-stat strong {
color: var(--text-secondary);
}
.equip-body {
border: 1px solid var(--border-subtle);
border-top: none;
border-radius: 0 0 6px 6px;
overflow: hidden;
}
.equip-body table { margin: 0; }
.equip-group.collapsed .equip-body { display: none; }
.equip-group.collapsed .equip-header { border-radius: 6px; }
/* ─── Responsive ─── */
@media (max-width: 768px) {
.sidebar { display: none; }
.main { margin-left: 0; }
.search-box { width: 100%; }
.topbar { padding: 0 16px; }
.page-content { padding: 16px; }
}

View File

@@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FabWorks</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<aside class="sidebar">
<div class="sidebar-brand">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 20V8l4-4h6l2 2h6a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z"/>
<path d="M8 10v6" opacity="0.5"/>
<path d="M12 8v8" opacity="0.5"/>
<path d="M16 11v3" opacity="0.5"/>
</svg>
</div>
<nav class="sidebar-nav">
<a class="nav-item active" data-page="exports" onclick="router.go('exports')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
<span class="nav-tooltip">Exports</span>
</a>
<a class="nav-item" data-page="drawings" onclick="router.go('drawings')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>
<span class="nav-tooltip">Drawings</span>
</a>
<a class="nav-item" data-page="files" onclick="router.go('files')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
<span class="nav-tooltip">Files</span>
</a>
</nav>
</aside>
<div class="main">
<div class="topbar">
<div class="topbar-left">
<h2 id="page-title">Exports</h2>
<span class="topbar-tag" id="page-tag"></span>
</div>
<div id="topbar-actions"></div>
</div>
<div class="page-content" id="page-content"></div>
</div>
<script src="js/icons.js"></script>
<script src="js/helpers.js"></script>
<script src="js/components.js"></script>
<script src="js/pages.js"></script>
<script src="js/router.js"></script>
<script>router.init();</script>
</body>
</html>

View File

@@ -1,60 +0,0 @@
/* ─── BOM Detail Expansion ─── */
function renderBomDetails(b) {
let html = '<div class="bom-expand-content">';
if (b.cutTemplate) {
const ct = b.cutTemplate;
const displayName = ct.dxfFilePath?.split(/[/\\]/).pop() || '';
html += `
<div class="bom-section-title">${icons.laser} Cut Template</div>
<div class="info-grid">
<div class="info-item"><span class="lbl">File</span><span class="val">${esc(displayName)}</span></div>
<div class="info-item"><span class="lbl">Thickness</span><span class="val">${fmtThickness(ct.thickness)}</span></div>
<div class="info-item"><span class="lbl">K-Factor</span><span class="val">${ct.kFactor != null ? ct.kFactor : '\u2014'}</span></div>
<div class="info-item"><span class="lbl">Bend Radius</span><span class="val">${ct.defaultBendRadius != null ? ct.defaultBendRadius.toFixed(4) + '"' : '\u2014'}</span></div>
</div>`;
if (ct.contentHash) {
html += `<div style="margin-top:10px">
<a class="btn btn-cyan btn-sm" href="/api/files/blob/${encodeURIComponent(ct.contentHash)}?ext=dxf&download=true&name=${encodeURIComponent(displayName)}" onclick="event.stopPropagation()">${icons.download} Download DXF</a>
<span style="font-family:var(--font-mono);font-size:13px;color:var(--text-dim);margin-left:8px">${esc(displayName)}</span>
</div>`;
}
}
if (b.formProgram) {
const fp = b.formProgram;
html += `
<div class="bom-section-title">${icons.bend} Form Program</div>
<div class="info-grid">
<div class="info-item"><span class="lbl">Program</span><span class="val">${esc(fp.programName)}</span></div>
<div class="info-item"><span class="lbl">Thickness</span><span class="val">${fmtThickness(fp.thickness)}</span></div>
<div class="info-item"><span class="lbl">Material</span><span class="val">${esc(fp.materialType)}</span></div>
<div class="info-item"><span class="lbl">K-Factor</span><span class="val">${fp.kFactor != null ? fp.kFactor : '\u2014'}</span></div>
<div class="info-item"><span class="lbl">Bends</span><span class="val">${fp.bendCount}</span></div>
<div class="info-item"><span class="lbl">Upper Tools</span><span class="val">${esc(fp.upperToolNames) || '\u2014'}</span></div>
<div class="info-item"><span class="lbl">Lower Tools</span><span class="val">${esc(fp.lowerToolNames) || '\u2014'}</span></div>
</div>
${fp.setupNotes ? `<div style="margin-top:8px;padding:8px 12px;background:var(--amber-dim);border-radius:4px;font-size:13px;font-family:var(--font-mono);color:var(--amber)"><span class="lbl">Setup Notes</span>${esc(fp.setupNotes)}</div>` : ''}`;
}
html += '</div>';
return html;
}
function toggleEquipGroup(id) {
const group = document.getElementById(id);
const icon = document.getElementById(id + '-icon');
if (!group) return;
group.classList.toggle('collapsed');
if (icon) icon.classList.toggle('open', !group.classList.contains('collapsed'));
}
function toggleBomRow(id) {
const row = document.getElementById(id);
const icon = document.getElementById(id + '-icon');
if (!row) return;
const visible = row.style.display !== 'none';
row.style.display = visible ? 'none' : '';
if (icon) icon.classList.toggle('open', !visible);
}

View File

@@ -1,36 +0,0 @@
function fmtSize(b) {
if (!b) return '0 B';
const k = 1024, s = ['B','KB','MB','GB'];
const i = Math.floor(Math.log(b) / Math.log(k));
return parseFloat((b / Math.pow(k, i)).toFixed(1)) + ' ' + s[i];
}
function fmtDate(d) {
if (!d) return '';
const dt = new Date(d);
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
' ' + dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function fmtThickness(t) {
if (t == null) return '\u2014';
return `<span style="font-family:var(--font-mono)">${t.toFixed(4)}"</span>`;
}
function esc(s) {
return s ? s.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;') : '';
}
function setPage(title, tag = '') {
document.getElementById('page-title').textContent = title;
document.getElementById('page-tag').textContent = tag;
document.getElementById('page-tag').style.display = tag ? '' : 'none';
}
const api = {
async get(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
return r.json();
}
};

View File

@@ -1,19 +0,0 @@
const icons = {
search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
folder: `<svg viewBox="0 0 24 24" fill="var(--amber-dim)" stroke="var(--amber)" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
fileDxf: `<svg viewBox="0 0 24 24" fill="none" stroke="var(--cyan)" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
filePdf: `<svg viewBox="0 0 24 24" fill="none" stroke="var(--red)" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
fileGeneric: `<svg viewBox="0 0 24 24" fill="none" stroke="var(--text-dim)" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
download: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
back: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>`,
chevron: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>`,
laser: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--cyan)" stroke-width="1.2"><circle cx="8" cy="8" r="2"/><path d="M8 2v3M8 11v3M2 8h3M11 8h3" opacity="0.5"/></svg>`,
bend: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--amber)" stroke-width="1.2"><path d="M3 13V7a4 4 0 0 1 4-4h6"/><polyline points="10 6 13 3 10 0" transform="translate(0,2)"/></svg>`,
};
function fileIcon(name) {
const ext = name.split('.').pop().toLowerCase();
if (ext === 'dxf') return icons.fileDxf;
if (ext === 'pdf') return icons.filePdf;
return icons.fileGeneric;
}

View File

@@ -1,399 +0,0 @@
const pages = {
async exports(params) {
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage('Exports');
const searchVal = params.q || '';
actions.innerHTML = `
<div class="search-box">
${icons.search}
<input type="text" id="export-search" placeholder="Search drawing, part, user..." value="${esc(searchVal)}">
</div>`;
content.innerHTML = `<div class="loading">Loading exports</div>`;
const searchInput = document.getElementById('export-search');
let debounce;
searchInput.addEventListener('input', () => {
clearTimeout(debounce);
debounce = setTimeout(() => router.go('exports', { q: searchInput.value }), 400);
});
try {
const searchQ = searchVal ? `&search=${encodeURIComponent(searchVal)}` : '';
const data = await api.get(`/api/exports?take=500${searchQ}`);
if (data.items.length === 0) {
content.innerHTML = `<div class="empty">No exports found.</div>`;
return;
}
// Deduplicate: keep only the latest export per drawing number
const seen = new Set();
const unique = data.items.filter(e => {
const dn = e.drawingNumber || '';
if (seen.has(dn)) return false;
seen.add(dn);
return true;
});
// Group by equipment number (first token of drawing number)
const groups = new Map();
unique.forEach(e => {
const dn = e.drawingNumber || '';
const spaceIdx = dn.indexOf(' ');
const equip = spaceIdx > 0 ? dn.substring(0, spaceIdx) : (dn || 'Other');
if (!groups.has(equip)) groups.set(equip, []);
groups.get(equip).push(e);
});
// Sort equipment groups by number descending (most recent equipment first)
const sortedGroups = [...groups.entries()].sort((a, b) => {
const numA = parseInt(a[0]) || 0;
const numB = parseInt(b[0]) || 0;
return numB - numA;
});
const uniqueEquip = sortedGroups.length;
const uniqueDrawings = unique.length;
setPage('Exports', `${uniqueDrawings} drawings / ${uniqueEquip} equipment`);
const groupsHtml = sortedGroups.map(([equip, items], gi) => {
const totalBom = items.reduce((s, e) => s + e.bomItemCount, 0);
const rows = items.map((e, i) => {
const dn = e.drawingNumber || '';
const spaceIdx = dn.indexOf(' ');
const drawingPart = spaceIdx > 0 ? dn.substring(spaceIdx + 1) : dn;
return `
<tr class="clickable" onclick="router.go('export-detail', {id: ${e.id}})" style="animation: fadeSlideIn 0.2s ease ${0.02 * i}s forwards; opacity: 0">
<td style="font-family:var(--font-mono);color:var(--text-dim);font-size:13px">${e.id}</td>
<td><strong>${esc(drawingPart) || '<span style="color:var(--text-dim)">\u2014</span>'}</strong></td>
<td style="color:var(--text-secondary);font-size:13px">${esc(e.title) || ''}</td>
<td><span class="badge badge-count">${e.bomItemCount}</span></td>
<td style="color:var(--text-secondary)">${esc(e.exportedBy)}</td>
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary);white-space:nowrap">${fmtDate(e.exportedAt)}</td>
</tr>`;
}).join('');
return `
<div class="equip-group animate-in" id="equip-${esc(equip)}" style="animation-delay:${0.04 * gi}s">
<div class="equip-header" onclick="toggleEquipGroup('equip-${esc(equip)}')">
<span class="chevron-toggle open" id="equip-${esc(equip)}-icon">${icons.chevron}</span>
<span class="equip-header-number">${esc(equip)}</span>
<div class="equip-header-meta">
<span class="equip-header-stat"><strong>${items.length}</strong> exports</span>
<span class="equip-header-stat"><strong>${totalBom}</strong> items</span>
</div>
</div>
<div class="equip-body">
<table>
<thead><tr>
<th style="width:50px">#</th>
<th>Drawing</th>
<th>Title</th>
<th style="width:80px">Items</th>
<th>Exported By</th>
<th style="width:180px">Date</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
</div>
</div>`;
}).join('');
content.innerHTML = `
<div class="stats-grid">
<div class="stat-card animate-in"><div class="stat-label">Drawings</div><div class="stat-value">${uniqueDrawings}</div></div>
<div class="stat-card animate-in"><div class="stat-label">Equipment</div><div class="stat-value">${uniqueEquip}</div></div>
</div>
${groupsHtml}`;
} catch (err) {
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
}
},
async exportDetail(id) {
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage('Loading...');
actions.innerHTML = '';
content.innerHTML = `<div class="loading">Loading export</div>`;
try {
const exp = await api.get(`/api/exports/${id}`);
setPage(exp.drawingNumber || `Export #${exp.id}`, 'export detail');
const dxfCount = (exp.bomItems || []).filter(b => b.cutTemplate?.contentHash).length;
const bomRows = (exp.bomItems || []).map((b, i) => {
const hasDetails = b.cutTemplate || b.formProgram;
const toggleId = `bom-${b.id}`;
return `
<tr class="${hasDetails ? 'clickable' : ''}" ${hasDetails ? `onclick="toggleBomRow('${toggleId}')"` : ''} style="animation: fadeSlideIn 0.25s ease ${0.03 * i}s forwards; opacity: 0">
<td style="width:32px">${hasDetails ? `<span class="chevron-toggle" id="${toggleId}-icon">${icons.chevron}</span>` : ''}</td>
<td style="font-family:var(--font-mono);font-weight:600;color:var(--cyan)">${esc(b.itemNo)}</td>
<td><strong>${esc(b.partName)}</strong></td>
<td style="color:var(--text-secondary)">${esc(b.description)}</td>
<td><span style="font-family:var(--font-mono);font-size:13px">${esc(b.material)}</span></td>
<td style="font-family:var(--font-mono);text-align:center">${b.qty ?? ''}</td>
<td style="font-family:var(--font-mono);text-align:center">${b.totalQty ?? ''}</td>
<td>
${b.cutTemplate ? `<span class="badge badge-cyan">${icons.laser} DXF</span>` : ''}
${b.formProgram ? `<span class="badge badge-amber">${icons.bend} Form</span>` : ''}
</td>
</tr>
${hasDetails ? `<tr class="bom-expand-row" id="${toggleId}" style="display:none"><td colspan="8">${renderBomDetails(b)}</td></tr>` : ''}`;
}).join('');
content.innerHTML = `
<a class="back-link" onclick="router.go('exports')">${icons.back} Back to exports</a>
<div class="card animate-in" style="margin-bottom:20px">
<div class="card-header">Export Information</div>
<div class="card-body">
<div class="detail-grid">
<div class="detail-field"><label>Drawing Number</label><div class="value">${esc(exp.drawingNumber) || '\u2014'}</div></div>
${exp.title ? `<div class="detail-field"><label>Title</label><div class="value">${esc(exp.title)}</div></div>` : ''}
<div class="detail-field"><label>Exported By</label><div class="value">${esc(exp.exportedBy)}</div></div>
<div class="detail-field"><label>Date</label><div class="value mono">${fmtDate(exp.exportedAt)}</div></div>
<div class="detail-field"><label>Source File</label><div class="value mono">${esc(exp.sourceFilePath)}</div></div>
</div>
</div>
</div>
<div class="card animate-in">
<div class="card-header">
BOM Items
<span class="badge badge-count">${exp.bomItems?.length || 0} items</span>
<span style="margin-left:auto;display:flex;gap:6px">
${exp.pdfContentHash ? `<a class="btn btn-amber btn-sm" href="/api/filebrowser/download?hash=${encodeURIComponent(exp.pdfContentHash)}&ext=pdf&name=${encodeURIComponent((exp.drawingNumber || 'drawing') + '.pdf')}">${icons.download} PDF</a>` : ''}
${dxfCount > 0 ? `<a class="btn btn-cyan btn-sm" href="/api/exports/${exp.id}/download-dxfs">${icons.download} All DXFs</a>` : ''}
</span>
</div>
${exp.bomItems?.length ? `
<table>
<thead><tr>
<th style="width:32px"></th>
<th style="width:60px">Item</th>
<th>Part Name</th>
<th>Description</th>
<th>Material</th>
<th style="width:50px;text-align:center">Qty</th>
<th style="width:55px;text-align:center">Total</th>
<th style="width:120px">Data</th>
</tr></thead>
<tbody>${bomRows}</tbody>
</table>` : '<div class="empty">No BOM items for this export.</div>'}
</div>`;
} catch (err) {
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
}
},
async drawings() {
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage('Drawings');
actions.innerHTML = '';
content.innerHTML = `<div class="loading">Loading drawings</div>`;
try {
const numbers = await api.get('/api/exports/drawing-numbers');
if (numbers.length === 0) {
content.innerHTML = `<div class="empty">No drawings found.</div>`;
return;
}
numbers.sort();
setPage('Drawings', `${numbers.length} drawings`);
const cards = numbers.map((d, i) => `
<div class="drawing-card" onclick="router.go('drawing-detail', {id: '${encodeURIComponent(d)}'})" style="animation: fadeSlideIn 0.3s ease ${0.025 * Math.min(i, 20)}s forwards; opacity: 0">
<div class="drawing-card-title">${esc(d)}</div>
<div class="drawing-card-sub">Drawing</div>
</div>`).join('');
content.innerHTML = `
<div class="stats-grid">
<div class="stat-card animate-in"><div class="stat-label">Total Drawings</div><div class="stat-value">${numbers.length}</div></div>
</div>
<div class="drawings-grid">${cards}</div>`;
} catch (err) {
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
}
},
async drawingDetail(drawingEncoded) {
const drawingNumber = decodeURIComponent(drawingEncoded);
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage(drawingNumber, 'drawing');
actions.innerHTML = '';
content.innerHTML = `<div class="loading">Loading drawing</div>`;
try {
const exports = await api.get(`/api/exports/by-drawing?drawingNumber=${encodeURIComponent(drawingNumber)}`);
if (exports.length === 0) {
content.innerHTML = `
<a class="back-link" onclick="router.go('drawings')">${icons.back} Back to drawings</a>
<div class="empty">No exports found for this drawing.</div>`;
return;
}
const allBom = [];
exports.forEach(exp => {
(exp.bomItems || []).forEach(b => {
allBom.push({ ...b, exportId: exp.id, exportedAt: exp.exportedAt });
});
});
const bomRows = allBom.map((b, i) => {
const hasDetails = b.cutTemplate || b.formProgram;
const toggleId = `dbom-${b.id}`;
return `
<tr class="${hasDetails ? 'clickable' : ''}" ${hasDetails ? `onclick="toggleBomRow('${toggleId}')"` : ''} style="animation: fadeSlideIn 0.25s ease ${0.03 * i}s forwards; opacity: 0">
<td style="width:32px">${hasDetails ? `<span class="chevron-toggle" id="${toggleId}-icon">${icons.chevron}</span>` : ''}</td>
<td style="font-family:var(--font-mono);font-weight:600;color:var(--cyan)">${esc(b.itemNo)}</td>
<td><strong>${esc(b.partName)}</strong></td>
<td style="color:var(--text-secondary)">${esc(b.description)}</td>
<td><span style="font-family:var(--font-mono);font-size:13px">${esc(b.material)}</span></td>
<td style="font-family:var(--font-mono);text-align:center">${b.qty ?? ''}</td>
<td style="font-family:var(--font-mono);text-align:center">${b.totalQty ?? ''}</td>
<td>
${b.cutTemplate ? `<span class="badge badge-cyan">${icons.laser} DXF</span>` : ''}
${b.formProgram ? `<span class="badge badge-amber">${icons.bend} Form</span>` : ''}
</td>
</tr>
${hasDetails ? `<tr class="bom-expand-row" id="${toggleId}" style="display:none"><td colspan="8">${renderBomDetails(b)}</td></tr>` : ''}`;
}).join('');
content.innerHTML = `
<a class="back-link" onclick="router.go('drawings')">${icons.back} Back to drawings</a>
<div class="stats-grid">
<div class="stat-card animate-in"><div class="stat-label">Exports</div><div class="stat-value">${exports.length}</div></div>
<div class="stat-card animate-in"><div class="stat-label">BOM Items</div><div class="stat-value">${allBom.length}</div></div>
<div class="stat-card animate-in"><div class="stat-label">Latest Export</div><div class="stat-value stat-sm">${fmtDate(exports[0].exportedAt)}</div></div>
</div>
<div class="card animate-in">
<div class="card-header">
All BOM Items
<span class="badge badge-count">${allBom.length} items</span>
</div>
${allBom.length ? `
<table>
<thead><tr>
<th style="width:32px"></th>
<th style="width:60px">Item</th>
<th>Part Name</th>
<th>Description</th>
<th>Material</th>
<th style="width:50px;text-align:center">Qty</th>
<th style="width:55px;text-align:center">Total</th>
<th style="width:120px">Data</th>
</tr></thead>
<tbody>${bomRows}</tbody>
</table>` : '<div class="empty">No BOM items.</div>'}
</div>`;
} catch (err) {
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
}
},
async files(params) {
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage('Files');
const searchVal = params.q || '';
actions.innerHTML = `
<div style="display:flex;gap:8px;align-items:center">
<div class="search-box">
${icons.search}
<input type="text" id="file-search" placeholder="Search drawing number, filename..." value="${esc(searchVal)}">
</div>
<select id="file-type-filter" style="background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:6px 10px;color:var(--text);font-family:var(--font-body);font-size:14px;height:36px">
<option value="">All types</option>
<option value="dxf">DXF only</option>
<option value="pdf">PDF only</option>
</select>
</div>`;
content.innerHTML = `<div class="loading">Loading files</div>`;
const searchInput = document.getElementById('file-search');
const typeFilter = document.getElementById('file-type-filter');
let debounce;
const refresh = () => {
clearTimeout(debounce);
debounce = setTimeout(() => router.go('files', { q: searchInput.value + (typeFilter.value ? '&type=' + typeFilter.value : '') }), 400);
};
searchInput.addEventListener('input', refresh);
typeFilter.addEventListener('change', refresh);
// Parse search and type from combined param
let searchQ = searchVal;
let typeQ = '';
if (searchVal.includes('&type=')) {
const parts = searchVal.split('&type=');
searchQ = parts[0];
typeQ = parts[1] || '';
searchInput.value = searchQ;
typeFilter.value = typeQ;
}
try {
let url = '/api/filebrowser/files?';
if (searchQ) url += `search=${encodeURIComponent(searchQ)}&`;
if (typeQ) url += `type=${encodeURIComponent(typeQ)}&`;
const data = await api.get(url);
setPage('Files', `${data.total} files`);
if (data.files.length === 0) {
content.innerHTML = `<div class="empty">No files found.</div>`;
return;
}
const rows = data.files.map((f, i) => {
const ext = f.fileType || f.fileName.split('.').pop().toLowerCase();
const hashShort = f.contentHash ? f.contentHash.substring(0, 12) : '';
return `
<tr style="animation: fadeSlideIn 0.25s ease ${0.02 * i}s forwards; opacity: 0">
<td><div class="file-name-cell">${ext === 'pdf' ? icons.filePdf : icons.fileDxf}<a href="/api/filebrowser/download?hash=${encodeURIComponent(f.contentHash)}&ext=${ext}&name=${encodeURIComponent(f.fileName)}">${esc(f.fileName)}</a></div></td>
<td><span class="badge ${ext === 'dxf' ? 'badge-cyan' : 'badge-amber'}">${ext.toUpperCase()}</span></td>
<td style="color:var(--text-secondary)">${esc(f.drawingNumber)}</td>
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary)">${f.thickness != null ? f.thickness.toFixed(4) + '"' : '\u2014'}</td>
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary)">${fmtDate(f.createdAt)}</td>
<td style="font-family:var(--font-mono);font-size:12px;color:var(--text-dim)">${esc(hashShort)}</td>
<td style="white-space:nowrap">
<a class="btn btn-cyan btn-sm" href="/api/filebrowser/download?hash=${encodeURIComponent(f.contentHash)}&ext=${ext}&name=${encodeURIComponent(f.fileName)}">${icons.download}</a>
</td>
</tr>`;
}).join('');
content.innerHTML = `
<div class="card animate-in">
<table>
<thead><tr>
<th>Name</th>
<th style="width:60px">Type</th>
<th>Drawing</th>
<th style="width:90px">Thickness</th>
<th style="width:170px">Date</th>
<th style="width:100px">Hash</th>
<th style="width:90px">Actions</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
</div>`;
} catch (err) {
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
}
}
};

View File

@@ -1,35 +0,0 @@
const router = {
go(page, params = {}) {
const hash = page + (params.id ? '/' + params.id : '') + (params.q ? '?q=' + encodeURIComponent(params.q) : '');
location.hash = hash;
},
parse() {
const h = location.hash.slice(1) || 'exports';
const [path, qs] = h.split('?');
const parts = path.split('/');
const params = {};
if (qs) qs.split('&').forEach(p => { const [k,v] = p.split('='); params[k] = decodeURIComponent(v); });
return { page: parts[0], id: parts[1], params };
},
init() {
window.addEventListener('hashchange', () => this.dispatch());
this.dispatch();
},
dispatch() {
const { page, id, params } = this.parse();
document.querySelectorAll('.nav-item').forEach(el => {
el.classList.toggle('active',
el.dataset.page === page ||
(page === 'export-detail' && el.dataset.page === 'exports') ||
(page === 'drawing-detail' && el.dataset.page === 'drawings'));
});
switch(page) {
case 'exports': pages.exports(params); break;
case 'export-detail': pages.exportDetail(id); break;
case 'drawings': pages.drawings(); break;
case 'drawing-detail': pages.drawingDetail(id, params); break;
case 'files': pages.files(params); break;
default: pages.exports(params);
}
}
};

View File

@@ -1,79 +0,0 @@
using FabWorks.Core.Models;
using Microsoft.EntityFrameworkCore;
namespace FabWorks.Core.Data
{
public class FabWorksDbContext : DbContext
{
public DbSet<ExportRecord> ExportRecords { get; set; }
public DbSet<BomItem> BomItems { get; set; }
public DbSet<CutTemplate> CutTemplates { get; set; }
public DbSet<FormProgram> FormPrograms { get; set; }
public FabWorksDbContext(DbContextOptions<FabWorksDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ExportRecord>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.DrawingNumber).HasMaxLength(100);
entity.Property(e => e.Title).HasMaxLength(200);
entity.Property(e => e.EquipmentNo).HasMaxLength(50);
entity.Property(e => e.DrawingNo).HasMaxLength(50);
entity.Property(e => e.SourceFilePath).HasMaxLength(500);
entity.Property(e => e.OutputFolder).HasMaxLength(500);
entity.Property(e => e.ExportedBy).HasMaxLength(100);
entity.Property(e => e.PdfContentHash).HasMaxLength(64);
entity.HasMany(e => e.BomItems)
.WithOne(b => b.ExportRecord)
.HasForeignKey(b => b.ExportRecordId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<BomItem>(entity =>
{
entity.HasKey(e => e.ID);
entity.Property(e => e.ItemNo).HasMaxLength(50);
entity.Property(e => e.PartNo).HasMaxLength(100);
entity.Property(e => e.Description).HasMaxLength(500);
entity.Property(e => e.PartName).HasMaxLength(200);
entity.Property(e => e.ConfigurationName).HasMaxLength(100);
entity.Property(e => e.Material).HasMaxLength(100);
entity.HasOne(e => e.CutTemplate)
.WithOne(ct => ct.BomItem)
.HasForeignKey<CutTemplate>(ct => ct.BomItemId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.FormProgram)
.WithOne(fp => fp.BomItem)
.HasForeignKey<FormProgram>(fp => fp.BomItemId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<CutTemplate>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.DxfFilePath).HasMaxLength(500);
entity.Property(e => e.CutTemplateName).HasMaxLength(100);
entity.Property(e => e.ContentHash).HasMaxLength(64);
});
modelBuilder.Entity<FormProgram>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.ProgramFilePath).HasMaxLength(500);
entity.Property(e => e.ContentHash).HasMaxLength(64);
entity.Property(e => e.ProgramName).HasMaxLength(200);
entity.Property(e => e.MaterialType).HasMaxLength(50);
entity.Property(e => e.UpperToolNames).HasMaxLength(500);
entity.Property(e => e.LowerToolNames).HasMaxLength(500);
entity.Property(e => e.SetupNotes).HasMaxLength(2000);
});
}
}
}

View File

@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,270 +0,0 @@
// <auto-generated />
using System;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FabWorks.Core.Migrations
{
[DbContext(typeof(FabWorksDbContext))]
[Migration("20260218171742_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("FabWorks.Core.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>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("ExportRecordId")
.HasColumnType("int");
b.Property<string>("ItemNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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<int?>("TotalQty")
.HasColumnType("int");
b.HasKey("ID");
b.HasIndex("ExportRecordId");
b.ToTable("BomItems");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BomItemId")
.HasColumnType("int");
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>("DxfFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("CutTemplates");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("EquipmentNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.ToTable("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BendCount")
.HasColumnType("int");
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<string>("LowerToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MaterialType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ProgramFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("ProgramName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("SetupNotes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.Property<string>("UpperToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("FormPrograms");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems")
.HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExportRecord");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("CutTemplate")
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("FormProgram")
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
b.Navigation("FormProgram");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,151 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FabWorks.Core.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),
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
EquipmentNo = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
DrawingNo = table.Column<string>(type: "nvarchar(50)", maxLength: 50, 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),
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.CreateTable(
name: "CutTemplates",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DxfFilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
ContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
CutTemplateName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, 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),
BomItemId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CutTemplates", x => x.Id);
table.ForeignKey(
name: "FK_CutTemplates_BomItems_BomItemId",
column: x => x.BomItemId,
principalTable: "BomItems",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "FormPrograms",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ProgramFilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
ContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
ProgramName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
Thickness = table.Column<double>(type: "float", nullable: true),
MaterialType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
KFactor = table.Column<double>(type: "float", nullable: true),
BendCount = table.Column<int>(type: "int", nullable: false),
UpperToolNames = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
LowerToolNames = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
SetupNotes = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
BomItemId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FormPrograms", x => x.Id);
table.ForeignKey(
name: "FK_FormPrograms_BomItems_BomItemId",
column: x => x.BomItemId,
principalTable: "BomItems",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_BomItems_ExportRecordId",
table: "BomItems",
column: "ExportRecordId");
migrationBuilder.CreateIndex(
name: "IX_CutTemplates_BomItemId",
table: "CutTemplates",
column: "BomItemId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_FormPrograms_BomItemId",
table: "FormPrograms",
column: "BomItemId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CutTemplates");
migrationBuilder.DropTable(
name: "FormPrograms");
migrationBuilder.DropTable(
name: "BomItems");
migrationBuilder.DropTable(
name: "ExportRecords");
}
}
}

View File

@@ -1,273 +0,0 @@
// <auto-generated />
using System;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FabWorks.Core.Migrations
{
[DbContext(typeof(FabWorksDbContext))]
[Migration("20260219134027_AddCutTemplateRevision")]
partial class AddCutTemplateRevision
{
/// <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("FabWorks.Core.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>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("ExportRecordId")
.HasColumnType("int");
b.Property<string>("ItemNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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<int?>("TotalQty")
.HasColumnType("int");
b.HasKey("ID");
b.HasIndex("ExportRecordId");
b.ToTable("BomItems");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BomItemId")
.HasColumnType("int");
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>("DxfFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<int>("Revision")
.HasColumnType("int");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("CutTemplates");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("EquipmentNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.ToTable("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BendCount")
.HasColumnType("int");
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<string>("LowerToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MaterialType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ProgramFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("ProgramName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("SetupNotes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.Property<string>("UpperToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("FormPrograms");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems")
.HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExportRecord");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("CutTemplate")
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("FormProgram")
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
b.Navigation("FormProgram");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FabWorks.Core.Migrations
{
/// <inheritdoc />
public partial class AddCutTemplateRevision : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Revision",
table: "CutTemplates",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Revision",
table: "CutTemplates");
}
}
}

View File

@@ -1,270 +0,0 @@
// <auto-generated />
using System;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FabWorks.Core.Migrations
{
[DbContext(typeof(FabWorksDbContext))]
partial class FabWorksDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("FabWorks.Core.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>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("ExportRecordId")
.HasColumnType("int");
b.Property<string>("ItemNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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<int?>("TotalQty")
.HasColumnType("int");
b.HasKey("ID");
b.HasIndex("ExportRecordId");
b.ToTable("BomItems");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BomItemId")
.HasColumnType("int");
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>("DxfFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<int>("Revision")
.HasColumnType("int");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("CutTemplates");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("EquipmentNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.ToTable("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BendCount")
.HasColumnType("int");
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<string>("LowerToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MaterialType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ProgramFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("ProgramName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("SetupNotes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.Property<string>("UpperToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("FormPrograms");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems")
.HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExportRecord");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("CutTemplate")
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("FormProgram")
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
b.Navigation("FormProgram");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,22 +0,0 @@
namespace FabWorks.Core.Models
{
public class BomItem
{
public int ID { get; set; }
public string ItemNo { get; set; } = "";
public string PartNo { get; set; } = "";
public int SortOrder { get; set; }
public int? Qty { get; set; }
public int? TotalQty { get; set; }
public string Description { get; set; } = "";
public string PartName { get; set; } = "";
public string ConfigurationName { get; set; } = "";
public string Material { get; set; } = "";
public int ExportRecordId { get; set; }
public virtual ExportRecord ExportRecord { get; set; }
public virtual CutTemplate CutTemplate { get; set; }
public virtual FormProgram FormProgram { get; set; }
}
}

View File

@@ -1,33 +0,0 @@
using System;
namespace FabWorks.Core.Models
{
public class CutTemplate
{
public int Id { get; set; }
public string DxfFilePath { get; set; } = "";
public string ContentHash { get; set; }
public string CutTemplateName { get; set; } = "";
private double? _thickness;
public double? Thickness
{
get => _thickness;
set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null;
}
public int Revision { get; set; } = 1;
public double? KFactor { get; set; }
private double? _defaultBendRadius;
public double? DefaultBendRadius
{
get => _defaultBendRadius;
set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null;
}
public int BomItemId { get; set; }
public virtual BomItem BomItem { get; set; }
}
}

View File

@@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
namespace FabWorks.Core.Models
{
public class ExportRecord
{
public int Id { get; set; }
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
public DateTime ExportedAt { get; set; }
public string ExportedBy { get; set; }
public string PdfContentHash { get; set; }
public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
}
}

View File

@@ -1,20 +0,0 @@
namespace FabWorks.Core.Models
{
public class FormProgram
{
public int Id { get; set; }
public string ProgramFilePath { get; set; } = "";
public string ContentHash { get; set; }
public string ProgramName { get; set; } = "";
public double? Thickness { get; set; }
public string MaterialType { get; set; } = "";
public double? KFactor { get; set; }
public int BendCount { get; set; }
public string UpperToolNames { get; set; } = "";
public string LowerToolNames { get; set; } = "";
public string SetupNotes { get; set; } = "";
public int BomItemId { get; set; }
public virtual BomItem BomItem { get; set; }
}
}

View File

@@ -1,171 +0,0 @@
using System;
using System.Xml.Linq;
namespace FabWorks.Core.PressBrake
{
internal static class Extensions
{
private static bool? ToBool(this string s)
{
if (string.IsNullOrWhiteSpace(s))
return null;
int intValue;
if (!int.TryParse(s, out intValue))
return null;
return Convert.ToBoolean(intValue);
}
public static bool ToBool(this XAttribute a, bool defaultValue = false)
{
if (a == null)
return defaultValue;
var b = a.Value.ToBool();
return b != null ? b.Value : defaultValue;
}
public static bool? ToBoolOrNull(this XAttribute a)
{
if (a == null)
return null;
return a.Value.ToBool();
}
private static int? ToInt(this string s)
{
if (string.IsNullOrWhiteSpace(s))
return null;
int intValue;
if (!int.TryParse(s, out intValue))
return null;
return intValue;
}
public static int ToInt(this XAttribute a, int defaultValue = 0)
{
if (a == null)
return defaultValue;
var b = a.Value.ToInt();
return b != null ? b.Value : defaultValue;
}
public static int? ToIntOrNull(this XAttribute a)
{
if (a == null)
return null;
return a.Value.ToInt();
}
public static int ToInt(this XElement a, int defaultValue = 0)
{
if (a == null)
return defaultValue;
var b = a.Value.ToInt();
return b != null ? b.Value : defaultValue;
}
public static int? ToIntOrNull(this XElement a)
{
if (a == null)
return null;
return a.Value.ToInt();
}
private static double? ToDouble(this string s)
{
if (string.IsNullOrWhiteSpace(s))
return null;
double d;
if (!double.TryParse(s, out d))
return null;
return d;
}
public static double ToDouble(this XAttribute a, double defaultValue = 0)
{
if (a == null)
return defaultValue;
var b = a.Value.ToDouble();
return b != null ? b.Value : defaultValue;
}
public static double? ToDoubleOrNull(this XAttribute a)
{
if (a == null)
return null;
return a.Value.ToDouble();
}
public static double ToDouble(this XElement a, double defaultValue = 0)
{
if (a == null)
return defaultValue;
var b = a.Value.ToDouble();
return b != null ? b.Value : defaultValue;
}
public static double? ToDoubleOrNull(this XElement a)
{
if (a == null)
return null;
return a.Value.ToDouble();
}
public static DateTime? ToDateTime(this XAttribute a)
{
if (a == null || string.IsNullOrWhiteSpace(a.Value))
return null;
DateTime d;
if (!DateTime.TryParse(a.Value, out d))
return null;
return d;
}
public static TimeSpan? ToTimeSpan(this XElement e)
{
if (e == null || string.IsNullOrWhiteSpace(e.Value))
return null;
TimeSpan d;
if (!TimeSpan.TryParse(e.Value, out d))
return null;
return d;
}
public static DateTime RoundDown(this DateTime dt, TimeSpan d)
{
var modTicks = dt.Ticks % d.Ticks;
var delta = -modTicks;
return new DateTime(dt.Ticks + delta, dt.Kind);
}
}
}

View File

@@ -1,11 +0,0 @@
namespace FabWorks.Core.PressBrake
{
public enum MatType
{
MildSteel,
HighStrengthSteel,
Stainless,
SoftAluminum,
HardAluminum
}
}

View File

@@ -1,59 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace FabWorks.Core.PressBrake
{
public class Program
{
public Program()
{
UpperToolSets = new List<ToolSetup>();
LowerToolSets = new List<ToolSetup>();
Steps = new List<Step>();
}
public int Version { get; set; }
public string ProgName { get; set; }
public string FilePath { get; set; }
public double MatThick { get; set; }
public MatType MatType { get; set; }
public double KFactor { get; set; }
public string TeachName { get; set; }
public string PartName { get; set; }
public string SetupNotes { get; set; }
public string ProgNotes { get; set; }
public bool RZEnabled { get; set; }
public List<ToolSetup> UpperToolSets { get; set; }
public List<ToolSetup> LowerToolSets { get; set; }
public List<Step> Steps { get; set; }
public static Program Load(string file)
{
var reader = new ProgramReader();
reader.Read(file);
return reader.Program;
}
public static Program Load(Stream stream)
{
var reader = new ProgramReader();
reader.Read(stream);
return reader.Program;
}
}
}

View File

@@ -1,149 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace FabWorks.Core.PressBrake
{
public class ProgramReader
{
public Program Program { get; set; }
public ProgramReader()
{
Program = new Program();
}
public void Read(string file)
{
var xml = XDocument.Load(file);
Program.FilePath = file;
Read(xml);
}
public void Read(Stream stream)
{
var xml = XDocument.Load(stream);
Read(xml);
}
private void Read(XDocument doc)
{
var data = doc.Root.Element("PressBrakeProgram");
Program.Version = data.Attribute("Version").ToInt();
Program.ProgName = data.Attribute("ProgName")?.Value;
Program.MatThick = data.Attribute("MatThick").ToDouble();
Program.MatType = GetMaterialType(data.Attribute("MatType")?.Value);
Program.KFactor = data.Attribute("KFactor").ToDouble();
Program.TeachName = data.Attribute("TeachName")?.Value;
Program.PartName = data.Attribute("PartName")?.Value;
Program.SetupNotes = data.Attribute("SetupNotes")?.Value;
Program.ProgNotes = data.Attribute("ProgNotes")?.Value;
Program.RZEnabled = Convert.ToBoolean(data.Attribute("RZEnabled").ToInt());
foreach (var item in data.Element("UpperToolSets").Descendants("ToolSetup"))
{
var setup = ReadToolSetup(item);
Program.UpperToolSets.Add(setup);
}
foreach (var item in data.Element("LowerToolSets").Descendants("ToolSetup"))
{
var setup = ReadToolSetup(item);
Program.LowerToolSets.Add(setup);
}
foreach (var item in data.Element("StepData").Descendants("Step"))
{
var step = ReadStep(item);
step.UpperTool = Program.UpperToolSets.FirstOrDefault(t => t.Id == step.UpperID);
step.LowerTool = Program.LowerToolSets.FirstOrDefault(t => t.Id == step.LowerID);
Program.Steps.Add(step);
}
}
private ToolSetup ReadToolSetup(XElement x)
{
var setup = new ToolSetup();
setup.Name = x.Attribute("Name").Value;
setup.Id = x.Attribute("ID").ToInt();
setup.Length = x.Attribute("Length").ToDouble();
setup.StackedHolderType = x.Attribute("StackedHolderType").ToInt();
setup.HolderHeight = x.Attribute("HolderHeight").ToDouble();
foreach (var item in x.Descendants("SegEntry"))
{
var entry = new SegEntry();
entry.SegValue = item.Attribute("SegValue").ToDouble();
setup.Segments.Add(entry);
}
return setup;
}
private Step ReadStep(XElement x)
{
var step = new Step();
step.RevMode = x.Attribute("RevMode").ToInt();
step.RevTons = x.Attribute("RevTons").ToDouble();
step.MaxTons = x.Attribute("MaxTons").ToDouble();
step.RevAbsPos = x.Attribute("RevAbsPos").ToDouble();
step.ActualAng = x.Attribute("ActualAng").ToDouble();
step.AngleAdj = x.Attribute("AngleAdj").ToDouble();
step.BendLen = x.Attribute("BendLen").ToDouble();
step.StrokeLen = x.Attribute("StrokeLen").ToDouble();
step.UpperID = x.Attribute("UpperID").ToInt();
step.LowerID = x.Attribute("LowerID").ToInt();
step.SpdChgDwn = x.Attribute("SpdChgDwn").ToDouble();
step.SpdChgUp = x.Attribute("SpdChgUp").ToDouble();
step.Tilt = x.Attribute("Tilt").ToDouble();
step.FormSpeed = x.Attribute("FormSpeed").ToDouble();
step.XLeft = x.Attribute("XLeft").ToDouble();
step.XRight = x.Attribute("XRight").ToDouble();
step.RLeft = x.Attribute("RLeft").ToDouble();
step.RRight = x.Attribute("RRight").ToDouble();
step.ZLeft = x.Attribute("ZLeft").ToDouble();
step.ZRight = x.Attribute("ZRight").ToDouble();
step.FLeft = x.Attribute("FLeft").ToDouble();
step.FRight = x.Attribute("FRight").ToDouble();
step.SSLeft = x.Attribute("SSLeft").ToDouble();
step.SSRight = x.Attribute("SSRight").ToDouble();
step.ReturnSpd = x.Attribute("ReturnSpd").ToDouble();
step.SideFlgHeight = x.Attribute("SideFlgHeight").ToDouble();
return step;
}
private MatType GetMaterialType(string value)
{
if (value == null)
return MatType.MildSteel;
int i;
if (!int.TryParse(value, out i))
return MatType.MildSteel;
switch (i)
{
case 0:
return MatType.MildSteel;
case 1:
return MatType.HighStrengthSteel;
case 2:
return MatType.Stainless;
case 3:
return MatType.SoftAluminum;
case 4:
return MatType.HardAluminum;
}
return MatType.MildSteel;
}
}
}

View File

@@ -1,7 +0,0 @@
namespace FabWorks.Core.PressBrake
{
public class SegEntry
{
public double SegValue { get; set; }
}
}

View File

@@ -1,36 +0,0 @@
namespace FabWorks.Core.PressBrake
{
public class Step
{
public int RevMode { get; set; }
public double RevTons { get; set; }
public double MaxTons { get; set; }
public double RevAbsPos { get; set; }
public double ActualAng { get; set; }
public double AngleAdj { get; set; }
public double BendLen { get; set; }
public double StrokeLen { get; set; }
public double Tilt { get; set; }
public int UpperID { get; set; }
public int LowerID { get; set; }
public double SpdChgDwn { get; set; }
public double SpdChgUp { get; set; }
public double FormSpeed { get; set; }
public double XLeft { get; set; }
public double XRight { get; set; }
public double RLeft { get; set; }
public double RRight { get; set; }
public double ZLeft { get; set; }
public double ZRight { get; set; }
public double FLeft { get; set; }
public double FRight { get; set; }
public double SSLeft { get; set; }
public double SSRight { get; set; }
public double ReturnSpd { get; set; }
public double SideFlgHeight { get; set; }
public ToolSetup UpperTool { get; set; }
public ToolSetup LowerTool { get; set; }
}
}

View File

@@ -1,24 +0,0 @@
using System.Collections.Generic;
namespace FabWorks.Core.PressBrake
{
public class ToolSetup
{
public ToolSetup()
{
Segments = new List<SegEntry>();
}
public string Name { get; set; }
public int Id { get; set; }
public double Length { get; set; }
public int StackedHolderType { get; set; }
public double HolderHeight { get; set; }
public List<SegEntry> Segments { get; set; }
}
}

View File

@@ -1,32 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FabWorks.Core\FabWorks.Core.csproj" />
<ProjectReference Include="..\FabWorks.Api\FabWorks.Api.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="TestData\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -1,53 +0,0 @@
using FabWorks.Api.Services;
using Xunit;
namespace FabWorks.Tests
{
public class FormProgramServiceTests
{
[Fact]
public void ParseFromFile_SamplePgm_PopulatesMaterialType()
{
var service = new FormProgramService();
var fp = service.ParseFromFile("TestData/sample.pgm");
// ProgName is empty in the sample file, so verify MaterialType instead
Assert.False(string.IsNullOrEmpty(fp.MaterialType));
}
[Fact]
public void ParseFromFile_SamplePgm_PopulatesThickness()
{
var service = new FormProgramService();
var fp = service.ParseFromFile("TestData/sample.pgm");
Assert.NotNull(fp.Thickness);
Assert.True(fp.Thickness > 0);
}
[Fact]
public void ParseFromFile_SamplePgm_PopulatesBendCount()
{
var service = new FormProgramService();
var fp = service.ParseFromFile("TestData/sample.pgm");
Assert.True(fp.BendCount > 0);
}
[Fact]
public void ParseFromFile_SamplePgm_PopulatesToolNames()
{
var service = new FormProgramService();
var fp = service.ParseFromFile("TestData/sample.pgm");
Assert.False(string.IsNullOrEmpty(fp.UpperToolNames));
Assert.False(string.IsNullOrEmpty(fp.LowerToolNames));
}
[Fact]
public void ParseFromFile_SamplePgm_ComputesContentHash()
{
var service = new FormProgramService();
var fp = service.ParseFromFile("TestData/sample.pgm");
Assert.NotNull(fp.ContentHash);
Assert.Equal(64, fp.ContentHash.Length); // SHA256 hex = 64 chars
}
}
}

View File

@@ -1,48 +0,0 @@
using FabWorks.Core.PressBrake;
using Xunit;
namespace FabWorks.Tests.PressBrake
{
public class ProgramReaderTests
{
[Fact]
public void Load_SamplePgm_ParsesProgramAttributes()
{
var pgm = Program.Load("TestData/sample.pgm");
// ProgName may be empty on some exports; verify PartName was parsed instead
Assert.False(string.IsNullOrEmpty(pgm.PartName));
}
[Fact]
public void Load_SamplePgm_ParsesThickness()
{
var pgm = Program.Load("TestData/sample.pgm");
Assert.True(pgm.MatThick > 0);
}
[Fact]
public void Load_SamplePgm_ParsesSteps()
{
var pgm = Program.Load("TestData/sample.pgm");
Assert.NotEmpty(pgm.Steps);
}
[Fact]
public void Load_SamplePgm_ParsesToolSetups()
{
var pgm = Program.Load("TestData/sample.pgm");
Assert.NotEmpty(pgm.UpperToolSets);
Assert.NotEmpty(pgm.LowerToolSets);
}
[Fact]
public void Load_SamplePgm_ResolvesStepToolReferences()
{
var pgm = Program.Load("TestData/sample.pgm");
var step = pgm.Steps[0];
Assert.NotNull(step.UpperTool);
Assert.NotNull(step.LowerTool);
}
}
}

View File

@@ -1,593 +0,0 @@
<?xml version="1.0" ?>
<Document>
<PressBrakeProgram Version="13" ProgName="" TeachName="" PartName="C:\Users\aj.REMCO\Desktop\4980 A05-1 PT02.part" SetupNotes="" ProgNotes="" MatThick="0.06" MatType="2" KFactor="0.42" RZEnabled="1" FEnabled="0" KFactorAuto="0" PartsBetween="0" DryRun="0" LeftFingerType="2" RightFingerType="2" HasPart="1" CBAngMode="1" CBVee="0.75" CBMute="20.752" CBClamp="20.492" CBDieAng="85" CBTopOfDie="20.442" ToolSelLock="0" NumSteps="8" GageType="6">
<UpperToolSets>
<ToolList Count="1">
<ToolSetup Name="50210 with double riser" ID="1" Length="63" NumSegs="4">
<SegStackup>
<SegEntry SegValue="36"/>
<SegEntry SegValue="18"/>
<SegEntry SegValue="8"/>
<SegEntry SegValue="1"/>
</SegStackup>
</ToolSetup>
</ToolList>
</UpperToolSets>
<LowerToolSets>
<ToolList Count="1">
<ToolSetup Name="0.750 x 85 x 144" ID="1" Length="144"/>
</ToolList>
</LowerToolSets>
<StepData>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.0859" StrokeLen="1.7399" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-15.2429" ZRight="15.2429" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" CBTopStop="22.1819" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.5429" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.7179" StrokeLen="8.8696" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="1" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-16.7954" ZRight="0.3125" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.859" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.0859" StrokeLen="1.7399" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-15.2429" ZRight="15.2429" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="22.1819" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.5429" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.7179" StrokeLen="8.8696" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="1" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-0.3125" ZRight="16.7954" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.859" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4609" StrokeLen="1.74" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-29.1179" ZRight="29.1179" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="22.182" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.7304" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4679" StrokeLen="8.8759" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-30.6705" ZRight="30.6704" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.734" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4609" StrokeLen="1.74" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-29.1179" ZRight="29.1179" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="22.182" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.7304" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4679" StrokeLen="8.8759" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-30.6704" ZRight="30.6704" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.734" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
</StepData>
</PressBrakeProgram>
<PressBrakePart Version="2" FlangeCount="9" MatType="2" KFactor="0.42" KFactorAuto="0" FingerSelPref="3">
<Flanges>
<Flange Version="5" FlangeID="1" FlangeDim="63.3409" FlangeWidth="1.554956" Editable="0" BendCount="1" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0" IntBend="0">
<Quaternion Version="1" SVal="1">
<Segment3D XVal="0" YVal="0" ZVal="0"/>
</Quaternion>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="0" YVal="0" ZVal="0"/>
</Quaternion>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="0" YVal="0" ZVal="0"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="63.460859" BendRadius="0.125" BendSeq="5" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="1" EndFlangeID="2" Rotate180="1" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="5" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="0.117968" YVal="31.73043" ZVal="0"/>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0" YVal="0" ZVal="1"/>
</Quaternion>
<Segment3D XVal="0.000001" YVal="31.73043" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="6" OrgSegListCount="6">
<SegmentList>
<Segment3D XVal="0.117972" YVal="0.059985" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.122918" YVal="0.0625" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.672918" YVal="1.6125" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.672918" YVal="61.848358" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.122918" YVal="63.398358" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.117961" YVal="63.400878" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0.000003" YVal="0" ZVal="0.03"/>
<Segment3D XVal="0.122918" YVal="0.0625" ZVal="0.03"/>
<Segment3D XVal="1.672918" YVal="1.6125" ZVal="0.03"/>
<Segment3D XVal="1.672918" YVal="61.848358" ZVal="0.03"/>
<Segment3D XVal="0.122918" YVal="63.398358" ZVal="0.03"/>
<Segment3D XVal="0" YVal="63.460859" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="2" FlangeDim="63.5859" FlangeWidth="8.1099" Editable="0" BendCount="1" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0.707107" YVal="0" ZVal="0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0" YVal="0" ZVal="1"/>
</Quaternion>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0.707107" YVal="0" ZVal="0.707107"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="63.467927" BendRadius="0.125" BendSeq="6" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="2" EndFlangeID="3" Rotate180="1" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="6" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109896" YVal="0" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.34583" YVal="0" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="8" OrgSegListCount="8">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-31.790412" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="-31.792928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104948" YVal="-31.792928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109896" YVal="-31.790554" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109899" YVal="31.790554" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104945" YVal="31.79293" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004945" YVal="31.79293" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="31.790414" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0.000003" YVal="-31.73043" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="-31.792928" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="-31.792928" ZVal="0.03"/>
<Segment3D XVal="8.345832" YVal="-31.733962" ZVal="0.03"/>
<Segment3D XVal="8.345827" YVal="31.733966" ZVal="0.03"/>
<Segment3D XVal="8.222912" YVal="31.79293" ZVal="0.03"/>
<Segment3D XVal="0.122912" YVal="31.79293" ZVal="0.03"/>
<Segment3D XVal="-0.000003" YVal="31.73043" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="3" FlangeDim="63.3599" FlangeWidth="35.609897" Editable="0" BendCount="3" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="0">
<Segment3D XVal="1" YVal="0" ZVal="-0"/>
</Quaternion>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0" YVal="0" ZVal="1"/>
</Quaternion>
<Quaternion Version="1" SVal="0">
<Segment3D XVal="1" YVal="0" ZVal="-0"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="63.467928" BendRadius="0.125" BendSeq="8" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="3" EndFlangeID="4" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="8" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="35.609893" YVal="-0.000003" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="35.845827" YVal="-0.000003" ZVal="0"/>
</Bend>
<Bend Version="9" BendLength="35.717928" BendRadius="0.125" BendSeq="2" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="3" EndFlangeID="5" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="2" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="17.804945" YVal="-31.679949" ZVal="0"/>
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="0.707107"/>
</Quaternion>
<Segment3D XVal="17.922911" YVal="-31.797916" ZVal="0"/>
</Bend>
<Bend Version="9" BendLength="35.717928" BendRadius="0.125" BendSeq="4" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="3" EndFlangeID="6" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="4" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="17.804948" YVal="31.679946" ZVal="0"/>
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Segment3D XVal="17.922915" YVal="31.797913" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="12" OrgSegListCount="12">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-31.677374" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004945" YVal="-31.675001" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.00257" YVal="-31.679952" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.607317" YVal="-31.679947" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.604945" YVal="-31.675002" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.609896" YVal="-31.677377" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.609893" YVal="31.67737" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.604948" YVal="31.674998" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.607323" YVal="31.679949" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.002576" YVal="31.679944" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="31.674999" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="31.677374" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0" YVal="-31.733964" ZVal="0.03"/>
<Segment3D XVal="0.122912" YVal="-31.675001" ZVal="0.03"/>
<Segment3D XVal="0.063947" YVal="-31.797916" ZVal="0.03"/>
<Segment3D XVal="35.781875" YVal="-31.797916" ZVal="0.03"/>
<Segment3D XVal="35.722912" YVal="-31.675002" ZVal="0.03"/>
<Segment3D XVal="35.845827" YVal="-31.733967" ZVal="0.03"/>
<Segment3D XVal="35.845827" YVal="31.733961" ZVal="0.03"/>
<Segment3D XVal="35.722915" YVal="31.674998" ZVal="0.03"/>
<Segment3D XVal="35.781879" YVal="31.797912" ZVal="0.03"/>
<Segment3D XVal="0.063951" YVal="31.797913" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="31.674999" ZVal="0.03"/>
<Segment3D XVal="0" YVal="31.733964" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="4" FlangeDim="63.5859" FlangeWidth="8.109903" Editable="0" BendCount="1" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="0">
<Segment3D XVal="0.707107" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0" YVal="0" ZVal="1"/>
</Quaternion>
<Quaternion Version="1" SVal="0">
<Segment3D XVal="1" YVal="0" ZVal="-0"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="63.460859" BendRadius="0.125" BendSeq="7" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="4" EndFlangeID="7" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="7" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109896" YVal="0.000001" ZVal="0"/>
<Quaternion Version="1" SVal="-1">
<Segment3D XVal="0" YVal="0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.34583" YVal="0.000001" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="8" OrgSegListCount="8">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-31.790554" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="-31.792928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104948" YVal="-31.792929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109902" YVal="-31.79041" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109893" YVal="31.790416" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104951" YVal="31.792929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004951" YVal="31.79293" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="31.790554" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0" YVal="-31.733964" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="-31.792928" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="-31.792929" ZVal="0.03"/>
<Segment3D XVal="8.34583" YVal="-31.730429" ZVal="0.03"/>
<Segment3D XVal="8.34583" YVal="31.730431" ZVal="0.03"/>
<Segment3D XVal="8.222918" YVal="31.792929" ZVal="0.03"/>
<Segment3D XVal="0.122918" YVal="31.79293" ZVal="0.03"/>
<Segment3D XVal="-0" YVal="31.733964" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="5" FlangeDim="35.8359" FlangeWidth="8.109898" Editable="0" BendCount="2" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="-0.5">
<Segment3D XVal="-0.5" YVal="-0.5" ZVal="0.5"/>
</Quaternion>
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="-0.5">
<Segment3D XVal="-0.5" YVal="-0.5" ZVal="0.5"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="19.16793" BendRadius="0.125" BendSeq="1" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="5" EndFlangeID="8" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="1" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109897" YVal="8.271464" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.345831" YVal="8.271464" ZVal="0"/>
</Bend>
<Bend Version="9" BendLength="15.917929" BendRadius="0.125" BendSeq="1" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="5" EndFlangeID="8" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="1" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109895" YVal="-9.896465" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.345829" YVal="-9.896465" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="12" OrgSegListCount="12">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-17.915554" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.00495" YVal="-17.917929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.10495" YVal="-17.917928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109895" YVal="-17.915413" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109896" YVal="-1.937499" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104949" YVal="-1.937499" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104949" YVal="-1.312499" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109898" YVal="-1.312499" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109898" YVal="17.915413" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104948" YVal="17.91793" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="17.917929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="17.915555" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0.000002" YVal="-17.858964" ZVal="0.03"/>
<Segment3D XVal="0.122916" YVal="-17.917929" ZVal="0.03"/>
<Segment3D XVal="8.222916" YVal="-17.917928" ZVal="0.03"/>
<Segment3D XVal="8.345831" YVal="-17.855428" ZVal="0.03"/>
<Segment3D XVal="8.34583" YVal="-1.937499" ZVal="0.03"/>
<Segment3D XVal="8.222916" YVal="-1.937499" ZVal="0.03"/>
<Segment3D XVal="8.222916" YVal="-1.312499" ZVal="0.03"/>
<Segment3D XVal="8.345831" YVal="-1.312499" ZVal="0.03"/>
<Segment3D XVal="8.345828" YVal="17.85543" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="17.91793" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="17.917929" ZVal="0.03"/>
<Segment3D XVal="-0.000002" YVal="17.858964" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="6" FlangeDim="35.8359" FlangeWidth="8.109899" Editable="0" BendCount="2" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="0.5">
<Segment3D XVal="-0.5" YVal="0.5" ZVal="0.5"/>
</Quaternion>
<Quaternion Version="1" SVal="0.707107">
<Segment3D XVal="-0" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="0.5">
<Segment3D XVal="-0.5" YVal="0.5" ZVal="0.5"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="19.167929" BendRadius="0.125" BendSeq="3" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="6" EndFlangeID="9" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="3" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109896" YVal="-8.271465" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.345829" YVal="-8.271465" ZVal="0"/>
</Bend>
<Bend Version="9" BendLength="15.917929" BendRadius="0.125" BendSeq="3" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="6" EndFlangeID="9" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="3" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109897" YVal="9.896464" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.345831" YVal="9.896464" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="12" OrgSegListCount="12">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-17.915554" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.00495" YVal="-17.917929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.10495" YVal="-17.917928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109895" YVal="-17.915413" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109897" YVal="1.312501" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104949" YVal="1.312501" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104949" YVal="1.937501" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109898" YVal="1.937501" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109898" YVal="17.915413" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104948" YVal="17.91793" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="17.917929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="17.915555" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0.000002" YVal="-17.858964" ZVal="0.03"/>
<Segment3D XVal="0.122916" YVal="-17.917929" ZVal="0.03"/>
<Segment3D XVal="8.222916" YVal="-17.917928" ZVal="0.03"/>
<Segment3D XVal="8.345831" YVal="-17.855428" ZVal="0.03"/>
<Segment3D XVal="8.34583" YVal="1.312501" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="1.312501" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="1.937501" ZVal="0.03"/>
<Segment3D XVal="8.34583" YVal="1.937501" ZVal="0.03"/>
<Segment3D XVal="8.345828" YVal="17.85543" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="17.91793" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="17.917929" ZVal="0.03"/>
<Segment3D XVal="-0.000002" YVal="17.858964" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="7" FlangeDim="63.3409" FlangeWidth="1.554952" Editable="0" BendCount="0" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0" YVal="0" ZVal="1"/>
</Quaternion>
<Quaternion Version="1" SVal="0">
<Segment3D XVal="-0" YVal="0" ZVal="-1"/>
</Quaternion>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="-1" YVal="-0" ZVal="0"/>
</Quaternion>
<TopList>
<Feature3D Version="6" SegCount="6" OrgSegListCount="6">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-31.670448" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004951" YVal="-31.66793" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554951" YVal="-30.11793" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554948" YVal="30.117928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="31.667928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="31.670445" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0.000003" YVal="-31.73043" ZVal="0.03"/>
<Segment3D XVal="0.122918" YVal="-31.66793" ZVal="0.03"/>
<Segment3D XVal="1.672918" YVal="-30.11793" ZVal="0.03"/>
<Segment3D XVal="1.672915" YVal="30.117928" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="31.667928" ZVal="0.03"/>
<Segment3D XVal="-0.000003" YVal="31.73043" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="8" FlangeDim="35.5909" FlangeWidth="1.554951" Editable="0" BendCount="0" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="0.707107"/>
</Quaternion>
<TopList>
<Feature3D Version="6" SegCount="10" OrgSegListCount="10">
<SegmentList>
<Segment3D XVal="-0" YVal="-9.583965" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.554948" YVal="-9.583965" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.554948" YVal="-10.208965" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0" YVal="-10.208965" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="-26.06691" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004947" YVal="-26.064394" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554947" YVal="-24.514394" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554949" YVal="7.971464" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.00495" YVal="9.521464" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0" YVal="9.523981" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0" YVal="-9.583965" ZVal="0.03"/>
<Segment3D XVal="0.672915" YVal="-9.583965" ZVal="0.03"/>
<Segment3D XVal="0.672915" YVal="-10.208965" ZVal="0.03"/>
<Segment3D XVal="-0" YVal="-10.208965" ZVal="0.03"/>
<Segment3D XVal="-0.000001" YVal="-26.126894" ZVal="0.03"/>
<Segment3D XVal="0.122913" YVal="-26.064394" ZVal="0.03"/>
<Segment3D XVal="1.672914" YVal="-24.514394" ZVal="0.03"/>
<Segment3D XVal="1.672916" YVal="7.971464" ZVal="0.03"/>
<Segment3D XVal="0.122916" YVal="9.521464" ZVal="0.03"/>
<Segment3D XVal="-0" YVal="9.583965" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="9" FlangeDim="35.5909" FlangeWidth="1.554949" Editable="0" BendCount="0" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="0.707107">
<Segment3D XVal="-0" YVal="-0" ZVal="0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="0.707107">
<Segment3D XVal="-0" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="0.707107">
<Segment3D XVal="-0" YVal="-0" ZVal="0.707107"/>
</Quaternion>
<TopList>
<Feature3D Version="6" SegCount="10" OrgSegListCount="10">
<SegmentList>
<Segment3D XVal="-0" YVal="-9.523981" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="-9.521464" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554948" YVal="-7.971464" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554948" YVal="24.514393" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="26.064393" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="26.06691" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0" YVal="10.208964" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.554948" YVal="10.208964" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.554948" YVal="9.583964" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0" YVal="9.583964" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="-0" YVal="-9.583964" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="-9.521464" ZVal="0.03"/>
<Segment3D XVal="1.672915" YVal="-7.971464" ZVal="0.03"/>
<Segment3D XVal="1.672915" YVal="24.514393" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="26.064393" ZVal="0.03"/>
<Segment3D XVal="-0.000001" YVal="26.126894" ZVal="0.03"/>
<Segment3D XVal="0" YVal="10.208964" ZVal="0.03"/>
<Segment3D XVal="0.672915" YVal="10.208964" ZVal="0.03"/>
<Segment3D XVal="0.672915" YVal="9.583964" ZVal="0.03"/>
<Segment3D XVal="-0" YVal="9.583964" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
</Flanges>
</PressBrakePart>
</Document>

198
README.md
View File

@@ -1,89 +1,109 @@
# ExportDXF
A Windows desktop application that automates exporting flat pattern DXF files from SolidWorks drawings, assemblies, and parts. Built for sheet metal fabrication workflows, it extracts BOM data, generates DXF flat patterns with etch/bend line markings, and exports drawing PDFs to a local output folder with SQL Server tracking.
## Features
- **Batch DXF export** from SolidWorks drawings, assemblies, or individual parts
- **Flat pattern generation** with automatic sheet metal detection
- **Etch line insertion** on bend-up lines for fabrication reference (via EtchBendLines library)
- **PDF export** of SolidWorks drawings
- **Local file export** -- saves DXFs and PDFs to a configurable output folder
- **SQL Server database** -- tracks export history and BOM data via Entity Framework Core
- **View flip control** with automatic, manual, and prefer-up strategies
- **Active document tracking** -- connects to SolidWorks and displays the active document in the title bar
- **BOM extraction** from drawing BOM tables or assembly component trees
- **Drawing history dropdowns** -- equipment and drawing number filters populated from past exports
## Requirements
- Windows 10/11
- .NET 8.0
- SolidWorks (installed and licensed)
- SQL Server (default: `localhost`, database: `ExportDxfDb`, Windows auth)
## Solution Structure
```
ExportDXF.sln
ExportDXF/ Main WinForms application
EtchBendLines/ Library for adding etch lines to DXF files (git submodule)
netDxf/ DXF file read/write library
```
### Key Namespaces
| Namespace | Purpose |
|-----------|---------|
| `ExportDXF.Services` | Core services -- SolidWorks connection, DXF export, BOM extraction, PDF export, file export |
| `ExportDXF.Forms` | WinForms UI -- main export form with log and BOM grids |
| `ExportDXF.Models` | Data models -- BomItem, ExportRecord, ExportContext, SolidWorksDocument |
| `ExportDXF.Data` | Entity Framework Core DbContext for SQL Server persistence |
| `ExportDXF.ViewFlipDeciders` | Strategies for determining if a flat pattern view should be flipped |
| `ExportDXF.ItemExtractors` | Extract component items from BOM tables and assemblies |
| `ExportDXF.Utilities` | SolidWorks helpers, sheet metal property extraction, text utilities |
| `ExportDXF.Extensions` | Extension methods for SolidWorks, UI, unit conversion, strings, TimeSpan |
| `EtchBendLines` | Post-processes DXF files to add etch marks on bend-up lines |
## How It Works
1. **Startup** -- Connects to a running SolidWorks instance (or launches one) asynchronously. Loads past export history from the database to populate equipment/drawing dropdowns. Displays the active SolidWorks document name in the title bar.
2. **Export** -- Based on the active document type:
- **Drawing**: Extracts BOM items from BOM tables, exports the drawing as PDF, then iterates through each component to generate flat pattern DXFs.
- **Assembly**: Extracts components from the assembly tree and generates flat pattern DXFs for each sheet metal part.
- **Part**: Generates a single flat pattern DXF for the active part.
3. **Post-processing** -- Each exported DXF is run through the EtchBendLines library, which identifies bend-up lines and adds short etch marks at their endpoints on a dedicated `ETCH` layer. Only up bends are etched because SolidWorks automatically flips the flat pattern so the shortest flange is the first bend. The etch marks let the press brake operator verify part orientation right off the laser table without flipping -- the first bend (shortest flange) will always have an etch line.
4. **Save** -- DXF and PDF files are copied to the configured output folder. Export records and BOM items (with sheet metal properties) are saved to the SQL Server database.
## Configuration
Settings are in `App.config`:
```xml
<appSettings>
<add key="MaxBendRadius" value="2.0" />
<add key="ExportOutputFolder" value="C:\ExportDXF\Output" />
</appSettings>
<connectionStrings>
<add name="ExportDxfDb"
connectionString="Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;"
providerName="Microsoft.Data.SqlClient" />
</connectionStrings>
```
## DXF Filename Format
Exported files follow the pattern: `{DrawingNumber} PT{ItemNo}.dxf`
Example: `5007 A02 PT01.dxf`
## Building
Open `ExportDXF.sln` in Visual Studio and build. The EtchBendLines submodule must be initialized:
```bash
git submodule update --init --recursive
```
# ExportDXF
A Windows desktop application that automates exporting flat pattern DXF files from SolidWorks drawings, assemblies, and parts. Built for sheet metal fabrication workflows, it extracts BOM data, generates DXF flat patterns with etch/bend line markings, and exports everything to a local folder with an Excel workbook for downstream tools like [OpenNest](https://github.com/ajisaacs/OpenNest).
## Features
- **Batch DXF export** from SolidWorks drawings, assemblies, or individual parts
- **Flat pattern generation** with automatic sheet metal detection
- **Etch line insertion** on bend-up lines for fabrication reference (via EtchBendLines/ACadSharp)
- **PDF export** of SolidWorks drawings
- **Excel BOM output** -- generates an xlsx workbook with a BOM sheet (direct copy of the SolidWorks BOM table) and a Cut Templates sheet (DXF filenames, thicknesses, K-factors, bend radii, content hashes)
- **Revision tracking** -- content hashing detects unchanged DXFs across re-exports; changed files get revision suffixes (e.g., `PT03 Rev2.dxf`)
- **Configurable filename template** -- format like `4321 A01 PT{item_no:2}` with placeholders for item number, part name, configuration, and material
- **Pluggable drawing info extraction** -- auto-fills the filename template from the document name; extensible via `IDrawingInfoExtractor`
- **View flip control** with automatic, manual, and prefer-up strategies
- **Active document tracking** -- connects to SolidWorks and updates the UI when the active document changes
- **Per-export and app-level logging** to plain text log files
## Requirements
- Windows 10/11
- .NET 8.0
- SolidWorks (installed and licensed)
## Solution Structure
```
ExportDXF.sln
ExportDXF/ Main WinForms application
EtchBendLines/ Library for adding etch lines to DXF files (git submodule, uses ACadSharp)
```
### Key Namespaces
| Namespace | Purpose |
|-----------|---------|
| `ExportDXF.Services` | Core services -- SolidWorks connection, DXF export, BOM extraction, PDF export, Excel output, logging, filename template parsing, drawing info extraction |
| `ExportDXF.Forms` | WinForms UI -- main export form with log, BOM, and cut templates grids |
| `ExportDXF.Models` | Data models -- BomItem, CutTemplate, ExportContext, SolidWorksDocument |
| `ExportDXF.ViewFlipDeciders` | Strategies for determining if a flat pattern view should be flipped |
| `ExportDXF.ItemExtractors` | Extract component items from BOM tables and assemblies |
| `ExportDXF.Utilities` | SolidWorks helpers, content hashing, sheet metal property extraction, text utilities |
| `ExportDXF.Extensions` | Extension methods for SolidWorks, UI, unit conversion, strings, TimeSpan |
| `EtchBendLines` | Post-processes DXF files to add etch marks on bend-up lines |
## How It Works
1. **Startup** -- Connects to a running SolidWorks instance (or launches one). Auto-fills the filename template from the active document name using pluggable extractors.
2. **Export** -- Based on the active document type:
- **Drawing**: Copies the raw BOM table to the Excel BOM sheet, exports the drawing as PDF, then generates flat pattern DXFs for each sheet metal component.
- **Assembly**: Extracts components from the assembly tree and generates flat pattern DXFs for each sheet metal part.
- **Part**: Generates a single flat pattern DXF for the active part.
3. **Post-processing** -- Each exported DXF is run through the EtchBendLines library, which identifies bend-up lines and adds short etch marks at their endpoints on a dedicated `ETCH` layer. Only up bends are etched because SolidWorks automatically flips the flat pattern so the shortest flange is the first bend. The etch marks let the press brake operator verify part orientation right off the laser table without flipping.
4. **Save** -- DXF and PDF files are saved to a `Templates/` folder next to the source file. An Excel workbook is written with the BOM and Cut Templates sheets. If re-exporting, content hashes are compared against the existing workbook -- unchanged DXFs are skipped, changed DXFs get a revision suffix.
## Output
```
C:\Projects\4321\
├── 4321 A01.SLDDRW
└── Templates\
├── 4321 A01 PT01.dxf
├── 4321 A01 PT02.dxf
├── 4321 A01 PT03.dxf
├── 4321 A01 PT03 Rev2.dxf (revised, original kept)
├── 4321 A01.pdf
├── 4321 A01.xlsx
└── 4321 A01.log
```
### Excel Workbook
**BOM sheet** -- exact copy of all visible columns and rows from the SolidWorks BOM table (Drawing exports only).
**Cut Templates sheet**:
| Item # | File Name | Revision | Thickness | K-Factor | Bend Radius | Content Hash |
|--------|-----------|----------|-----------|----------|-------------|--------------|
## Configuration
Settings are in `App.config`:
```xml
<appSettings>
<add key="MaxBendRadius" value="2.0" />
<add key="DefaultSuffix" value="PT{item_no:2}" />
</appSettings>
```
### Filename Template Placeholders
| Placeholder | Description | Example |
|-------------|-------------|---------|
| `{item_no:N}` | Item number, zero-padded to N digits | `{item_no:2}``03` |
| `{part_name}` | SolidWorks part name | `Bracket` |
| `{config}` | Configuration name | `Default` |
| `{material}` | Material name | `AISI 304` |
## Building
Open `ExportDXF.sln` in Visual Studio and build. The EtchBendLines submodule must be initialized:
```bash
git submodule update --init --recursive
```