Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d01b2654d | |||
| 1d3b6b8f0f | |||
| 9bc29e98c8 | |||
| c6dde6e217 | |||
| cf17e71b80 | |||
| 742d86ab8a | |||
| ba782b99db | |||
| 9a33d405e2 | |||
| e0d4563cc6 | |||
| a4f6dffe12 | |||
| b7d35bbe78 | |||
| 5e5c6ab72f |
+1
-1
Submodule EtchBendLines updated: da4d3228b0...89d987f6c6
@@ -9,12 +9,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EtchBendLines", "EtchBendLi
|
||||
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
|
||||
@@ -61,42 +55,6 @@ Global
|
||||
{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
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.104.2" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Generated
+55
-101
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+37
-162
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
+11
-25
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -332,7 +332,7 @@ namespace ExportDXF.Services
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSinglePartFileName(ModelDoc2 model, string prefix)
|
||||
private string GetSinglePartFileName(ModelDoc2 model)
|
||||
{
|
||||
var title = model.GetTitle().Replace(".SLDPRT", "");
|
||||
var config = model.ConfigurationManager.ActiveConfiguration.Name;
|
||||
@@ -341,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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace FabWorks.Api.Configuration
|
||||
{
|
||||
public class FileStorageOptions
|
||||
{
|
||||
public const string SectionName = "FileStorage";
|
||||
|
||||
public string OutputFolder { get; set; } = @"C:\ExportDXF\Output";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,366 +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
|
||||
};
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(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();
|
||||
|
||||
_db.ExportRecords.Remove(record);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,26 +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(new StaticFileOptions
|
||||
{
|
||||
OnPrepareResponse = ctx =>
|
||||
ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store")
|
||||
});
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,745 +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-red {
|
||||
background: rgba(217, 45, 32, 0.08);
|
||||
color: var(--red);
|
||||
border-color: rgba(217, 45, 32, 0.25);
|
||||
}
|
||||
|
||||
.btn-red:hover {
|
||||
background: rgba(217, 45, 32, 0.15);
|
||||
border-color: rgba(217, 45, 32, 0.4);
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.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; }
|
||||
}
|
||||
@@ -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?v=2"></script>
|
||||
<script src="js/helpers.js?v=2"></script>
|
||||
<script src="js/components.js?v=2"></script>
|
||||
<script src="js/pages.js?v=2"></script>
|
||||
<script src="js/router.js?v=2"></script>
|
||||
<script>router.init();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,50 +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,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''') : '';
|
||||
}
|
||||
|
||||
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();
|
||||
},
|
||||
async del(url) {
|
||||
const r = await fetch(url, { method: 'DELETE' });
|
||||
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
||||
}
|
||||
};
|
||||
|
||||
async function deleteExport(id) {
|
||||
if (!confirm('Delete this export record? This cannot be undone.')) return;
|
||||
try {
|
||||
await api.del(`/api/exports/${id}`);
|
||||
router.dispatch();
|
||||
} catch (err) {
|
||||
alert('Failed to delete: ' + err.message);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +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>`,
|
||||
trash: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></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;
|
||||
}
|
||||
@@ -1,428 +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;
|
||||
}
|
||||
|
||||
setPage('Exports', `${data.items.length} exports`);
|
||||
|
||||
const rows = data.items.map((e, i) => `
|
||||
<tr class="clickable" onclick="router.go('export-detail', {id: ${e.id}})" style="animation: fadeSlideIn 0.2s ease ${0.02 * Math.min(i, 25)}s forwards; opacity: 0">
|
||||
<td style="font-family:var(--font-mono);color:var(--text-dim);font-size:13px">${e.id}</td>
|
||||
<td><strong>${esc(e.drawingNumber) || '<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>
|
||||
<td><button class="btn btn-red btn-sm" onclick="event.stopPropagation();deleteExport(${e.id})">${icons.trash}</button></td>
|
||||
</tr>`).join('');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="card animate-in">
|
||||
<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>
|
||||
<th style="width:50px"></th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} 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>` : ''}
|
||||
<button class="btn btn-red btn-sm" onclick="deleteExport(${exp.id})">${icons.trash} Delete</button>
|
||||
</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(params) {
|
||||
const actions = document.getElementById('topbar-actions');
|
||||
const content = document.getElementById('page-content');
|
||||
setPage('Drawings');
|
||||
|
||||
const searchVal = (params && params.q) || '';
|
||||
actions.innerHTML = `
|
||||
<div class="search-box">
|
||||
${icons.search}
|
||||
<input type="text" id="drawing-search" placeholder="Search drawing, part, user..." value="${esc(searchVal)}">
|
||||
</div>`;
|
||||
|
||||
content.innerHTML = `<div class="loading">Loading drawings</div>`;
|
||||
|
||||
const searchInput = document.getElementById('drawing-search');
|
||||
let debounce;
|
||||
searchInput.addEventListener('input', () => {
|
||||
clearTimeout(debounce);
|
||||
debounce = setTimeout(() => router.go('drawings', { 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 drawings 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('Drawings', `${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('drawing-detail', {id: '${encodeURIComponent(e.drawingNumber)}'})" style="animation: fadeSlideIn 0.2s ease ${0.02 * i}s forwards; opacity: 0">
|
||||
<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> drawings</span>
|
||||
<span class="equip-header-stat"><strong>${totalBom}</strong> items</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="equip-body">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Drawing</th>
|
||||
<th>Title</th>
|
||||
<th style="width:80px">Items</th>
|
||||
<th>Exported By</th>
|
||||
<th style="width:180px">Latest Export</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 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>`;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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(params); break;
|
||||
case 'drawing-detail': pages.drawingDetail(id, params); break;
|
||||
case 'files': pages.files(params); break;
|
||||
default: pages.exports(params);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace FabWorks.Core.PressBrake
|
||||
{
|
||||
public enum MatType
|
||||
{
|
||||
MildSteel,
|
||||
HighStrengthSteel,
|
||||
Stainless,
|
||||
SoftAluminum,
|
||||
HardAluminum
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace FabWorks.Core.PressBrake
|
||||
{
|
||||
public class SegEntry
|
||||
{
|
||||
public double SegValue { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
||||
# ExportDXF: Remove API, Export to Excel
|
||||
|
||||
**Date:** 2026-04-13
|
||||
**Status:** Draft
|
||||
|
||||
## Goal
|
||||
|
||||
Remove the FabWorks API and database dependencies from ExportDXF. Replace with local file output: DXF files + an Excel workbook (BOM + Cut Templates) that downstream tools like OpenNest can import directly. Make the tool self-contained — no server setup required.
|
||||
|
||||
## Approach
|
||||
|
||||
Merge the `feature/fabworks-api` branch into `master` to get its structural improvements (service layer, models, content hashing, EtchBendLines fixes), then remove the API/DB layer and replace with Excel + log file output.
|
||||
|
||||
---
|
||||
|
||||
## Output Structure
|
||||
|
||||
### Output Folder
|
||||
|
||||
- Default location: `Templates/` folder in the same directory as the source SolidWorks file
|
||||
- If `Templates/` already exists, the export writes into it (see Revision Handling below)
|
||||
|
||||
### Files Produced
|
||||
|
||||
```
|
||||
C:\Projects\4321\
|
||||
├── 4321 A01.SLDDRW (source drawing)
|
||||
└── Templates\
|
||||
├── 4321 A01 PT01.dxf
|
||||
├── 4321 A01 PT02.dxf
|
||||
├── 4321 A01 PT03.dxf
|
||||
├── 4321 A01 PT03 Rev2.dxf (revised file, original kept)
|
||||
├── 4321 A01.xlsx
|
||||
└── 4321 A01.log
|
||||
```
|
||||
|
||||
### Excel Workbook
|
||||
|
||||
Written with **ClosedXML 0.104.2** (same version as OpenNest).
|
||||
|
||||
**"BOM" sheet** — exact copy of the SolidWorks BOM table. All visible columns and rows reproduced as-is. Only present when exporting from a Drawing that contains a BOM table. Part and Assembly exports omit this sheet.
|
||||
|
||||
**"Cut Templates" sheet** — one row per sheet metal part:
|
||||
|
||||
| Column | Description |
|
||||
|--------|-------------|
|
||||
| Item # | Item number from the BOM or export sequence |
|
||||
| File Name | DXF filename stem (no path, no extension) |
|
||||
| Revision | Revision number (1 for initial, increments on change) |
|
||||
| Thickness | Sheet metal thickness |
|
||||
| K-Factor | K-Factor value |
|
||||
| Bend Radius | Default bend radius |
|
||||
| Content Hash | SHA256 of DXF content (excluding HEADER section) |
|
||||
|
||||
Present for all export types (Part, Assembly, Drawing).
|
||||
|
||||
### Log Files
|
||||
|
||||
**Per-export log:** `Templates/{prefix}.log`
|
||||
- Timestamped lines: `2026-04-13 14:32:05 [INFO] Exported 4321 A01 PT03.dxf`
|
||||
- Levels: INFO, WARNING, ERROR
|
||||
- Appends on re-export
|
||||
|
||||
**App-level log:** `C:\ExportDXF\ExportDXF.log`
|
||||
- Rolling log capturing all exports across all drawings
|
||||
- Same format as per-export log
|
||||
|
||||
---
|
||||
|
||||
## Filename Format Template
|
||||
|
||||
### User Interface
|
||||
|
||||
A single text box replaces the equipment/drawing dropdowns. The user types a format string that controls DXF and xlsx naming.
|
||||
|
||||
**Example:** `4321 A01 PT{item_no:2}`
|
||||
|
||||
### Supported Placeholders
|
||||
|
||||
| Placeholder | Description | Example Output |
|
||||
|-------------|-------------|----------------|
|
||||
| `{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` |
|
||||
|
||||
### Naming Rules
|
||||
|
||||
- DXF filename: full template evaluated per part → `4321 A01 PT03.dxf`
|
||||
- Excel filename: literal text before the first placeholder → `4321 A01.xlsx`
|
||||
- Log filename: same stem as excel → `4321 A01.log`
|
||||
- Validation: template must contain `{item_no}` (or `{item_no:N}`) to avoid filename collisions
|
||||
- If the template starts with a placeholder (no literal prefix), fall back to the document name for the xlsx/log filename
|
||||
|
||||
### Auto-Fill
|
||||
|
||||
On document open, the format template text box is auto-filled via `IDrawingInfoExtractor`.
|
||||
|
||||
---
|
||||
|
||||
## Drawing Info Extraction
|
||||
|
||||
### Interface
|
||||
|
||||
```csharp
|
||||
public interface IDrawingInfoExtractor
|
||||
{
|
||||
bool TryExtract(string documentName, out DrawingInfo info);
|
||||
}
|
||||
|
||||
public class DrawingInfo
|
||||
{
|
||||
public string EquipmentNumber { get; set; }
|
||||
public string DrawingNumber { get; set; }
|
||||
public string DefaultTemplate { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### Implementations
|
||||
|
||||
**`DefaultDrawingInfoExtractor`** — uses the document name as literal prefix, appends the default suffix from app.config (`DefaultSuffix` setting, default `PT{item_no:2}`).
|
||||
|
||||
Example: document `MyPart.SLDPRT` → template `MyPart {item_no:2}` (or with configured suffix)
|
||||
|
||||
**`EquipmentDrawingInfoExtractor`** — parses the `{EquipmentNo} {DrawingNo}` pattern from document names in AJ's workplace format. Builds template like `4321 A01 PT{item_no:2}`.
|
||||
|
||||
### Resolution
|
||||
|
||||
Extractors are tried in order. First one that returns `true` wins. Falls back to `DefaultDrawingInfoExtractor`.
|
||||
|
||||
---
|
||||
|
||||
## Revision Handling
|
||||
|
||||
When re-exporting to an existing `Templates` folder:
|
||||
|
||||
1. Read the existing xlsx Cut Templates sheet (if present)
|
||||
2. For each part being exported, compute the DXF content hash
|
||||
3. **Hash matches existing row** → skip, leave existing DXF file untouched
|
||||
4. **Hash differs from existing row** → write new DXF with revision suffix (e.g., `PT03 Rev2.dxf`), update the xlsx row with new filename, new hash, incremented revision number
|
||||
5. **New part (no existing row)** → write DXF, add new row to xlsx with revision 1
|
||||
6. Old revision DXF files are kept in the folder as history
|
||||
|
||||
The BOM sheet (if present) is fully rewritten from the current drawing's BOM table on each export.
|
||||
|
||||
---
|
||||
|
||||
## What We Keep from feature/fabworks-api
|
||||
|
||||
- Service layer architecture: `DxfExportService`, `IPartExporter`, `IDrawingExporter`, `IBomExtractor`
|
||||
- Models: `BomItem`, `CutTemplate`, `Item`, `ExportContext`, `LogEvent`
|
||||
- `ContentHasher` utility (SHA256 excluding DXF HEADER section)
|
||||
- All 9 EtchBendLines submodule fixes (ACadSharp migration, bend detection, degree symbol, etc.)
|
||||
- Async export flow with cancellation
|
||||
- View flip decider
|
||||
- SolidWorks user input disabling during export
|
||||
|
||||
## What We Remove
|
||||
|
||||
- `FabWorksApiClient` and all API DTOs
|
||||
- `ExportDxfDbContext`, EF Core migrations, SQL Server connection string
|
||||
- NuGet packages: `Microsoft.EntityFrameworkCore.SqlServer`, `Microsoft.EntityFrameworkCore.Tools`
|
||||
- Equipment/Drawing dropdowns from MainForm
|
||||
- Any FabWorks.Core / FabWorks.Api projects (if present after merge)
|
||||
|
||||
## What We Add
|
||||
|
||||
- `IDrawingInfoExtractor` interface + two implementations
|
||||
- Format template text box with placeholder parsing
|
||||
- `ExcelExportService` using ClosedXML — reads/writes BOM + Cut Templates sheets
|
||||
- `LogFileService` — per-export and app-level log writing
|
||||
- Output folder logic (Templates folder next to source, revision handling)
|
||||
- NuGet package: `ClosedXML 0.104.2`
|
||||
- `DefaultSuffix` setting in app.config
|
||||
|
||||
## UI Changes
|
||||
|
||||
- Replace equipment/drawing combo boxes with a single format template text box
|
||||
- Keep 3 tabs: Log Events, Bill of Materials, Cut Templates
|
||||
- Keep Start/Stop button
|
||||
- Keep View Flip Decider dropdown
|
||||
Reference in New Issue
Block a user