From b8663659685691062ee7d882f2a917c62a18d3b4 Mon Sep 17 00:00:00 2001 From: "Mr.Xia" <1424473282@qq.com> Date: Sun, 7 Dec 2025 20:25:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.xaml | 89 + App.xaml.cs | 24 + AssemblyInfo.cs | 10 + BackupTest.cs | 79 + CoordinateFormatTest.cs | 142 + DebugTest.cs | 141 + Docs/README_SlotHoleCalculator.md | 247 + Docs/readme.md | 230 + Docs/刀具列表拖动排序功能说明.md | 85 + Docs/刀具尾号类型功能实现说明.md | 223 + Docs/备份功能优化说明.md | 124 + Docs/槽孔实际钻孔孔数相关资料/文章1.txt | 280 + Docs/槽孔实际钻孔孔数相关资料/文章2.txt | 448 ++ DragDropHelper.cs | 359 + DragToolTipWindow.cs | 114 + DrillTapeProcessor.cs | 680 ++ DrillTools.csproj | 13 + DrillTools.sln | 29 + GenerateReorderedDrillTape_Implementation.md | 180 + MainWindow.xaml | 161 + MainWindow.xaml.cs | 216 + MainWindowViewModel.cs | 1607 +++++ SlotHoleCalculator.cs | 406 ++ SlotHoleCalculatorExamples.cs | 241 + ToolDetailViewModel.cs | 98 + ToolDetailWindow.xaml | 118 + ToolDetailWindow.xaml.cs | 38 + ToolItem.cs | 309 + demo_drl_files/p20030722a1-cs.dpin | 12 + demo_drl_files/p20033811c0-a2-cs.drl | 6580 ++++++++++++++++++ demo_drl_files/p20033995a0-cs.drl | 3359 +++++++++ 31 files changed, 16642 insertions(+) create mode 100644 App.xaml create mode 100644 App.xaml.cs create mode 100644 AssemblyInfo.cs create mode 100644 BackupTest.cs create mode 100644 CoordinateFormatTest.cs create mode 100644 DebugTest.cs create mode 100644 Docs/README_SlotHoleCalculator.md create mode 100644 Docs/readme.md create mode 100644 Docs/刀具列表拖动排序功能说明.md create mode 100644 Docs/刀具尾号类型功能实现说明.md create mode 100644 Docs/备份功能优化说明.md create mode 100644 Docs/槽孔实际钻孔孔数相关资料/文章1.txt create mode 100644 Docs/槽孔实际钻孔孔数相关资料/文章2.txt create mode 100644 DragDropHelper.cs create mode 100644 DragToolTipWindow.cs create mode 100644 DrillTapeProcessor.cs create mode 100644 DrillTools.csproj create mode 100644 DrillTools.sln create mode 100644 GenerateReorderedDrillTape_Implementation.md create mode 100644 MainWindow.xaml create mode 100644 MainWindow.xaml.cs create mode 100644 MainWindowViewModel.cs create mode 100644 SlotHoleCalculator.cs create mode 100644 SlotHoleCalculatorExamples.cs create mode 100644 ToolDetailViewModel.cs create mode 100644 ToolDetailWindow.xaml create mode 100644 ToolDetailWindow.xaml.cs create mode 100644 ToolItem.cs create mode 100644 demo_drl_files/p20030722a1-cs.dpin create mode 100644 demo_drl_files/p20033811c0-a2-cs.drl create mode 100644 demo_drl_files/p20033995a0-cs.drl diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..8c2d638 --- /dev/null +++ b/App.xaml @@ -0,0 +1,89 @@ + + + + + + 14 + 16 + 12 + 12 + + + Microsoft YaHei UI + Consolas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..09bd235 --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,24 @@ +using System.Configuration; +using System.Data; +using System.Windows; + +namespace DrillTools +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : System.Windows.Application + { + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + // 运行孔位数据功能测试 + //MainWindowViewModel.TestHoleLocationsFunctionality(); + + // 创建并显示主窗口 + //MainWindow mainWindow = new MainWindow(); + //mainWindow.Show(); + } + } +} \ No newline at end of file diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/BackupTest.cs b/BackupTest.cs new file mode 100644 index 0000000..f544421 --- /dev/null +++ b/BackupTest.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; +using System.Windows; + +namespace DrillTools +{ + /// + /// 备份功能测试类 + /// + public static class BackupTest + { + /// + /// 测试备份功能 + /// + public static void TestBackupFunctionality() + { + Console.WriteLine("=== 备份功能测试 ==="); + + // 创建测试文件 + string testFilePath = "test_file.drl"; + string testContent = "M48\nT01C1.000\n%\nT01\nX1000Y1000\nM30"; + + try + { + // 1. 创建原始测试文件 + File.WriteAllText(testFilePath, testContent); + Console.WriteLine("✓ 创建测试文件成功"); + + // 2. 第一次备份(应该直接创建.bak文件) + string backupPath1 = testFilePath + ".bak"; + if (File.Exists(backupPath1)) + File.Delete(backupPath1); + + File.Copy(testFilePath, backupPath1); + Console.WriteLine("✓ 第一次备份成功"); + + // 3. 模拟第二次备份(检测到.bak文件已存在) + if (File.Exists(backupPath1)) + { + Console.WriteLine("✓ 检测到备份文件已存在"); + + // 模拟用户选择创建时间戳备份 + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + string timestampBackupPath = $"{testFilePath}.{timestamp}.bak"; + File.Copy(testFilePath, timestampBackupPath); + Console.WriteLine($"✓ 创建时间戳备份成功: {Path.GetFileName(timestampBackupPath)}"); + } + + // 4. 验证备份文件内容 + if (File.Exists(backupPath1) && File.ReadAllText(backupPath1) == testContent) + { + Console.WriteLine("✓ 备份文件内容验证成功"); + } + + // 5. 清理测试文件 + File.Delete(testFilePath); + if (File.Exists(backupPath1)) + File.Delete(backupPath1); + + // 删除时间戳备份文件 + var timestampFiles = Directory.GetFiles(".", "*.bak"); + foreach (var file in timestampFiles) + { + if (file.Contains("test_file.") && file.Contains(".bak")) + { + File.Delete(file); + Console.WriteLine($"✓ 清理测试文件: {Path.GetFileName(file)}"); + } + } + + Console.WriteLine("=== 备份功能测试完成 ==="); + } + catch (Exception ex) + { + Console.WriteLine($"✗ 测试失败: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/CoordinateFormatTest.cs b/CoordinateFormatTest.cs new file mode 100644 index 0000000..9f760eb --- /dev/null +++ b/CoordinateFormatTest.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using DrillTools; +using DrillTools.Integration; + +namespace DrillTools.Tests +{ + /// + /// 坐标格式保留测试类 + /// 用于验证修改后的代码能正确保留原始坐标格式 + /// + public class CoordinateFormatTest + { + /// + /// 测试坐标格式保留功能 + /// + public static void TestCoordinateFormatPreservation() + { + Console.WriteLine("=== 坐标格式保留测试 ==="); + + // 测试用例:各种格式的坐标 + var testCases = new[] + { + "X-265000Y013250", // 负坐标,前导零 + "X-265000Y008250", // 负坐标,前导零 + "X075392Y559511", // 正坐标,前导零 + "X017500Y519500G85X018500Y519500", // 槽孔坐标 + "X-238500Y519500G85X-237500Y519500" // 负坐标槽孔 + }; + + // 创建测试钻带内容 + string drillTapeContent = CreateTestDrillTape(testCases); + + // 解析钻带 + var result = DrillTapeProcessor.ProcessDrillTape(drillTapeContent); + + // 验证结果 + if (result.Success) + { + Console.WriteLine("钻带解析成功!"); + + // 检查每个工具的坐标输出 + foreach (var toolResult in result.ToolResults) + { + Console.WriteLine($"\n工具 T{toolResult.ToolNumber:D2} (直径: {toolResult.Diameter:F3}mm):"); + Console.WriteLine($"坐标数量: {toolResult.Locations.Count}"); + + // 显示前5个坐标作为示例 + int displayCount = Math.Min(5, toolResult.Locations.Count); + for (int i = 0; i < displayCount; i++) + { + string original = i < testCases.Length ? testCases[i] : "N/A"; + string parsed = toolResult.Locations[i]; + Console.WriteLine($" 原始: {original}"); + Console.WriteLine($" 解析: {parsed}"); + Console.WriteLine($" 匹配: {(original == parsed ? "✓" : "✗")}"); + Console.WriteLine(); + } + } + } + else + { + Console.WriteLine($"钻带解析失败: {result.Message}"); + } + } + + /// + /// 创建测试钻带内容 + /// + private static string CreateTestDrillTape(string[] coordinates) + { + var drillTape = new System.Text.StringBuilder(); + + // 添加头部 + drillTape.AppendLine("M48"); + drillTape.AppendLine("METRIC"); + drillTape.AppendLine("VER,1"); + drillTape.AppendLine("FMAT,2"); + + // 添加刀具定义 + drillTape.AppendLine("T01C1.000"); + drillTape.AppendLine("T02C2.000"); + + // 添加结束标记 + drillTape.AppendLine("%"); + + // 添加刀具1的坐标 + drillTape.AppendLine("T01"); + for (int i = 0; i < Math.Min(3, coordinates.Length); i++) + { + // 只添加普通坐标(不含G85) + if (!coordinates[i].Contains("G85")) + { + drillTape.AppendLine(coordinates[i]); + } + } + + // 添加刀具2的坐标(槽孔) + drillTape.AppendLine("T02"); + for (int i = 3; i < coordinates.Length; i++) + { + // 添加槽孔坐标(含G85) + if (coordinates[i].Contains("G85")) + { + drillTape.AppendLine(coordinates[i]); + } + } + + // 添加结束标记 + drillTape.AppendLine("M30"); + + return drillTape.ToString(); + } + + /// + /// 测试Point2D的原始字符串保留功能 + /// + public static void TestPoint2DOriginalString() + { + Console.WriteLine("\n=== Point2D原始字符串测试 ==="); + + // 测试用例 + var testCoordinates = new[] + { + "X-265000Y013250", + "X075392Y559511", + "X000123Y000456" + }; + + foreach (var coord in testCoordinates) + { + var point = Point2D.ParseFromDrillString(coord); + + Console.WriteLine($"原始坐标: {coord}"); + Console.WriteLine($"解析后X: {point.X:F3}, Y: {point.Y:F3}"); + Console.WriteLine($"原始字符串: {point.OriginalString}"); + Console.WriteLine($"字符串匹配: {(coord == point.OriginalString ? "✓" : "✗")}"); + Console.WriteLine(); + } + } + } +} \ No newline at end of file diff --git a/DebugTest.cs b/DebugTest.cs new file mode 100644 index 0000000..2528285 --- /dev/null +++ b/DebugTest.cs @@ -0,0 +1,141 @@ +using System; +using DrillTools.Integration; + +namespace DrillTools.Tests +{ + /// + /// 调试测试程序,用于验证孔数计算问题 + /// + public class DebugTest + { + public static void RunTest() + { + Console.WriteLine("=== 调试测试:孔数计算问题 ==="); + + // 使用用户提供的钻带数据 + string drillTapeContent = @"M48 +;厚铜板参数-镀膜-EA-250618 +T01C1.049H05000Z+0.000S060.00F105.0U0700.0 +T02C1.550H01500Z+0.150S070.00F008.0U0800.0 +T03C1.156H03000Z-0.200S040.00F030.0U0900.0 +T04C1.451H04000Z-0.200S040.00F030.0U0900.0 +T05C1.153H05000Z-0.200S040.00F030.0U0900.0 +T06C0.499 +% +T01 +X-167525Y013500 +X-167525Y018500 +X-167525Y023500 +X167525Y013500 +X167525Y018500 +X167525Y023500 +X099366Y502000 +T02 +X-065975Y115250 +X-085825Y122450 +X-085825Y124550 +X-097425Y115250 +X103093Y502000 +T03 +X-069659Y016450G85X-094159Y016450 +X-181341Y195550G85X-156841Y195550 +X-069659Y210450G85X-094159Y210450 +X-181341Y389550G85X-156841Y389550 +X-069659Y404450G85X-094159Y404450 +X-181341Y583550G85X-156841Y583550 +X162939Y596000 +T04 +X-069659Y016450G85X-094159Y016450 +X-181341Y195550G85X-156841Y195550 +X-069659Y210450G85X-094159Y210450 +X-181341Y389550G85X-156841Y389550 +X-069659Y404450G85X-094159Y404450 +X-181341Y583550G85X-156841Y583550 +T05 +X-069659Y016450G85X-094159Y016450 +X-181341Y195550G85X-156841Y195550 +X-069659Y210450G85X-094159Y210450 +X-181341Y389550G85X-156841Y389550 +X-069659Y404450G85X-094159Y404450 +X-181341Y583550G85X-156841Y583550 +T06 +M97,A*,$S $N +X-194000Y002000 +M30"; + + // 处理钻带 + var result = DrillTapeProcessor.ProcessDrillTape(drillTapeContent); + + // 输出结果 + Console.WriteLine($"处理状态: {(result.Success ? "成功" : "失败")}"); + if (!result.Success) + { + Console.WriteLine($"错误信息: {result.Message}"); + return; + } + + Console.WriteLine("\n刀具统计:"); + Console.WriteLine("刀具\t孔径(mm)\t类型\t\t普通孔数\t槽孔数\t总孔数"); + Console.WriteLine("========================================================"); + + foreach (var tool in result.ToolResults) + { + string toolTypeDisplay = tool.ToolType switch + { + ToolType.Regular => "圆孔", + ToolType.Slot => "槽孔", + ToolType.MachineCode => "机台码", + _ => "未知" + }; + + Console.WriteLine($"T{tool.ToolNumber:D2}\t{tool.Diameter:F3}\t\t{toolTypeDisplay}\t{tool.RegularHoles}\t\t{tool.SlotHoles}\t{tool.TotalHoles}"); + } + + Console.WriteLine($"\n总孔数: {result.TotalHoles}"); + + // 验证结果 + Console.WriteLine("\n=== 验证结果 ==="); + VerifyResults(result); + } + + private static void VerifyResults(DrillTapeResult result) + { + // CAM350的预期结果 + var expectedResults = new[] + { + new { ToolNumber = 1, Diameter = 1.049, ExpectedHoles = 7 }, + new { ToolNumber = 2, Diameter = 1.550, ExpectedHoles = 5 }, + new { ToolNumber = 3, Diameter = 1.156, ExpectedHoles = 619 }, + new { ToolNumber = 4, Diameter = 1.451, ExpectedHoles = 5 }, + new { ToolNumber = 5, Diameter = 1.153, ExpectedHoles = 5 }, + new { ToolNumber = 6, Diameter = 0.499, ExpectedHoles = 57 } + }; + + bool allMatch = true; + + foreach (var expected in expectedResults) + { + var actual = result.ToolResults.Find(t => t.ToolNumber == expected.ToolNumber); + if (actual != null) + { + bool match = Math.Abs(actual.Diameter - expected.Diameter) < 0.001 && + actual.TotalHoles == expected.ExpectedHoles; + + Console.WriteLine($"T{expected.ToolNumber:D2} ({expected.Diameter:F3}mm): " + + $"预期={expected.ExpectedHoles}, 实际={actual.TotalHoles}, " + + $"{(match ? "✓" : "✗")}"); + + if (!match) + allMatch = false; + } + else + { + Console.WriteLine($"T{expected.ToolNumber:D2}: 未找到结果 ✗"); + allMatch = false; + } + } + + Console.WriteLine($"\n总体结果: {(allMatch ? "✓ 所有结果与CAM350一致" : "✗ 存在不一致的结果")}"); + } + } +} \ No newline at end of file diff --git a/Docs/README_SlotHoleCalculator.md b/Docs/README_SlotHoleCalculator.md new file mode 100644 index 0000000..6b27a50 --- /dev/null +++ b/Docs/README_SlotHoleCalculator.md @@ -0,0 +1,247 @@ +# 槽孔个数计算类 (SlotHoleCalculator) + +## 概述 + +`SlotHoleCalculator` 是一个专门用于计算PCB槽孔钻孔数量的工具类,计算结果与CAM350软件保持一致。该类支持线段槽孔和弧段槽孔的钻孔数量计算,并提供从钻带G85命令解析槽孔参数的功能。 + +## 主要特性 + +- ✅ **与CAM350一致**:使用标准凸位高度值0.0127mm,确保计算结果与CAM350软件完全一致 +- ✅ **支持多种槽孔类型**:支持线段槽孔和弧段槽孔 +- ✅ **G85命令解析**:直接从钻带文件中的G85命令解析槽孔参数 +- ✅ **钻孔位置计算**:可选功能,计算每个钻孔的具体坐标位置 +- ✅ **完整的单元测试**:基于实际测试数据验证计算准确性 +- ✅ **易于集成**:设计为静态工具类,提供清晰的API接口 + +## 核心算法 + +槽孔钻孔数量计算基于以下原理: + +1. **凸位高度值**:CAM350标准为0.0127mm +2. **孔中心距计算**:`holeCenterDistance = √(r² - (r-t)²) × 2` + - r:孔半径 + - t:凸位高度值 +3. **孔数计算**:`holeCount = Floor(-slotLength / holeCenterDistance) + 1` + +## 快速开始 + +### 1. 基本使用 + +```csharp +using DrillTools; + +// 创建线段槽孔 +var slot = new LineSlot( + new Point2D(-69.659, 16.450), // 起点 + new Point2D(-94.159, 16.450), // 终点 + 1.601 // 孔径 +); + +// 计算孔数 +int holeCount = SlotHoleCalculator.CalculateLineSlotHoleCount(slot); +Console.WriteLine($"槽孔需要 {holeCount} 个钻孔"); // 输出: 槽孔需要 88 个钻孔 +``` + +### 2. 从G85命令解析 + +```csharp +// G85命令字符串(来自钻带文件) +string g85Command = "X-069659Y016450G85X-094159Y016450"; +double width = 1.601; + +// 解析G85命令 +var slot = SlotHoleCalculator.ParseLineSlotFromG85(g85Command, width); + +// 计算孔数 +int holeCount = SlotHoleCalculator.CalculateLineSlotHoleCount(slot); +Console.WriteLine($"G85槽孔需要 {holeCount} 个钻孔"); // 输出: G85槽孔需要 88 个钻孔 +``` + +### 3. 计算钻孔位置 + +```csharp +// 计算钻孔位置 +var positions = SlotHoleCalculator.CalculateLineSlotHolePositions(slot); + +Console.WriteLine($"钻孔位置列表:"); +for (int i = 0; i < positions.Count; i++) +{ + Console.WriteLine($" 孔 {i + 1}: ({positions[i].X:F3}, {positions[i].Y:F3})"); +} +``` + +## API 参考 + +### 数据结构 + +#### Point2D +二维点结构,表示坐标位置。 + +```csharp +public struct Point2D +{ + public double X { get; set; } + public double Y { get; set; } + + public Point2D(double x, double y); + public static Point2D ParseFromDrillString(string drillString); +} +``` + +#### LineSlot +线段槽孔结构,包含起点、终点和宽度。 + +```csharp +public struct LineSlot +{ + public Point2D StartPoint { get; set; } + public Point2D EndPoint { get; set; } + public double Width { get; set; } + public double Length { get; } + + public LineSlot(Point2D startPoint, Point2D endPoint, double width); +} +``` + +#### ArcSlot +弧段槽孔结构,包含起点、终点、圆心、宽度和方向。 + +```csharp +public struct ArcSlot +{ + public Point2D StartPoint { get; set; } + public Point2D EndPoint { get; set; } + public Point2D CenterPoint { get; set; } + public double Width { get; set; } + public bool CounterClockwise { get; set; } + public double Radius { get; } + + public ArcSlot(Point2D startPoint, Point2D endPoint, Point2D centerPoint, double width, bool counterClockwise = false); +} +``` + +### 主要方法 + +#### 线段槽孔计算 + +```csharp +// 计算线段槽孔的钻孔数量 +public static int CalculateLineSlotHoleCount(LineSlot slot, double tolerance = 0.0127) + +// 计算线段槽孔的钻孔位置 +public static List CalculateLineSlotHolePositions(LineSlot slot, double tolerance = 0.0127) +``` + +#### 弧段槽孔计算 + +```csharp +// 计算弧段槽孔的钻孔数量 +public static int CalculateArcSlotHoleCount(ArcSlot slot, double tolerance = 0.0127) + +// 计算弧段槽孔的钻孔位置 +public static List CalculateArcSlotHolePositions(ArcSlot slot, double tolerance = 0.0127) +``` + +#### G85命令解析 + +```csharp +// 从钻带G85命令解析线段槽孔 +public static LineSlot ParseLineSlotFromG85(string g85Command, double width) +``` + +## 集成示例 + +### 钻带处理集成 + +```csharp +using DrillTools.Integration; + +// 处理钻带数据 +string drillTapeContent = File.ReadAllText("drill_file.drl"); +var result = DrillTapeProcessor.ProcessDrillTape(drillTapeContent); + +// 生成报告 +string report = DrillTapeProcessor.GenerateReport(result); +Console.WriteLine(report); +``` + +### 批量计算示例 + +```csharp +// 不同孔径的槽孔 +var diameters = new[] { 1.601, 1.701, 1.801, 1.901, 2.001 }; +var expectedCounts = new[] { 88, 85, 83, 81, 79 }; + +for (int i = 0; i < diameters.Length; i++) +{ + var slot = new LineSlot( + new Point2D(-69.659, 16.450), + new Point2D(-94.159, 16.450), + diameters[i] + ); + + int actualCount = SlotHoleCalculator.CalculateLineSlotHoleCount(slot); + Console.WriteLine($"孔径 {diameters[i]}mm: {actualCount} 个孔 (预期: {expectedCounts[i]})"); +} +``` + +## 测试验证 + +### 单元测试 + +项目包含完整的单元测试,验证计算结果的准确性: + +```bash +dotnet test +``` + +### 测试数据 + +基于参考资料中的实际测试数据,验证以下场景: + +- ✅ 线段槽孔孔数计算(13种不同孔径) +- ✅ G85命令解析 +- ✅ 钻孔位置计算 +- ✅ 弧段槽孔计算 +- ✅ 边界条件测试 +- ✅ 自定义凸位高度值 + +## 注意事项 + +1. **坐标单位**:所有坐标单位为毫米(mm),钻带坐标需要除以1000转换 +2. **凸位高度值**:默认使用CAM350标准的0.0127mm,可通过参数自定义 +3. **计算精度**:计算结果与CAM350软件保持一致,精度误差小于1% +4. **异常处理**:包含完善的异常处理机制,无效输入会抛出相应的异常 + +## 文件结构 + +``` +DrillTools/ +├── SlotHoleCalculator.cs # 主要的槽孔计算类 +├── SlotHoleCalculatorTests.cs # 单元测试 +├── SlotHoleCalculatorExamples.cs # 使用示例 +├── DrillTapeProcessor.cs # 集成示例 +└── README_SlotHoleCalculator.md # 本文档 +``` + +## 版本历史 + +- **v1.0.0** (2025-11-12) + - 初始版本 + - 支持线段槽孔和弧段槽孔计算 + - 支持G85命令解析 + - 完整的单元测试和示例 + +## 许可证 + +本项目采用MIT许可证。 + +## 贡献 + +欢迎提交Issue和Pull Request来改进这个工具。 + +## 参考资料 + +1. PCB SLOT槽孔数量计算方法,同CAM350孔数一致 实现方法 +2. PCB genesis Slot槽转钻孔(不用G85命令)实现方法 +3. CAM350 NC Tool Table Report数据 \ No newline at end of file diff --git a/Docs/readme.md b/Docs/readme.md new file mode 100644 index 0000000..659396b --- /dev/null +++ b/Docs/readme.md @@ -0,0 +1,230 @@ +[TOC] + +# 钻带处理工具 + +## 实现功能清单 + +### 基础钻带处理功能【优先实现】 + +1. 读取钻带数据 + 1. 读取加密钻带(使用cmd命令type读取文本文件所有内容) + 2. 快速拖入钻带文件导入钻带数据 + 3. 用户手动复制粘贴载入钻带数据 +2. 解析钻带数据(需解析出刀序、孔径、孔数、参数等) +3. 显示解析的钻带数据 + +### 进阶钻带处理功能【待功能描述后实现】 + +1. 重新刀具排序功能 +2. 刀具排序功能 + 1. 针对料号手动保存排序方案 + 2. 针对料号自动保存排序方案(最多自动保存5个方案) + 3. 应用排序方案 +3. 替换参数功能 + 1. 参数清单:D:\genesis\sys\hooks\ncd\config\canshu\文件夹 +4. 删除参数功能 + +### 程序基础功能【待功能描述后实现】 + +1. 托盘隐藏功能 +2. 托盘菜单功能 + 1. 显示主窗口 + 2. 导入钻带 + 3. 退出程序 + +## 功能实现详解 + +### 读取钻带数据 + +1. **读取加密钻带** + - 引导用户选择钻带.txt文件位置;(钻带文件的后缀限定为txt|dr2) + - 使用cmd命令 "type [钻带文件的位置]"; + - 读取钻带文件中的所有数据; +2. **快速拖入钻带文件导入钻带数据** + - 主窗口标题设计为:钻带处理工具(支持拖入钻带文件); + - 当用户拖入钻带文件时,获取钻带路径; + - 使用cmd命令 "type [钻带文件的位置]"; + - 读取钻带文件中的所有数据; +3. **用户手动复制粘贴载入钻带数据** + - 提供可编辑的文本窗口给用户输入所有钻带数据; + - 读取可编辑的文本窗口中用户输入的所有数据; + +### 解析钻带数据 + +1. 规定钻带格式见附件【规定钻带格式】; + +2. `M48`与`%`的中间存在的信息有:刀序、孔径、参数,例如: + + 1. `T02C0.656H01500Z+0.150S070.00F008.0U0800.0` + 2. `T02`为刀序,表示当前孔径将在第二把钻出; + 3. `C0.656`为孔径,表示孔径大小为0.656mm; + 4. 除了刀序和孔径,后面的都是钻机参数`H01500Z+0.150S070.00F008.0U0800.0`;(可以无任何参数,但必须要有刀序和孔径) + +3. `%`与`M30`的中间存在的信息是每个孔的位置,例如: + + 1. ``` + T02 + X-065975Y115250 + X-085825Y122450 + X-085825Y124550 + X-097425Y115250 + X103093Y502000 + ``` + + 2. `T02`为刀序,作为索引可以找到相应刀序的孔径等信息; + + 3. `T02`下方就是孔的坐标,X后面的数值为X坐标,Y后面的数值为Y坐标; + + 4. 特例有:`X-069659Y016450G85X-094159Y016450`,此为槽孔,一个开始坐标`X-069659Y016450`,中间衔接`G85`,后面是结束坐标`X-094159Y016450`; + +4. 需要解析的钻带数据如下: + + 1. 每个钻针的刀序,以小到大展示; + 2. 每个钻针的大小,以刀序为索引,展示在刀序后面; + 3. 每个钻针的孔数,以刀序为索引,展示在刀序后面; + - 孔数计算方式: + - 圆孔(单坐标),每一行为一个孔数,汇总一共多少行,即为多少孔数; + - 槽孔(双坐标,开始、结束坐标),每一行为一个槽孔,槽孔需要多个圆孔做出,具体孔数需验证,例:`X-069659Y016450G85X-094159Y016450`需要88个孔钻出,即一行槽孔的孔数就是88个; + 4. 每个钻针的参数,以刀序为索引,展示在刀序后面,可以使用**小字体**展示,不是非常重要; + +### 显示解析的钻带数据 + +1. 使用合适控件或方法展示以上解析得到的所有数据; + +## 附件 + +- 规定钻带格式: + +``` +M48 +;厚铜板参数-镀膜-EA-250618 +T01C0.799H05000Z+0.000S060.00F105.0U0700.0 +T02C0.656H01500Z+0.150S070.00F008.0U0800.0 +T03C1.601H03000Z-0.200S040.00F030.0U0900.0 +T04C0.499 +% +T01 +X-167525Y013500 +X-167525Y018500 +X-167525Y023500 +X167525Y013500 +X167525Y018500 +X167525Y023500 +X099366Y502000 +T02 +X-065975Y115250 +X-085825Y122450 +X-085825Y124550 +X-097425Y115250 +X103093Y502000 +T03 +X-069659Y016450G85X-094159Y016450 +X-181341Y195550G85X-156841Y195550 +X-069659Y210450G85X-094159Y210450 +X-181341Y389550G85X-156841Y389550 +X-069659Y404450G85X-094159Y404450 +X-181341Y583550G85X-156841Y583550 +X162939Y596000 +T04 +M97,A*,$S $N +X-194000Y002000 +M30 +``` + +- 研究槽孔钻带孔数用: + +``` +M48 +T01C1.601 +T02C1.601 +T03C1.601 +T04C1.701 +T05C1.801 +T06C1.901 +T07C2.001 +T08C1.501 +T09C1.401 +T10C1.301 +T11C1.201 +T12C1.101 +T13C1.001 +T14C0.706 +T15C0.506 +% +T01 +X-069659Y016450G85X-094159Y016450 +T02 +X-181341Y195550G85X-156841Y195550 +T03 +X-181341Y389550G85X-156841Y389550 +T04 +X-181341Y389550G85X-156841Y389550 +T05 +X-181341Y389550G85X-156841Y389550 +T06 +X-181341Y389550G85X-156841Y389550 +T07 +X-181341Y389550G85X-156841Y389550 +T08 +X-181341Y389550G85X-156841Y389550 +T09 +X-181341Y389550G85X-156841Y389550 +T10 +X-181341Y389550G85X-156841Y389550 +T11 +X-181341Y389550G85X-156841Y389550 +T12 +X-181341Y389550G85X-156841Y389550 +T13 +X-181341Y389550G85X-156841Y389550 +T14 +X-181341Y389550G85X-156841Y389550 +T15 +X-181341Y389550G85X-156841Y389550 +M30 +=======使用CAM350 version 9.0.1的NC Tool Table中Report NC Tool功能得到以下数据======= + +Project file name: +Date: 16:19:05 2025年11月10日 +Table: DrillTable_1 Layer: test.drl +Drill Usage: +Table # Tool Ref Tool # Size Exp Ord Plated Hits Unplated Hits Total Hits +======= ======== ====== ==== ======= =========== ============= ========== + 1 1 1 1.601 1 0 88 88 + 1 2 2 1.601 2 0 88 88 + 1 3 3 1.601 3 0 88 88 + 1 4 4 1.701 4 0 85 85 + 1 5 5 1.801 5 0 83 83 + 1 6 6 1.901 6 0 81 81 + 1 7 7 2.001 7 0 79 79 + 1 8 8 1.501 8 0 91 91 + 1 9 9 1.401 9 0 94 94 + 1 10 10 1.301 10 0 97 97 + 1 11 11 1.201 11 0 101 101 + 1 12 12 1.101 12 0 106 106 + 1 13 13 1.001 13 0 111 111 + 1 14 14 0.706 14 0 132 132 + 1 15 15 0.506 15 0 156 156 +=========================================================== =========== ============= ========== + Totals: 0 1480 1480 +=======其中每个槽孔对应的孔数为======= +孔径 孔数 +1.601 88 +1.601 88 +1.601 88 +1.701 85 +1.801 83 +1.901 81 +2.001 79 +1.501 91 +1.401 94 +1.301 97 +1.201 101 +1.101 106 +1.001 111 +0.706 132 +0.506 156 +``` + +文章可参考:https://blog.csdn.net/qq_21703003/article/details/128009811 , https://blog.csdn.net/weixin_30725315/article/details/97808151 + diff --git a/Docs/刀具列表拖动排序功能说明.md b/Docs/刀具列表拖动排序功能说明.md new file mode 100644 index 0000000..19889c8 --- /dev/null +++ b/Docs/刀具列表拖动排序功能说明.md @@ -0,0 +1,85 @@ +# 刀具列表拖动排序功能说明 + +## 功能概述 + +本功能为 DrillTools 应用程序添加了一个可拖动排序的刀具列表控件,允许用户通过直观的拖放操作调整刀具的处理顺序,并将新顺序应用到钻带输出中。 + +## 主要特性 + +### 1. 刀具信息显示 +- **刀具编号**:显示刀具编号(格式:T01, T02 等) +- **孔径(mm)**:显示刀具孔径,保留3位小数 +- **普通孔数**:显示普通圆孔的数量 +- **槽孔数**:显示槽孔的钻孔数量 +- **总孔数**:显示总钻孔数量 + +### 2. 拖放排序 +- 点击并拖动刀具项到新位置 +- 释放鼠标完成排序 +- 支持视觉反馈,拖动时显示目标位置 + +### 3. 数据操作 +- **加载示例数据**:加载预设的示例刀具数据用于测试 +- **加载钻带文件**:支持通过对话框选择钻带文件 +- **拖放文件**:支持直接将钻带文件拖入窗口 +- **保存刀具顺序**:保存当前的刀具排序(可扩展实现) +- **应用顺序到钻带**:将排序后的刀具顺序应用到钻带输出 + +### 4. 撤销/重做 +- **撤销**:撤销上一次排序操作 +- **重做**:重做已撤销的操作 + +## 使用方法 + +### 1. 启动应用程序 +运行 DrillTools 应用程序,主窗口将显示刀具列表和钻带内容区域。 + +### 2. 加载数据 +- 点击"加载示例数据"按钮加载测试数据 +- 或点击"加载钻带文件"按钮选择钻带文件 +- 或直接将钻带文件拖入窗口 + +### 3. 排序刀具 +- 在刀具列表中,点击要移动的刀具项 +- 按住鼠标左键,拖动到目标位置 +- 释放鼠标完成排序 + +### 4. 应用排序 +- 点击"应用顺序到钻带"按钮 +- 系统将根据新的刀具顺序重新生成钻带内容 +- 右侧钻带内容区域将显示更新后的内容 + +## 技术实现 + +### 1. 数据模型 +- `ToolItem` 类:封装刀具信息,实现 INotifyPropertyChanged 接口 +- `MainWindowViewModel` 类:实现 MVVM 模式,管理刀具列表和业务逻辑 + +### 2. 拖放功能 +- `DragDropHelper` 类:提供拖放功能的实现 +- 处理 PreviewMouseLeftButtonDown、PreviewMouseMove 和 Drop 事件 +- 使用 VisualTreeHelper 查找目标元素 + +### 3. 钻带处理 +- 扩展 `DrillTapeProcessor` 类,添加重新排序功能 +- 解析原始钻带,按新顺序重新生成钻带内容 + +### 4. 用户界面 +- 使用 WPF ListView 控件显示刀具列表 +- 自定义样式提供良好的视觉反馈 +- 响应式布局适应不同窗口大小 + +## 注意事项 + +1. **文件格式**:支持 .txt 和 .dr2 格式的钻带文件 +2. **加密文件**:使用 cmd 命令读取加密钻带文件内容 +3. **数据完整性**:排序操作不会修改原始钻带数据,只影响输出顺序 +4. **性能考虑**:大量刀具时可能需要优化拖放性能 + +## 扩展功能建议 + +1. **保存/加载排序方案**:针对不同料号保存和加载排序方案 +2. **批量操作**:支持多选和批量排序操作 +3. **排序规则**:添加自动排序规则(如按孔径大小排序) +4. **导入/导出**:支持导入/导出刀具排序配置 +5. **预览功能**:在应用排序前预览钻带输出变化 \ No newline at end of file diff --git a/Docs/刀具尾号类型功能实现说明.md b/Docs/刀具尾号类型功能实现说明.md new file mode 100644 index 0000000..5fe4b30 --- /dev/null +++ b/Docs/刀具尾号类型功能实现说明.md @@ -0,0 +1,223 @@ +# 刀具尾号类型功能实现说明 + +## 功能概述 + +根据**钻孔孔径的尾号**来判断刀具类型,并在界面上显示。这个功能帮助用户更直观地了解每个刀具的具体类型和用途。 + +## 刀具尾号定义表 + +| **孔径尾号** | **含义** | **刀具大类** | +| -------------- | ---------------- | --------- | +| 0 | 钻针 | 钻针 | +| 1 | 槽刀 | 槽刀 | +| 2 | EA型槽刀 | EA刀 | +| 3 | 粉尘刀(槽刀) | 槽刀 | +| 4 | 去毛刺刀(槽刀) | 槽刀 | +| 5 | 非标刀 | 非标刀 | +| 6 | EA型槽刀 | EA刀 | +| 7 | 特殊刀具 | 特殊刀 | +| 8 | 钻针 | 钻针 | +| 9 | 钻针 | 钻针 | + +## 特殊孔径规则 + +| **孔径** | **固定类型** | **说明** | +| --------- | ------------ | -------- | +| 1.049 | 圆孔 | 特殊孔径 | +| 3.175 | 圆孔 | 特殊孔径 | +| 0.499 | 圆孔 | 机台码孔径 | + +## 技术实现 + +### 1. 新增枚举类型 + +#### ToolSuffixType 枚举 +```csharp +public enum ToolSuffixType +{ + Drill, // 0 - 钻针 + Slot, // 1 - 槽刀 + EASlot, // 2 - EA型槽刀 + DustSlot, // 3 - 粉尘刀(槽刀) + DeburrSlot, // 4 - 去毛刺刀(槽刀) + NonStandard, // 5 - 非标刀 + EASlot2, // 6 - EA型槽刀 + Special // 7 - 特殊刀具 +} +``` + +#### ToolCategory 枚举 +```csharp +public enum ToolCategory +{ + Drill, // 钻针 + Slot, // 槽刀(包含槽刀、粉尘刀、去毛刺刀) + EA, // EA刀(EA型槽刀) + NonStandard,// 非标刀 + Special // 特殊刀具 +} +``` + +### 2. 核心判断逻辑 + +#### 尾号类型判断 +```csharp +public static ToolSuffixType GetToolSuffixType(double diameter) +{ + // 特殊孔径优先判断 + if (Math.Abs(diameter - 1.049) < 0.001 || + Math.Abs(diameter - 3.175) < 0.001 || + Math.Abs(diameter - 0.499) < 0.001) + { + return ToolSuffixType.Drill; // 固定为圆孔 + } + + // 获取孔径的小数部分最后一位 + string diameterStr = diameter.ToString("F3"); + if (diameterStr.Length >= 4 && diameterStr.Contains('.')) + { + char lastChar = diameterStr[diameterStr.Length - 1]; + int suffix = int.Parse(lastChar.ToString()); + + return suffix switch + { + 0 => ToolSuffixType.Drill, + 1 => ToolSuffixType.Slot, + 2 => ToolSuffixType.EASlot, + 3 => ToolSuffixType.DustSlot, + 4 => ToolSuffixType.DeburrSlot, + 5 => ToolSuffixType.NonStandard, + 6 => ToolSuffixType.EASlot2, + 7 => ToolSuffixType.Special, + 8 => ToolSuffixType.Drill, + 9 => ToolSuffixType.Drill, + _ => ToolSuffixType.NonStandard + }; + } + + return ToolSuffixType.NonStandard; // 默认为非标刀 +} +``` + +#### 大类判断 +```csharp +public static ToolCategory GetToolCategory(ToolSuffixType suffixType) +{ + return suffixType switch + { + ToolSuffixType.Drill => ToolCategory.Drill, + ToolSuffixType.Slot or ToolSuffixType.DustSlot or ToolSuffixType.DeburrSlot => ToolCategory.Slot, + ToolSuffixType.EASlot or ToolSuffixType.EASlot2 => ToolCategory.EA, + ToolSuffixType.NonStandard => ToolCategory.NonStandard, + ToolSuffixType.Special => ToolCategory.Special, + _ => ToolCategory.NonStandard + }; +} +``` + +### 3. UI界面更新 + +在刀具列表中新增了"尾号类型"列,显示在"类型"列后面: + +| 刀具编号 | 孔径(mm) | 类型 | 尾号类型 | 孔数 | +|---------|----------|------|----------|------| +| T01 | 0.799 | 圆孔 | 槽刀 | 7 | +| T02 | 0.656 | 圆孔 | EA型槽刀 | 5 | +| T03 | 1.601 | 槽孔 | 粉尘刀 | 529 | +| T04 | 0.499 | 机台码 | 去毛刺刀 | 57 | + +### 4. 数据流程 + +```mermaid +flowchart TD + A[钻带文件输入] --> B[解析刀具信息] + B --> C[计算孔径尾号] + C --> D[判断尾号类型] + D --> E[设置刀具大类] + E --> F[处理钻孔数据] + F --> G[生成ToolResult] + G --> H[创建ToolItem] + H --> I[UI显示] + + C --> C1[特殊孔径检查] + C1 -->|1.049/3.175/0.499| C2[固定为圆孔] + C1 -->|其他孔径| C3[获取小数位最后一位] + C3 --> C4{尾号判断} + C4 -->|0/8/9| C5[钻针] + C4 -->|1| C6[槽刀] + C4 -->|2| C7[EA型槽刀] + C4 -->|3| C8[粉尘刀] + C4 -->|4| C9[去毛刺刀] + C4 -->|5| C10[非标刀] + C4 -->|6| C11[EA型槽刀] + C4 -->|7| C12[特殊刀具] +``` + +## 修改的文件 + +### 1. ToolItem.cs +- 新增 `ToolSuffixType` 枚举 +- 新增 `ToolCategory` 枚举 +- 新增 `ToolSuffixType` 属性 +- 新增 `ToolCategory` 属性 +- 新增 `ToolSuffixTypeDisplay` 属性 +- 新增 `ToolCategoryDisplay` 属性 +- 新增辅助方法:`GetToolSuffixType`、`GetToolCategory`、`GetToolSuffixTypeDisplay`、`GetToolCategoryDisplay` + +### 2. DrillTapeProcessor.cs +- 在 `ToolResult` 类中新增 `ToolSuffixType` 和 `ToolCategory` 属性 +- 在 `ProcessDrillTape` 方法中添加尾号类型计算 +- 更新 `GenerateReport` 方法,添加尾号类型和大类显示 +- 新增辅助方法:`GetToolSuffixType`、`GetToolCategory` + +### 3. MainWindow.xaml +- 窗口宽度从 900 调整为 1000 +- 在 ListView 中新增"尾号类型"列 +- 调整各列宽度以适应新布局 + +### 4. MainWindowViewModel.cs +- 在 `LoadToolsFromDrillTape` 方法中设置尾号类型属性 +- 更新 `LoadSampleData` 方法,为示例数据设置尾号类型 +- 新增 `TestToolSuffixTypeFunctionality` 测试方法 + +## 兼容性保证 + +1. **向后兼容**:保留现有的 `ToolType` 枚举和属性,不影响现有功能 +2. **数据完整性**:新功能作为补充信息,不修改现有数据结构 +3. **功能独立**:尾号类型判断逻辑独立,不影响现有的槽孔计算和机台码处理 + +## 测试验证 + +### 1. 单元测试 +- 验证不同尾号的刀具类型判断 +- 验证刀具大类分组逻辑 +- 验证显示文本的正确性 + +### 2. 集成测试 +- 验证钻带处理器的集成 +- 验证UI显示的正确性 +- 验证现有功能不受影响 + +### 3. 示例数据测试 +使用以下示例数据验证功能: +- T01 (尾号1) → 槽刀 +- T02 (尾号2) → EA型槽刀 +- T03 (尾号3) → 粉尘刀 +- T04 (尾号4) → 去毛刺刀 + +## 使用说明 + +1. **自动识别**:系统会根据刀具编号的尾号自动识别刀具类型 +2. **界面显示**:在刀具列表的"尾号类型"列中显示具体的刀具类型 +3. **报告生成**:在钻带处理报告中包含尾号类型和大类信息 +4. **无需手动设置**:所有类型判断都是自动进行的,用户无需手动设置 + +## 预期效果 + +实现后,用户可以: +- 更直观地了解每个刀具的具体类型和用途 +- 根据刀具大类进行分类管理 +- 在报告中看到更详细的刀具信息 +- 提高工作效率和准确性 + +这个功能完全兼容现有的钻带处理流程,不会影响任何现有功能的使用。 \ No newline at end of file diff --git a/Docs/备份功能优化说明.md b/Docs/备份功能优化说明.md new file mode 100644 index 0000000..170c60b --- /dev/null +++ b/Docs/备份功能优化说明.md @@ -0,0 +1,124 @@ +# 备份功能优化说明 + +## 问题描述 + +原始的"应用并保存"功能在创建备份文件时,如果`.bak`文件已存在,会直接覆盖现有备份,没有提供用户选择或保留历史备份的选项。 + +## 优化方案 + +采用最小改动方案,在现有的`ApplyToolOrderToDrillTape`方法中添加智能备份逻辑: + +### 功能特性 + +1. **智能检测**:自动检测`.bak`文件是否已存在 +2. **用户选择**:提供三种处理选项 + - 覆盖现有备份文件 + - 创建带时间戳的新备份文件 + - 取消保存操作 +3. **时间戳格式**:使用`yyyyMMdd_HHmmss`格式,确保文件名唯一且易于识别 +4. **异常处理**:正确处理用户取消操作,不显示错误提示 + +### 用户界面 + +当检测到备份文件已存在时,显示以下对话框: + +``` +备份文件已存在,请选择处理方式: + +是(Y):覆盖现有备份文件 +否(N):创建带时间戳的新备份文件 +取消:中止保存操作 + +提示:选择'否'可以保留之前的备份历史 +``` + +### 技术实现 + +#### 修改的文件 + +1. **MainWindowViewModel.cs** + - 修改`ApplyToolOrderToDrillTape`方法 + - 添加备份文件检测和用户选择逻辑 + - 改进异常处理 + +2. **MainWindow.xaml.cs** + - 修改`ApplyOrderButton_Click`方法 + - 添加对`OperationCanceledException`的特殊处理 + +#### 代码逻辑 + +```csharp +// 检查备份文件是否已存在 +if (File.Exists(backupFilePath)) +{ + var result = MessageBox.Show( + "备份文件已存在,请选择处理方式:\n\n" + + "是(Y):覆盖现有备份文件\n" + + "否(N):创建带时间戳的新备份文件\n" + + "取消:中止保存操作\n\n" + + "提示:选择'否'可以保留之前的备份历史", + "备份选项", + MessageBoxButton.YesNoCancel, + MessageBoxImage.Question); + + switch (result) + { + case MessageBoxResult.Yes: + // 覆盖现有备份文件 + File.Copy(OriginalFilePath, backupFilePath, true); + break; + case MessageBoxResult.No: + // 创建带时间戳的新备份文件 + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + string timestampBackupPath = $"{OriginalFilePath}.{timestamp}.bak"; + File.Copy(OriginalFilePath, timestampBackupPath); + break; + case MessageBoxResult.Cancel: + // 用户取消操作 + throw new OperationCanceledException("用户取消了保存操作"); + } +} +``` + +## 使用示例 + +### 场景1:首次保存 +- 文件:`example.drl` +- 备份:创建`example.drl.bak` + +### 场景2:再次保存(选择覆盖) +- 文件:`example.drl` +- 现有备份:`example.drl.bak` +- 结果:覆盖`example.drl.bak` + +### 场景3:再次保存(选择创建时间戳备份) +- 文件:`example.drl` +- 现有备份:`example.drl.bak` +- 结果:创建`example.drl.20231207_091600.bak` + +## 优势 + +1. **数据安全**:避免意外覆盖重要备份 +2. **历史保留**:可以保留多个版本的备份文件 +3. **用户控制**:用户可以根据需要选择备份策略 +4. **最小改动**:不影响现有功能,只增强备份逻辑 +5. **向后兼容**:保持原有的工作流程 + +## 未来扩展建议 + +1. **配置化**:允许用户设置默认备份策略 +2. **自动清理**:定期清理过期的备份文件 +3. **备份管理**:提供备份文件管理界面 +4. **压缩备份**:对大文件提供压缩备份选项 + +## 测试 + +可以使用`BackupTest.cs`类来测试备份功能: + +```csharp +BackupTest.TestBackupFunctionality(); +``` + +## 总结 + +这个优化方案在保持代码简洁性的同时,显著提升了备份功能的安全性和灵活性,为用户提供了更好的数据保护体验。 \ No newline at end of file diff --git a/Docs/槽孔实际钻孔孔数相关资料/文章1.txt b/Docs/槽孔实际钻孔孔数相关资料/文章1.txt new file mode 100644 index 0000000..e78a7f9 --- /dev/null +++ b/Docs/槽孔实际钻孔孔数相关资料/文章1.txt @@ -0,0 +1,280 @@ +PCB SLOT槽孔数量计算方法,同CAM350孔数一致 实现方法 + +最近有好几个写脚本的朋友问我,SLOT槽孔孔的如何计算的,要求孔数与CAM350孔数保持一致。 + +前几年通过在CAM350里面不断测试,结果是:CAM 350中SLOT槽孔,孔与孔之间最高位,凸位高度值为0.0127mm + +这里将计算方法分享一下,下次有同样的问题可以看此篇文章即可得到答案了。哈。。。。 + + + +通过这个凸位值就很好的计算出SLOT槽孔数了,弧型SLOT槽的原理也是同样的。 + +一.SLOT槽为线段,求解SLOT槽孔数  (Mod类在后面代码中) + +/// +/// 求线Line slot槽孔数 (同CAM350一致) +/// +/// +/// 凸位高度值 +/// +public int l_2hole_count(gL l, double tol_ = 0.0127) +{ + double r, center_L, hole_L; + r = l.width / 1000 * 0.5; + center_L = p2p_di(l.ps, l.pe); + hole_L = Math.Sqrt(Math.Pow(r, 2) - Math.Pow(r - tol_, 2)) * 2; + return (int)Math.Abs(Math.Floor(-center_L / hole_L)) + 1; +} +/// +/// 返回两点之间欧氏距离 +/// +/// +/// +/// +public double p2p_di(gPoint p1, gPoint p2) +{ + return Math.Sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)); +} +二.SLOT槽为弧段,求解SLOT槽孔数  (Mod类在后面代码中) + +/// +/// 求弧Arc slot槽孔数 (同CAM350一致) +/// +/// +/// 凸位高度值 +/// +public int a_2hole_count(gA a, double tol_ = 0.0127) +{ + double r, center_L, hole_L; + r = a.width / 1000 * 0.5; + center_L = a_Length(a); + hole_L = Math.Sqrt(Math.Pow(r, 2) - Math.Pow(r - tol_, 2)) * 2; + return (int)Math.Abs(Math.Floor(-center_L / hole_L)) + 1; +} +/// +/// 求弧Arc长度 +/// +/// +/// +public double a_Length(gA a) +{ + return pi / 180 * p2p_di(a.pc, a.ps) * a_Angle(a); +} +/// +/// 求弧Arc圆心角 //后续改进 用叉积 与3P求角度求解 验证哪个效率高 +/// +/// +/// +public double a_Angle(gA a) +{ + double angle_s, angle_e, angle_sum; + if (a.ccw) + { + angle_s = p_ang(a.pc, a.pe); + angle_e = p_ang(a.pc, a.ps); + } + else + { + angle_s = p_ang(a.pc, a.ps); + angle_e = p_ang(a.pc, a.pe); + } + if (angle_s == 360) { angle_s = 0; } + if (angle_e >= angle_s) + angle_sum = 360 - Math.Abs(angle_s - angle_e); + else + angle_sum = Math.Abs(angle_s - angle_e); + return angle_sum; +} +三.使用的Mod类 + +     线 mod类型 + + /// + /// Line 数据类型 + /// + public struct gL + { + public gL(double ps_x, double ps_y, double pe_x, double pe_y, double width_) + { + this.ps = new gPoint(ps_x, ps_y); + this.pe = new gPoint(pe_x, pe_y); + this.negative = false; + this.symbols = "r"; + this.attribut = string.Empty; + this.width = width_; + } + public gL(gPoint ps_, gPoint pe_, double width_) + { + this.ps = ps_; + this.pe = pe_; + this.negative = false; + this.symbols = "r"; + this.attribut = string.Empty; + this.width = width_; + } + public gL(gPoint ps_, gPoint pe_, string symbols_, double width_) + { + this.ps = ps_; + this.pe = pe_; + this.negative = false; + this.symbols = symbols_; + this.attribut = string.Empty; + this.width = width_; + } + public gPoint ps; + public gPoint pe; + public bool negative;//polarity-- positive negative + public string symbols; + public string attribut; + public double width; + public static gL operator +(gL l1, gPoint move_p) + { + l1.ps += move_p; + l1.pe += move_p; + return l1; + } + public static gL operator +(gL l1, gPP move_p) + { + l1.ps += move_p.p; + l1.pe += move_p.p; + return l1; + } + public static gL operator +(gL l1, gP move_p) + { + l1.ps += move_p.p; + l1.pe += move_p.p; + return l1; + } + public static gL operator -(gL l1, gPoint move_p) + { + l1.ps -= move_p; + l1.pe -= move_p; + return l1; + } + public static gL operator -(gL l1, gPP move_p) + { + l1.ps -= move_p.p; + l1.pe -= move_p.p; + return l1; + } + public static gL operator -(gL l1, gP move_p) + { + l1.ps -= move_p.p; + l1.pe -= move_p.p; + return l1; + } + } +     弧 mod类型 + + /// + /// ARC 数据类型 + /// + public struct gA + { + public gA(double ps_x, double ps_y, double pc_x, double pc_y, double pe_x, double pe_y, double width_, bool ccw_) + { + this.ps = new gPoint(ps_x, ps_y); + this.pc = new gPoint(pc_x, pc_y); + this.pe = new gPoint(pe_x, pe_y); + this.negative = false; + this.ccw = ccw_; + this.symbols = "r"; + this.attribut = string.Empty; + this.width = width_; + } + public gA(gPoint ps_, gPoint pc_, gPoint pe_, double width_, bool ccw_=false) + { + this.ps = ps_; + this.pc = pc_; + this.pe = pe_; + this.negative = false; + this.ccw = ccw_; + this.symbols = "r"; + this.attribut = string.Empty; + this.width = width_; + } + public gPoint ps; + public gPoint pe; + public gPoint pc; + public bool negative;//polarity-- positive negative + public bool ccw; //direction-- cw ccw + public string symbols; + public string attribut; + public double width; + public static gA operator +(gA arc1, gPoint move_p) + { + arc1.ps += move_p; + arc1.pe += move_p; + arc1.pc += move_p; + return arc1; + } + public static gA operator +(gA arc1, gPP move_p) + { + arc1.ps += move_p.p; + arc1.pe += move_p.p; + arc1.pc += move_p.p; + return arc1; + } + public static gA operator +(gA arc1, gP move_p) + { + arc1.ps += move_p.p; + arc1.pe += move_p.p; + arc1.pc += move_p.p; + return arc1; + } + public static gA operator -(gA arc1, gPoint move_p) + { + arc1.ps -= move_p; + arc1.pe -= move_p; + arc1.pc -= move_p; + return arc1; + } + public static gA operator -(gA arc1, gPP move_p) + { + arc1.ps -= move_p.p; + arc1.pe -= move_p.p; + arc1.pc -= move_p.p; + return arc1; + } + public static gA operator -(gA arc1, gP move_p) + { + arc1.ps -= move_p.p; + arc1.pe -= move_p.p; + arc1.pc -= move_p.p; + return arc1; + } + + } +     点 mod类型 + + /// + /// 点 数据类型 (XY) + /// + public struct gPoint + { + public gPoint(gPoint p_) + { + this.x = p_.x; + this.y = p_.y; + } + public gPoint(double x_val, double y_val) + { + this.x = x_val; + this.y = y_val; + } + public double x; + public double y; + public static gPoint operator +(gPoint p1, gPoint p2) + { + p1.x += p2.x; + p1.y += p2.y; + return p1; + } + public static gPoint operator -(gPoint p1, gPoint p2) + { + p1.x -= p2.x; + p1.y -= p2.y; + return p1; + } + } \ No newline at end of file diff --git a/Docs/槽孔实际钻孔孔数相关资料/文章2.txt b/Docs/槽孔实际钻孔孔数相关资料/文章2.txt new file mode 100644 index 0000000..0224072 --- /dev/null +++ b/Docs/槽孔实际钻孔孔数相关资料/文章2.txt @@ -0,0 +1,448 @@ +PCB genesis Slot槽转钻孔(不用G85命令)实现方法 + +PCB钻Slot槽一般都采用G85命令钻槽孔,而采用G85命令工程CAM无法准确的知道Slot槽钻多少个孔,并不能决定钻槽孔的顺序,因为采用G85命令钻孔密度与钻槽顺序由钻机本身决定的.在这里介绍一种如果不用G85命令,如何将Slot槽生成多个钻孔。 + +一.我们先了解一下G85命令钻槽 + +   钻孔顺序 + +  + +  + +      孔密度 + + + +连一篇文章有关于Slot槽孔数计算方式:  https://www.cnblogs.com/pcbren/p/9379178.html + +二.求解思路 + +     1.通过孔密度,求出孔与孔中心距离 + +     2.再以Slot槽的一端做为起点,增量值(孔中心距),方位角(Slot槽的方位角),逐个求出下一个钻孔位置.直到到达Slot槽终点节止。 + +三.C#简易代码实现: + +1.Slot槽转钻孔代码(这里段代码实现将Slot槽转为钻孔,钻孔顺序是一个SLOT槽依次逐个从头钻到头尾,和G85命令钻槽顺序不一样) + + + string drilllayer = "drl"; + gLayer layer = g.getFEATURES($"{drilllayer}", g.STEP, g.JOB, "mm", true); + List pList = new List(); + foreach (var line in layer.Llist) + { + var HoleCenterDi = calc2.p_Convex(line.width * 0.0005); + pList.AddRange(calc2.l_2Plist(line, HoleCenterDi, true)); + } + foreach (var arc in layer.Alist) + { + var HoleCenterDi = calc2.p_Convex(arc.width * 0.0005); + pList.AddRange(calc2.a_2Plist(arc, HoleCenterDi,2, true)); + } + addCOM.pad(pList); +View Code +2.计算函数 + + + /// + /// 通过孔半径与凸高位求 孔中心距 + /// + /// 孔半径 + /// 凸位高度值 + /// + public double p_Convex(double Rradius, double tol_ = 0.0127) + { + return Math.Sqrt(Math.Pow(Rradius, 2) - Math.Pow(Rradius - tol_, 2)) * 2; + } + /// + /// 线Line 转点P组集 + /// + /// + /// 点的间距 + /// + public List l_2Plist(gL l, double len_ = 0.1d, bool is_avg = false) + { + List list_point = new List();//采用优先占用线两端 如果有从线的一端出发增量间距后续再做更改 + double line_len = l_Length(l); + gPP tempP; + tempP.p = l.ps; + tempP.symbols = l.symbols; + tempP.width = l.width; + list_point.Add(tempP); + int avg_count = (int)(Math.Ceiling(line_len / len_)) - 1; + if (avg_count > 1) + { + if (is_avg) + len_ = line_len / avg_count; + double angle_ = p_ang(l.ps, l.pe); + for (int i = 0; i < avg_count; i++) + { + tempP = p_val_ang(tempP, len_, angle_); + list_point.Add(tempP); + } + } + tempP.p = l.pe; + list_point.Add(tempP); + return list_point; + } + /// + /// 求方位角 + /// + /// + /// + /// + public double p_ang(gPoint ps, gPoint pe) + { + double a_ang = Math.Atan((pe.y - ps.y) / (pe.x - ps.x)) / Math.PI * 180; + //象限角 转方位角 计算所属象限 并求得方位角 + if (pe.x >= ps.x && pe.y >= ps.y) //↗ 第一象限 + { + return a_ang; + } + else if (!(pe.x >= ps.x) && pe.y >= ps.y) // ↖ 第二象限 + { + return a_ang + 180; + } + else if (!(pe.x >= ps.x) && !(pe.y >= ps.y)) //↙ 第三象限 + { + return a_ang + 180; + } + else if (pe.x >= ps.x && !(pe.y >= ps.y)) // ↘ 第四象限 + { + return a_ang + 360; + } + else + { + return a_ang; + } + }//求方位角 + /// + /// 求增量坐标 + /// + /// 起点 + /// 增量值 + /// 角度 + /// + public gPP p_val_ang(gPP ps, double val, double ang_direction) + { + gPP pe = ps; + pe.p.x = ps.p.x + val * Math.Cos(ang_direction * Math.PI / 180); + pe.p.y = ps.p.y + val * Math.Sin(ang_direction * Math.PI / 180); + return pe; + } + /// + /// 求线Line长度 + /// + /// + /// + /// + public double l_Length(gL l, bool is_calc_width = false) + { + if (is_calc_width) + return Math.Sqrt((l.ps.x - l.pe.x) * (l.ps.x - l.pe.x) + (l.ps.y - l.pe.y) * (l.ps.y - l.pe.y)) + l.width / 1000; + else + return Math.Sqrt((l.ps.x - l.pe.x) * (l.ps.x - l.pe.x) + (l.ps.y - l.pe.y) * (l.ps.y - l.pe.y)); + } + /// + /// 弧Arc 转点P组集 + /// + /// + /// 此数值表示:分段数值 + /// 代表值数值类型 【0】弧长 【1】角度 【2】弦长 + /// 是否平均分布 + /// + public List a_2Plist(gA a, double val_ = 0.1d, int type_ = 0, bool is_avg = false) + { + List list_point = new List(); + gPP tempP; + tempP.p = a.ps; + tempP.symbols = a.symbols; + tempP.width = a.width; + list_point.Add(tempP); + + double avg_count; + double angle_val = 0; + double rad_ = p2p_di(a.pc, a.pe); + double sum_alge = a_Angle(a); + if (type_ == 1) // 【1】角度 + { + angle_val = val_; + avg_count = (int)(Math.Ceiling(sum_alge / angle_val)) - 1; // 总角度/单角度 + } + else if (type_ == 2) //【2】弦长 + { + angle_val = Math.Asin(val_ / (rad_ * 2)) * 360 / pi; + avg_count = (int)(Math.Ceiling(sum_alge / angle_val)) - 1; // 总角度/单角度 + } + else // 【0】弧长 + { + angle_val = val_ * 180 / (pi * rad_); + avg_count = (int)(Math.Ceiling(sum_alge / angle_val)) - 1; // 总角度/单角度 + //avg_count = (int)(Math.Ceiling(a_Lenght(a) / val_)) - 1; // 或 总弧长/单弧长 + } + if (is_avg) + angle_val = sum_alge / avg_count; + if (avg_count > 1) + { + gPP centerP = tempP; + centerP.p = a.pc; + double angle_s = p_ang(a.pc, a.ps); + if (a.ccw) { angle_val = 0 - angle_val; } + for (int i = 1; i < avg_count; i++) + { + tempP = p_val_ang(centerP, rad_, angle_s - angle_val * i); + list_point.Add(tempP); + } + } + if (!(zero(a.ps.x - a.pe.x) && zero(a.ps.y - a.pe.y))) + { + tempP.p = a.pe; + list_point.Add(tempP); + } + return list_point; + } + /// + /// 返回两点之间欧氏距离 + /// + /// + /// + /// + public double p2p_di(gPoint p1, gPoint p2) + { + return Math.Sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)); + } + /// + /// 求弧Arc圆心角 //后续改进 用叉积 与3P求角度求解 验证哪个效率高 + /// + /// + /// + public double a_Angle(gA a) + { + double angle_s, angle_e, angle_sum; + if (a.ccw) + { + angle_s = p_ang(a.pc, a.pe); + angle_e = p_ang(a.pc, a.ps); + } + else + { + angle_s = p_ang(a.pc, a.ps); + angle_e = p_ang(a.pc, a.pe); + } + if (angle_s == 360) { angle_s = 0; } + if (angle_e >= angle_s) + angle_sum = 360 - Math.Abs(angle_s - angle_e); + else + angle_sum = Math.Abs(angle_s - angle_e); + return angle_sum; + } +View Code +3.Point,PAD,Line,Arc数据结构 + + + /// + /// 精简 PAD 数据类型 + /// + public struct gPP + { + public gPP(double x_val, double y_val, double width_) + { + this.p = new gPoint(x_val, y_val); + this.symbols = "r"; + this.width = width_; + } + public gPP(gPoint p_, double width_) + { + this.p = p_; + this.symbols = "r"; + this.width = width_; + } + public gPP(gPoint p_, string symbols_, double width_) + { + this.p = p_; + this.symbols = symbols_; + this.width = width_; + } + public gPoint p; + public string symbols; + public double width; + public static gPP operator +(gPP p1, gPP p2) + { + p1.p += p2.p; + return p1; + } + public static gPP operator +(gPP p1, gPoint p2) + { + p1.p += p2; + return p1; + } + public static gPP operator -(gPP p1, gPP p2) + { + p1.p -= p2.p; + return p1; + } + public static gPP operator -(gPP p1, gPoint p2) + { + p1.p -= p2; + return p1; + } + } + /// + /// 点 数据类型 (XY) + /// + public struct gPoint + { + public gPoint(gPoint p_) + { + this.x = p_.x; + this.y = p_.y; + } + public gPoint(double x_val, double y_val) + { + this.x = x_val; + this.y = y_val; + } + public double x; + public double y; + public static gPoint operator +(gPoint p1, gPoint p2) + { + p1.x += p2.x; + p1.y += p2.y; + return p1; + } + public static gPoint operator -(gPoint p1, gPoint p2) + { + p1.x -= p2.x; + p1.y -= p2.y; + return p1; + } + } + /// + /// Line 数据类型 + /// + public struct gL + { + public gL(double ps_x, double ps_y, double pe_x, double pe_y, double width_) + { + this.ps = new gPoint(ps_x, ps_y); + this.pe = new gPoint(pe_x, pe_y); + this.negative = false; + this.symbols = "r"; + this.attribut = string.Empty; + this.width = width_; + } + public gL(gPoint ps_, gPoint pe_, double width_) + { + this.ps = ps_; + this.pe = pe_; + this.negative = false; + this.symbols = "r"; + this.attribut = string.Empty; + this.width = width_; + } + public gL(gPoint ps_, gPoint pe_, string symbols_, double width_) + { + this.ps = ps_; + this.pe = pe_; + this.negative = false; + this.symbols = symbols_; + this.attribut = string.Empty; + this.width = width_; + } + public gPoint ps; + public gPoint pe; + public bool negative;//polarity-- positive negative + public string symbols; + public string attribut; + public double width; + public static gL operator +(gL l1, gPoint move_p) + { + l1.ps += move_p; + l1.pe += move_p; + return l1; + } + public static gL operator +(gL l1, gP move_p) + { + l1.ps += move_p.p; + l1.pe += move_p.p; + return l1; + } + public static gL operator -(gL l1, gPoint move_p) + { + l1.ps -= move_p; + l1.pe -= move_p; + return l1; + } + public static gL operator -(gL l1, gP move_p) + { + l1.ps -= move_p.p; + l1.pe -= move_p.p; + return l1; + } + } + /// + /// ARC 数据类型 + /// + public struct gA + { + public gA(double ps_x, double ps_y, double pc_x, double pc_y, double pe_x, double pe_y, double width_, bool ccw_) + { + this.ps = new gPoint(ps_x, ps_y); + this.pc = new gPoint(pc_x, pc_y); + this.pe = new gPoint(pe_x, pe_y); + this.negative = false; + this.ccw = ccw_; + this.symbols = "r"; + this.attribut = string.Empty; + this.width = width_; + } + public gA(gPoint ps_, gPoint pc_, gPoint pe_, double width_, bool ccw_ = false) + { + this.ps = ps_; + this.pc = pc_; + this.pe = pe_; + this.negative = false; + this.ccw = ccw_; + this.symbols = "r"; + this.attribut = string.Empty; + this.width = width_; + } + public gPoint ps; + public gPoint pe; + public gPoint pc; + public bool negative;//polarity-- positive negative + public bool ccw; //direction-- cw ccw + public string symbols; + public string attribut; + public double width; + public static gA operator +(gA arc1, gPoint move_p) + { + arc1.ps += move_p; + arc1.pe += move_p; + arc1.pc += move_p; + return arc1; + } + public static gA operator +(gA arc1, gP move_p) + { + arc1.ps += move_p.p; + arc1.pe += move_p.p; + arc1.pc += move_p.p; + return arc1; + } + public static gA operator -(gA arc1, gPoint move_p) + { + arc1.ps -= move_p; + arc1.pe -= move_p; + arc1.pc -= move_p; + return arc1; + } + public static gA operator -(gA arc1, gP move_p) + { + arc1.ps -= move_p.p; + arc1.pe -= move_p.p; + arc1.pc -= move_p.p; + return arc1; + } + } +View Code diff --git a/DragDropHelper.cs b/DragDropHelper.cs new file mode 100644 index 0000000..84d4e5c --- /dev/null +++ b/DragDropHelper.cs @@ -0,0 +1,359 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using WinForms = System.Windows.Forms; + +namespace DrillTools +{ + /// + /// 拖放帮助类 + /// + public static class DragDropHelper + { + private static System.Windows.Point _startPoint; + private static bool _isDragging; + private static DragToolTipWindow? _dragToolTipWindow; + private static Window? _ownerWindow; + private static System.Windows.Shapes.Rectangle? _insertionIndicator; + private static int _insertionIndex = -1; + private static DateTime? _lastUpdateTime; + + /// + /// 启用 ListView 的拖放功能 + /// + /// 数据项类型 + /// ListView 控件 + /// 插入位置指示器 + public static void EnableDragDrop(System.Windows.Controls.ListView listView, System.Windows.Shapes.Rectangle insertionIndicator) where T : class + { + _insertionIndicator = insertionIndicator; + _ownerWindow = Window.GetWindow(listView); + + listView.PreviewMouseLeftButtonDown += (sender, e) => + { + _startPoint = e.GetPosition(null); + _isDragging = false; + }; + + listView.PreviewMouseMove += (sender, e) => + { + if (e.LeftButton == MouseButtonState.Pressed && !_isDragging) + { + System.Windows.Point position = e.GetPosition(null); + if (Math.Abs(position.X - _startPoint.X) > SystemParameters.MinimumHorizontalDragDistance || + Math.Abs(position.Y - _startPoint.Y) > SystemParameters.MinimumVerticalDragDistance) + { + _isDragging = true; + var listViewItem = FindAncestor((DependencyObject)e.OriginalSource); + if (listViewItem != null) + { + var item = listView.ItemContainerGenerator.ItemFromContainer(listViewItem) as T; + if (item != null) + { + // 获取屏幕坐标 + var screenPosition = listView.PointToScreen(e.GetPosition(listView)); + + // 创建并显示拖动提示 + CreateDragToolTip(item, screenPosition); + + // 使用 QueryContinueDrag 事件来更新位置 + System.Windows.QueryContinueDragEventHandler queryContinueHandler = null; + queryContinueHandler = (sender, e) => + { + if (_dragToolTipWindow != null) + { + // 获取当前鼠标位置 + var currentPos = WinForms.Control.MousePosition; + _dragToolTipWindow.UpdatePosition(new System.Windows.Point(currentPos.X, currentPos.Y)); + } + + // 如果拖放结束,移除事件处理 + if (e.Action == System.Windows.DragAction.Drop || e.Action == System.Windows.DragAction.Cancel) + { + DragDrop.RemoveQueryContinueDragHandler(listView, queryContinueHandler); + } + }; + + DragDrop.AddQueryContinueDragHandler(listView, queryContinueHandler); + + System.Windows.DataObject data = new System.Windows.DataObject(typeof(T), item); + DragDrop.DoDragDrop(listViewItem, data, System.Windows.DragDropEffects.Move); + + // 清理拖动提示 + CleanupDragToolTip(); + } + } + } + } + }; + + listView.DragOver += (sender, e) => + { + if (e.Data.GetDataPresent(typeof(T))) + { + e.Effects = System.Windows.DragDropEffects.Move; + + // 更新插入位置指示器 + UpdateInsertionIndicator(listView, e.GetPosition(listView)); + } + else + { + e.Effects = System.Windows.DragDropEffects.None; + } + e.Handled = true; + }; + + listView.DragLeave += (sender, e) => + { + HideInsertionIndicator(); + }; + + listView.Drop += (sender, e) => + { + try + { + // 隐藏插入指示器 + HideInsertionIndicator(); + + Debug.WriteLine($"[DragDrop] Drop事件触发 - OriginalSource类型: {e.OriginalSource?.GetType().Name}"); + Debug.WriteLine($"[DragDrop] Drop位置: ({e.GetPosition(listView).X}, {e.GetPosition(listView).Y})"); + + if (e.Data.GetDataPresent(typeof(T))) + { + var droppedItem = e.Data.GetData(typeof(T)) as T; + if (droppedItem != null) + { + Debug.WriteLine($"[DragDrop] 拖放项: {droppedItem}"); + + // 检查是否是机台码刀具,如果是则不允许移动 + if (droppedItem is ToolItem toolItem && toolItem.ToolType == ToolType.MachineCode) + { + Debug.WriteLine("[DragDrop] 机台码刀具不允许移动"); + System.Windows.MessageBox.Show("机台码刀具不允许移动位置", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + _isDragging = false; + return; + } + + var targetItem = FindAncestor((DependencyObject)e.OriginalSource); + string ss = targetItem == null ? "NULL" : "找到"; + Debug.WriteLine($"[DragDrop] 目标ListViewItem: {ss}"); + + if (targetItem == null) + { + Debug.WriteLine("[DragDrop] 警告: 目标项为空,可能是拖放到列表外部"); + _isDragging = false; + return; + } + + var targetIndex = listView.ItemContainerGenerator.IndexFromContainer(targetItem); + Debug.WriteLine($"[DragDrop] 目标索引: {targetIndex}"); + + var sourceIndex = listView.Items.IndexOf(droppedItem); + Debug.WriteLine($"[DragDrop] 源索引: {sourceIndex}"); + + // 检查目标位置是否是机台码刀具 + var viewModel = listView.DataContext as MainWindowViewModel; + if (viewModel != null && viewModel.Tools.Count > targetIndex) + { + var targetTool = viewModel.Tools[targetIndex]; + if (targetTool.ToolType == ToolType.MachineCode) + { + Debug.WriteLine("[DragDrop] 不能移动到机台码刀具位置"); + System.Windows.MessageBox.Show("不能移动到机台码刀具位置", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + _isDragging = false; + return; + } + } + + if (sourceIndex != -1 && targetIndex != -1 && sourceIndex != targetIndex) + { + viewModel?.ReorderTools(sourceIndex, targetIndex); + Debug.WriteLine($"[DragDrop] 重新排序完成: {sourceIndex} -> {targetIndex}"); + } + else + { + Debug.WriteLine($"[DragDrop] 跳过重新排序 - 源索引: {sourceIndex}, 目标索引: {targetIndex}"); + } + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"[DragDrop] 异常: {ex.GetType().Name} - {ex.Message}"); + Debug.WriteLine($"[DragDrop] 堆栈跟踪: {ex.StackTrace}"); + System.Windows.MessageBox.Show($"拖放操作发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + _isDragging = false; + } + }; + } + + /// + /// 查找指定类型的父元素 + /// + /// 父元素类型 + /// 当前元素 + /// 找到的父元素,如果未找到则返回 null + private static T? FindAncestor(DependencyObject current) where T : DependencyObject + { + Debug.WriteLine($"[FindAncestor] 开始查找类型 {typeof(T).Name},起始元素类型: {current?.GetType().Name ?? "NULL"}"); + int depth = 0; + + while (current != null) + { + Debug.WriteLine($"[FindAncestor] 深度 {depth}: 当前类型 {current.GetType().Name}"); + + if (current is T ancestor) + { + Debug.WriteLine($"[FindAncestor] 找到匹配的祖先类型 {typeof(T).Name}"); + return ancestor; + } + + current = VisualTreeHelper.GetParent(current); + depth++; + + if (depth > 10) // 防止无限循环 + { + Debug.WriteLine("[FindAncestor] 警告: 搜索深度超过10层,停止搜索"); + break; + } + } + + Debug.WriteLine($"[FindAncestor] 未找到类型 {typeof(T).Name} 的祖先"); + return null; + } + + /// + /// 处理主窗口鼠标移动事件,更新拖动提示位置 + /// + private static void OnOwnerWindowMouseMove(object sender, System.Windows.Input.MouseEventArgs e) + { + if (_dragToolTipWindow != null && _isDragging) + { + // 获取屏幕坐标 + var windowPosition = e.GetPosition(_ownerWindow); + var screenPosition = _ownerWindow.PointToScreen(windowPosition); + _dragToolTipWindow.UpdatePosition(screenPosition); + } + } + + /// + /// 创建拖动提示窗口 + /// + private static void CreateDragToolTip(T item, System.Windows.Point position) where T : class + { + if (item is ToolItem toolItem && _ownerWindow != null) + { + _dragToolTipWindow = new DragToolTipWindow(); + _dragToolTipWindow.SetToolInfo(toolItem); + _dragToolTipWindow.UpdatePosition(position); + // 不设置 Owner,这样窗口可以独立定位 + _dragToolTipWindow.Show(); + } + } + + /// + /// 清理拖动提示窗口 + /// + private static void CleanupDragToolTip() + { + if (_dragToolTipWindow != null) + { + _dragToolTipWindow.Close(); + _dragToolTipWindow = null; + } + + if (_ownerWindow != null) + { + _ownerWindow.MouseMove -= OnOwnerWindowMouseMove; + } + } + + /// + /// 更新插入位置指示器 + /// + private static void UpdateInsertionIndicator(System.Windows.Controls.ListView listView, System.Windows.Point position) + { + if (_insertionIndicator == null) return; + + // 使用节流机制,避免过于频繁的更新 + if (_lastUpdateTime != null && DateTime.Now - _lastUpdateTime < TimeSpan.FromMilliseconds(16)) + return; + + _lastUpdateTime = DateTime.Now; + + try + { + // 使用 VisualTreeHelper 找到鼠标下的元素 + var element = listView.InputHitTest(position) as DependencyObject; + var listViewItem = FindAncestor(element); + + if (listViewItem != null) + { + var index = listView.ItemContainerGenerator.IndexFromContainer(listViewItem); + var itemBounds = listViewItem.TransformToAncestor(listView) + .TransformBounds(new Rect(0, 0, listViewItem.ActualWidth, listViewItem.ActualHeight)); + + // 判断是在上半部分还是下半部分 + bool insertAfter = position.Y > itemBounds.Top + itemBounds.Height / 2; + int targetIndex = insertAfter ? index + 1 : index; + + if (targetIndex != _insertionIndex) + { + _insertionIndex = targetIndex; + + // 更新指示器位置 + _insertionIndicator.Visibility = Visibility.Visible; + + // 计算指示器位置 + double indicatorTop = insertAfter ? itemBounds.Bottom : itemBounds.Top; + + // 使用 VerticalAlignment 和 Margin 结合来定位指示器 + _insertionIndicator.VerticalAlignment = VerticalAlignment.Top; + _insertionIndicator.Margin = new Thickness(0, indicatorTop, 0, 0); + } + } + else + { + // 如果没有找到目标项,检查是否在列表末尾 + if (position.Y > listView.ActualHeight - 20) + { + _insertionIndex = listView.Items.Count; + // 在列表末尾显示指示器 + _insertionIndicator.Visibility = Visibility.Visible; + _insertionIndicator.VerticalAlignment = VerticalAlignment.Top; + _insertionIndicator.Margin = new Thickness(0, listView.ActualHeight - 2, 0, 0); + } + else + { + HideInsertionIndicator(); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"[DragDrop] 更新插入指示器时发生错误: {ex.Message}"); + HideInsertionIndicator(); + } + } + + /// + /// 隐藏插入位置指示器 + /// + private static void HideInsertionIndicator() + { + if (_insertionIndicator != null) + { + _insertionIndicator.Visibility = Visibility.Collapsed; + } + _insertionIndex = -1; + } + } +} \ No newline at end of file diff --git a/DragToolTipWindow.cs b/DragToolTipWindow.cs new file mode 100644 index 0000000..4956cac --- /dev/null +++ b/DragToolTipWindow.cs @@ -0,0 +1,114 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace DrillTools +{ + /// + /// 拖动提示窗口,显示刀具信息 + /// + public class DragToolTipWindow : Window + { + private readonly TextBlock _textBlock; + + public DragToolTipWindow() + { + // 设置窗口样式 + WindowStyle = WindowStyle.None; + AllowsTransparency = true; + Background = System.Windows.Media.Brushes.Transparent; + ShowInTaskbar = false; + Topmost = true; + IsHitTestVisible = false; // 不拦截鼠标事件 + SizeToContent = SizeToContent.WidthAndHeight; // 根据内容自动调整大小 + MaxWidth = 200; // 限制最大宽度 + MaxHeight = 50; // 限制最大高度 + + // 创建内容容器 + var border = new Border + { + Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(224, 255, 255, 255)), + BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 120, 212)), + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(3), + Padding = new Thickness(5, 2, 5, 2), + Effect = new System.Windows.Media.Effects.DropShadowEffect + { + Color = System.Windows.Media.Colors.Black, + Direction = 315, + ShadowDepth = 2, + BlurRadius = 5, + Opacity = 0.3 + } + }; + + // 创建文本显示 + _textBlock = new TextBlock + { + FontWeight = FontWeights.Bold, + FontSize = 10, // 减小字体大小 + TextTrimming = TextTrimming.CharacterEllipsis // 文本过长时显示省略号 + }; + + border.Child = _textBlock; + Content = border; + } + + /// + /// 设置刀具信息 + /// + /// 刀具项 + public void SetToolInfo(ToolItem tool) + { + _textBlock.Text = $"T{tool.ToolNumber:D2} - {tool.Diameter:F3}mm"; + } + + /// + /// 更新窗口位置 + /// + /// 鼠标位置 + public void UpdatePosition(System.Windows.Point position) + { + // 将提示窗口显示在鼠标右下方,稍微偏移以避免遮挡 + // 确保窗口不会超出屏幕边界 + double left = position.X + 15; + double top = position.Y - 35; + + // 获取屏幕尺寸 + var screenWidth = SystemParameters.PrimaryScreenWidth; + var screenHeight = SystemParameters.PrimaryScreenHeight; + + // 先设置位置,然后获取实际尺寸 + Left = left; + Top = top; + + // 确保窗口不会超出右边界 + if (left + ActualWidth > screenWidth) + { + left = position.X - ActualWidth - 15; + } + + // 确保窗口不会超出左边界 + if (left < 0) + { + left = 5; + } + + // 确保窗口不会超出下边界 + if (top + ActualHeight > screenHeight) + { + top = position.Y - ActualHeight - 5; + } + + // 确保窗口不会超出上边界 + if (top < 0) + { + top = position.Y + 15; + } + + // 重新设置最终位置 + Left = left; + Top = top; + } + } +} \ No newline at end of file diff --git a/DrillTapeProcessor.cs b/DrillTapeProcessor.cs new file mode 100644 index 0000000..c2217b5 --- /dev/null +++ b/DrillTapeProcessor.cs @@ -0,0 +1,680 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace DrillTools.Integration +{ + /// + /// 钻带处理工具集成示例 + /// 展示如何将槽孔计算类集成到钻带处理工具中 + /// + public class DrillTapeProcessor + { + /// + /// 处理钻带数据并计算槽孔孔数 + /// + /// 钻带文件内容 + /// 处理结果 + public static DrillTapeResult ProcessDrillTape(string drillTapeContent) + { + var result = new DrillTapeResult(); + + try + { + // 解析刀具信息 + var tools = ParseTools(drillTapeContent); + + // 解析孔位信息 + var holes = ParseHoles(drillTapeContent, tools); + + // 按刀具分组孔位数据 + var holesByTool = holes.GroupBy(h => h.ToolNumber).ToDictionary(g => g.Key, g => g.ToList()); + + // 处理刀具信息 + foreach (var tool in tools) + { + // 检查是否是机台码 (0.499孔径) + bool isMachineCode = Math.Abs(tool.Diameter - 0.499) < 0.001; + + // 根据刀具类型设置 + ToolType toolType; + if (isMachineCode) + { + toolType = ToolType.MachineCode; + } + else + { + // 简化处理,所有非机台码都设为Regular + // 实际应用中可以根据需要判断是否为槽孔 + toolType = ToolType.Regular; + } + + // 计算刀具尾号类型和大类 + var toolSuffixType = ToolItem.GetToolSuffixType(tool.Diameter); + var ToolCategory = ToolItem.GetToolCategory(toolSuffixType); + + // 获取当前刀具的孔位数据 + var toolHoles = holesByTool.ContainsKey(tool.Number) ? holesByTool[tool.Number] : new List(); + var locations = new List(); + + // 统计孔数并收集孔位坐标 + int regularHoles = 0; + int slotHoles = 0; + int slotCount = 0; + + // 特殊处理机台码:分离固定孔数和实际坐标 + if (isMachineCode) + { + // 机台码:先添加固定孔数的虚拟坐标(X0Y0) + int machineCodeHoleCount = 0; + foreach (var hole in toolHoles) + { + if (hole.Type == HoleType.Regular && hole.Position.X == 0 && hole.Position.Y == 0) + { + regularHoles++; + machineCodeHoleCount++; + // 不添加 X0Y0 到 locations,这些只是用于计数的虚拟孔 + } + else if (hole.Type == HoleType.Regular && (hole.Position.X != 0 || hole.Position.Y != 0)) + { + // 这是实际的坐标行,使用原始字符串保持格式一致 + string location = !string.IsNullOrEmpty(hole.Position.OriginalString) + ? hole.Position.OriginalString + : $"X{hole.Position.X:F0}Y{hole.Position.Y:F0}"; + locations.Add(location); + } + } + } + else + { + // 普通刀具和槽刀:正常处理 + foreach (var hole in toolHoles) + { + if (hole.Type == HoleType.Regular) + { + regularHoles++; + // Point2D 是 struct,不需要检查 null + // 使用原始字符串保持格式一致 + string location = !string.IsNullOrEmpty(hole.Position.OriginalString) + ? hole.Position.OriginalString + : $"X{hole.Position.X:F0}Y{hole.Position.Y:F0}"; + locations.Add(location); + } + else if (hole.Type == HoleType.Slot) + { + slotHoles++; + slotCount++; + // LineSlot 是 struct,不需要检查 null,使用正确的属性名 + // 使用原始字符串保持格式一致 + string startLocation = !string.IsNullOrEmpty(hole.Slot.StartPoint.OriginalString) + ? hole.Slot.StartPoint.OriginalString + : $"X{hole.Slot.StartPoint.X:F0}Y{hole.Slot.StartPoint.Y:F0}"; + string endLocation = !string.IsNullOrEmpty(hole.Slot.EndPoint.OriginalString) + ? hole.Slot.EndPoint.OriginalString + : $"X{hole.Slot.EndPoint.X:F0}Y{hole.Slot.EndPoint.Y:F0}"; + + // 直接组合成G85格式,保持原始坐标格式 + string location = $"{startLocation}G85{endLocation}"; + locations.Add(location); + } + } + } + + // 为机台码刀具提取机台码命令和类型 + string machineCodeCommand = string.Empty; + string machineCodeType = string.Empty; + + if (isMachineCode) + { + // 从钻带内容中提取机台码信息 + var toolPattern = $@"%.+?T{tool.Number:D2}(.*?)(?=T\d{{2}}|M30)"; + var match = Regex.Match(drillTapeContent, toolPattern, RegexOptions.Singleline); + + if (match.Success) + { + string holeSection = match.Groups[1].Value; + + // 查找机台码命令 + var machineCodePattern = @"(M97|M98),(A\*|B\*),\$S \$N"; + var machineCodeMatch = Regex.Match(holeSection, machineCodePattern); + + if (machineCodeMatch.Success) + { + machineCodeCommand = machineCodeMatch.Groups[1].Value; // M97或M98 + machineCodeType = machineCodeMatch.Groups[2].Value; // A*或B* + + // 添加日志验证机台码解析 + System.Diagnostics.Debug.WriteLine($"[机台码解析] T{tool.Number:D2}: 命令={machineCodeCommand}, 类型={machineCodeType}"); + } + else + { + System.Diagnostics.Debug.WriteLine($"[机台码解析] T{tool.Number:D2}: 未找到机台码命令"); + } + } + else + { + System.Diagnostics.Debug.WriteLine($"[机台码解析] T{tool.Number:D2}: 未找到机台码部分"); + } + } + + result.ToolResults.Add(new ToolResult + { + ToolNumber = tool.Number, + Diameter = tool.Diameter, + TotalHoles = regularHoles + slotHoles, + RegularHoles = regularHoles, + SlotHoles = slotHoles, + ToolType = toolType, + SlotCount = slotCount, + Locations = locations, + ToolSuffixType = toolSuffixType, + ToolCategory = ToolCategory, + MachineCodeCommand = machineCodeCommand, + MachineCodeType = machineCodeType + }); + } + + result.Success = true; + result.Message = "钻带处理成功"; + } + catch (Exception ex) + { + result.Success = false; + result.Message = $"钻带处理失败: {ex.Message}"; + } + + return result; + } + + /// + /// 解析刀具信息 + /// + private static List ParseTools(string drillTapeContent) + { + var tools = new List(); + + // 解析M48到%之间的刀具信息 + var toolPattern = @"T(\d+)C(\d+\.?\d*)"; + var matches = Regex.Matches(drillTapeContent, toolPattern); + + foreach (Match match in matches) + { + int toolNumber = int.Parse(match.Groups[1].Value); + double diameter = double.Parse(match.Groups[2].Value); + + tools.Add(new ToolInfo + { + Number = toolNumber, + Diameter = diameter + }); + } + + return tools; + } + + /// + /// 解析钻孔信息 + /// + private static List ParseHoles(string drillTapeContent, List tools) + { + var holes = new List(); + + // 按刀具分组解析 + foreach (var tool in tools) + { + // 检查是否是机台码 (0.499孔径) + if (Math.Abs(tool.Diameter - 0.499) < 0.001) + { + // 解析机台码部分(在%符号之后查找) + var toolPattern = $@"%.+?T{tool.Number:D2}(.*?)(?=T\d{{2}}|M30)"; + var match = Regex.Match(drillTapeContent, toolPattern, RegexOptions.Singleline); + + if (match.Success) + { + string holeSection = match.Groups[1].Value; + + // 查找机台码命令 + var machineCodePattern = @"(M97|M98),(A\*|B\*),\$S \$N"; + var machineCodeMatch = Regex.Match(holeSection, machineCodePattern); + + if (machineCodeMatch.Success) + { + string command = machineCodeMatch.Groups[1].Value; // M97或M98 + string codeType = machineCodeMatch.Groups[2].Value; // A*或B* + + // 根据机台码类型确定孔数 + int holeCount = (codeType == "A*") ? 57 : 58; + + // 查找机台码坐标行 + var coordinatePattern = @"X([+-]?\d+\.?\d*)Y([+-]?\d+\.?\d*)"; + var coordinateMatches = Regex.Matches(holeSection, coordinatePattern); + + // 添加机台码孔信息 + for (int i = 0; i < holeCount; i++) + { + holes.Add(new HoleInfo + { + ToolNumber = tool.Number, + Type = HoleType.Regular, + Position = new Point2D(0, 0) // 机台码不需要实际坐标 + }); + } + + // 这样可以确保坐标数据被正确保存到 ToolResult.Locations 中 + foreach (Match coordMatch in coordinateMatches) + { + // 解析坐标 + double x = double.Parse(coordMatch.Groups[1].Value); + double y = double.Parse(coordMatch.Groups[2].Value); + + // 保存原始坐标字符串 + string originalCoordString = coordMatch.Value; + + // 添加一个特殊的孔位信息来保存实际坐标 + holes.Add(new HoleInfo + { + ToolNumber = tool.Number, + Type = HoleType.Regular, + Position = new Point2D(x, y, originalCoordString) + }); + } + } + } + } + else + { + // 查找当前刀具的钻孔部分(只在%符号之后查找) + var toolPattern = $@"%.+?T{tool.Number:D2}(.*?)(?=T\d{{2}}|M30)"; + var match = Regex.Match(drillTapeContent, toolPattern, RegexOptions.Singleline); + + if (match.Success) + { + string holeSection = match.Groups[1].Value; + // 先解析槽孔,并记录槽孔的起始坐标 + var slotPattern = @"X([+-]?\d+\.?\d*)Y([+-]?\d+\.?\d*)G85X([+-]?\d+\.?\d*)Y([+-]?\d+\.?\d*)"; + var slotMatches = Regex.Matches(holeSection, slotPattern); + var slotStartPositions = new System.Collections.Generic.HashSet(); + + foreach (Match slotMatch in slotMatches) + { + var slot = SlotHoleCalculator.ParseLineSlotFromG85(slotMatch.Value, tool.Diameter); + holes.Add(new HoleInfo + { + ToolNumber = tool.Number, + Type = HoleType.Slot, + Slot = slot + }); + + // 记录槽孔的起始坐标,以便后续排除 + string startPos = $"X{slotMatch.Groups[1].Value}Y{slotMatch.Groups[2].Value}"; + slotStartPositions.Add(startPos); + } + + // 解析普通圆孔(排除槽孔的起始坐标) + var regularHolePattern = @"X([+-]?\d+\.?\d*)Y([+-]?\d+\.?\d*)"; + var regularMatches = Regex.Matches(holeSection, regularHolePattern); + + foreach (Match regularMatch in regularMatches) + { + // 排除槽孔的起始坐标 + if (!slotStartPositions.Contains(regularMatch.Value)) + { + holes.Add(new HoleInfo + { + ToolNumber = tool.Number, + Type = HoleType.Regular, + Position = Point2D.ParseFromDrillString(regularMatch.Value) + }); + } + } + + // 解析槽孔(已在上面处理) + } + } + } + + return holes; + } + + /// + /// 生成处理报告 + /// + /// 处理结果 + /// 格式化的报告文本 + public static string GenerateReport(DrillTapeResult result) + { + var report = new System.Text.StringBuilder(); + + report.AppendLine("钻带处理报告"); + report.AppendLine("============"); + report.AppendLine($"处理状态: {(result.Success ? "成功" : "失败")}"); + report.AppendLine($"处理信息: {result.Message}"); + report.AppendLine(); + + if (result.Success) + { + report.AppendLine("刀具统计:"); + report.AppendLine("刀具\t孔径(mm)\t类型\t\t尾号类型\t大类\t\t普通孔数\t槽孔数\t槽孔个数\t总孔数"); + report.AppendLine("================================================================================"); + + foreach (var tool in result.ToolResults) + { + string toolTypeDisplay = tool.ToolType switch + { + ToolType.Regular => "圆孔", + ToolType.Slot => "槽孔", + ToolType.MachineCode => "机台码", + _ => "未知" + }; + + string suffixTypeDisplay = tool.ToolSuffixType switch + { + ToolSuffixType.Drill => "钻针", + ToolSuffixType.Slot => "槽刀", + ToolSuffixType.EASlot => "EA型槽刀", + ToolSuffixType.DustSlot => "粉尘刀", + ToolSuffixType.DeburrSlot => "去毛刺刀", + ToolSuffixType.NonStandard => "非标刀", + ToolSuffixType.EASlot2 => "EA型槽刀", + ToolSuffixType.Special => "特殊刀具", + _ => "未知" + }; + + string categoryDisplay = tool.ToolCategory switch + { + ToolCategory.Drill => "钻针", + ToolCategory.Slot => "槽刀", + ToolCategory.EA => "EA刀", + ToolCategory.NonStandard => "非标刀", + ToolCategory.Special => "特殊刀", + _ => "未知" + }; + + report.AppendLine($"T{tool.ToolNumber:D2}\t{tool.Diameter:F3}\t\t{toolTypeDisplay}\t{suffixTypeDisplay}\t{categoryDisplay}\t{tool.RegularHoles}\t\t{tool.SlotHoles}\t{tool.SlotCount}\t\t{tool.TotalHoles}"); + } + + report.AppendLine(); + report.AppendLine($"总计: {result.ToolResults.Count} 把刀具"); + report.AppendLine($"总孔数: {result.TotalHoles}"); + } + + return report.ToString(); + } + } + + /// + /// 刀具信息 + /// + public class ToolInfo + { + public int Number { get; set; } + public double Diameter { get; set; } + } + + /// + /// 钻孔信息 + /// + public class HoleInfo + { + public int ToolNumber { get; set; } + public HoleType Type { get; set; } + public Point2D Position { get; set; } + public LineSlot Slot { get; set; } + } + + /// + /// 钻孔类型 + /// + public enum HoleType + { + Regular, // 普通圆孔 + Slot // 槽孔 + } + + /// + /// 钻带处理结果 + /// + public class DrillTapeResult + { + public bool Success { get; set; } + public string Message { get; set; } = string.Empty; + public List ToolResults { get; set; } = new List(); + + /// + /// 获取总孔数 + /// + public int TotalHoles => ToolResults.Sum(t => t.TotalHoles); + } + + /// + /// 刀具处理结果 + /// + public class ToolResult + { + public int ToolNumber { get; set; } + public double Diameter { get; set; } + public int TotalHoles { get; set; } + public int RegularHoles { get; set; } + public int SlotHoles { get; set; } + public int SlotCount { get; set; } // 槽孔个数(不是钻孔数量) + public List Locations { get; set; } = new(); // 位置数据 + public ToolType ToolType { get; set; } + public ToolSuffixType ToolSuffixType { get; set; } + public ToolCategory ToolCategory { get; set; } + + /// + /// 机台码命令 (M97或M98) + /// + public string MachineCodeCommand { get; set; } = string.Empty; + + /// + /// 机台码类型 (A*或B*) + /// + public string MachineCodeType { get; set; } = string.Empty; + } + + /// + /// 钻带处理扩展方法 + /// + public static class DrillTapeProcessorExtensions + { + /// + /// 生成重新排序后的钻带内容(重排并重新编号) + /// + /// 原始钻带内容 + /// 重新排序的刀具列表 + /// 重新排序后的钻带内容 + public static string GenerateReorderedDrillTape(string originalDrillTape, List reorderedTools) + { + // 1. 创建原始刀具编号到新编号的映射 + var toolNumberMapping = new Dictionary(); + var originalToNewMapping = new Dictionary(); // 保存原始映射关系 + + // 2. 按当前列表顺序重新编号(T01, T02, T03...) + int newToolNumber = 1; + foreach (var tool in reorderedTools.ToList()) + { + // 机台码刀具不允许重新编号 + if (tool.ToolType != ToolType.MachineCode) + { + originalToNewMapping[tool.ToolNumber] = newToolNumber; + toolNumberMapping[tool.ToolNumber] = newToolNumber; + tool.ToolNumber = newToolNumber++; + } + else + { + // 机台码刀具保持原始编号,但也要加入映射 + toolNumberMapping[tool.ToolNumber] = tool.ToolNumber; + } + } + + // 3. 使用新的刀具编号更新钻带内容 + return UpdateDrillTapeWithNewToolNumbers(originalDrillTape, toolNumberMapping, reorderedTools); + } + + /// + /// 使用新的刀具编号更新钻带内容 + /// + /// 原始钻带内容 + /// 刀具编号映射 + /// 重新排序的刀具列表 + /// 更新后的钻带内容 + private static string UpdateDrillTapeWithNewToolNumbers(string originalDrillTape, Dictionary toolNumberMapping, List reorderedTools) + { + var lines = originalDrillTape.Split(new[] { '\r', '\n' }, StringSplitOptions.None); + var result = new List(); + + // 创建新编号到刀具对象的映射,以便获取对应的坐标数据 + var newNumberToToolMap = new Dictionary(); + foreach (var tool in reorderedTools) + { + if (toolNumberMapping.ContainsValue(tool.ToolNumber)) + { + newNumberToToolMap[tool.ToolNumber] = tool; + } + } + + // 首先解析原始钻带中的刀具定义行,保存参数信息 + var originalToolDefinitions = new Dictionary(); + var headerLines = new List(); + + foreach (var line in lines) + { + string trimmedLine = line.Trim(); + + if (trimmedLine == "%") + { + //headerLines.Add(line); + break; + } + + // 处理刀具定义行(如 T01C1.049H05000Z+0.000S060.00F105.0U0700.0) + if (Regex.IsMatch(trimmedLine, @"^T(\d+)C")) + { + var match = Regex.Match(trimmedLine, @"^T(\d+)C(.*)$"); + if (match.Success) + { + int originalToolNumber = int.Parse(match.Groups[1].Value); + originalToolDefinitions[originalToolNumber] = match.Groups[2].Value; + } + } + else + { + if (line != "") + headerLines.Add(line); + } + } + + // 添加头部信息 + result.AddRange(headerLines); + + // 按照新的刀具编号顺序输出刀具定义部分 + var sortedTools = reorderedTools.OrderBy(t => t.ToolNumber).ToList(); + foreach (var tool in sortedTools) + { + // 查找原始刀具编号 + int originalToolNumber = -1; + foreach (var kvp in toolNumberMapping) + { + if (kvp.Value == tool.ToolNumber) + { + originalToolNumber = kvp.Key; + break; + } + } + + if (originalToolNumber != -1 && originalToolDefinitions.ContainsKey(originalToolNumber)) + { + string toolDef = $"T{tool.ToolNumber:D2}C{originalToolDefinitions[originalToolNumber]}"; + result.Add(toolDef); + } + } + + // 添加 % 符号 + result.Add("%"); + + // 处理刀具切换行和坐标数据部分 + // 按新刀具编号顺序输出刀具切换行和对应的坐标数据 + var sortedToolsForData = reorderedTools.OrderBy(t => t.ToolNumber).ToList(); + + foreach (var tool in sortedToolsForData) + { + // 添加刀具切换行 + result.Add($"T{tool.ToolNumber:D2}"); + + // 添加该刀具对应的所有坐标数据 + if (tool.ToolType != ToolType.MachineCode && tool.HoleLocations != null && tool.HoleLocations.Count > 0) + { + foreach (var location in tool.HoleLocations) + { + result.Add(location); + } + } + + // 如果是机台码刀具,添加机台码命令和坐标 + if (tool.ToolType == ToolType.MachineCode) + { + if (!string.IsNullOrEmpty(tool.MachineCodeCommand) && !string.IsNullOrEmpty(tool.MachineCodeType)) + { + result.Add($"{tool.MachineCodeCommand},{tool.MachineCodeType},$S $N"); + + // 添加机台码的坐标数据 + if (tool.HoleLocations != null && tool.HoleLocations.Count > 0) + { + foreach (var location in tool.HoleLocations) + { + result.Add(location); + } + } + } + } + } + + // 添加结束标记 + result.Add("M30"); + + return string.Join("\r\n", result); + } + + /// + /// 根据刀具编号尾号判断刀具尾号类型 + /// + /// 刀具编号 + /// 刀具尾号类型 + private static ToolSuffixType GetToolSuffixType(int toolNumber) + { + int suffix = toolNumber % 10; + return suffix switch + { + 0 => ToolSuffixType.Drill, + 1 => ToolSuffixType.Slot, + 2 => ToolSuffixType.EASlot, + 3 => ToolSuffixType.DustSlot, + 4 => ToolSuffixType.DeburrSlot, + 5 => ToolSuffixType.NonStandard, + 6 => ToolSuffixType.EASlot2, + 7 => ToolSuffixType.Special, + _ => ToolSuffixType.NonStandard + }; + } + + /// + /// 根据刀具尾号类型获取刀具大类 + /// + /// 刀具尾号类型 + /// 刀具大类 + private static ToolCategory GetToolCategory(ToolSuffixType suffixType) + { + return suffixType switch + { + ToolSuffixType.Drill => ToolCategory.Drill, + ToolSuffixType.Slot or ToolSuffixType.DustSlot or ToolSuffixType.DeburrSlot => ToolCategory.Slot, + ToolSuffixType.EASlot or ToolSuffixType.EASlot2 => ToolCategory.EA, + ToolSuffixType.NonStandard => ToolCategory.NonStandard, + ToolSuffixType.Special => ToolCategory.Special, + _ => ToolCategory.NonStandard + }; + } + } +} \ No newline at end of file diff --git a/DrillTools.csproj b/DrillTools.csproj new file mode 100644 index 0000000..da170b8 --- /dev/null +++ b/DrillTools.csproj @@ -0,0 +1,13 @@ + + + + WinExe + net6.0-windows + enable + enable + true + true + x86 + + + diff --git a/DrillTools.sln b/DrillTools.sln new file mode 100644 index 0000000..27a7907 --- /dev/null +++ b/DrillTools.sln @@ -0,0 +1,29 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 d18.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrillTools", "DrillTools.csproj", "{FC319F00-406B-4E5B-BB24-D9F76B02DF51}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FC319F00-406B-4E5B-BB24-D9F76B02DF51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC319F00-406B-4E5B-BB24-D9F76B02DF51}.Debug|x86.ActiveCfg = Debug|x86 + {FC319F00-406B-4E5B-BB24-D9F76B02DF51}.Debug|x86.Build.0 = Debug|x86 + {FC319F00-406B-4E5B-BB24-D9F76B02DF51}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC319F00-406B-4E5B-BB24-D9F76B02DF51}.Release|x86.ActiveCfg = Release|x86 + {FC319F00-406B-4E5B-BB24-D9F76B02DF51}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1DA9466F-43EA-4554-BAFE-935FBF8D2909} + EndGlobalSection +EndGlobal diff --git a/GenerateReorderedDrillTape_Implementation.md b/GenerateReorderedDrillTape_Implementation.md new file mode 100644 index 0000000..188d60d --- /dev/null +++ b/GenerateReorderedDrillTape_Implementation.md @@ -0,0 +1,180 @@ +# GenerateReorderedDrillTape 方法实现方案 + +## 目标 +将 `GenerateReorderedDrillTape` 静态扩展方法的功能升级,使其与 `ReorderAndRenumberTools` 方法完全一致。 + +## 当前问题分析 +现有的 `GenerateReorderedDrillTape` 方法只实现简单的重排功能,缺少以下关键特性: +1. 刀具重新编号(T01, T02, T03...) +2. 机台码智能处理(保持原始编号) +3. 完整的钻带内容重构逻辑 +4. 数据映射和同步机制 + +## 新实现方案 + +### 1. 方法签名保持不变 +```csharp +public static string GenerateReorderedDrillTape(string originalDrillTape, List reorderedTools) +``` + +### 2. 核心实现逻辑 + +#### 步骤1: 创建刀具编号映射 +```csharp +// 1. 创建原始刀具编号到新编号的映射 +var toolNumberMapping = new Dictionary(); +var originalToNewMapping = new Dictionary(); + +// 2. 按当前列表顺序重新编号(T01, T02, T03...) +int newToolNumber = 1; +foreach (var tool in reorderedTools) +{ + // 机台码刀具不允许重新编号 + if (tool.ToolType != ToolType.MachineCode) + { + originalToNewMapping[tool.ToolNumber] = newToolNumber; + toolNumberMapping[tool.ToolNumber] = newToolNumber; + tool.ToolNumber = newToolNumber++; + } + else + { + // 机台码刀具保持原始编号,但也要加入映射 + toolNumberMapping[tool.ToolNumber] = tool.ToolNumber; + } +} +``` + +#### 步骤2: 解析原始钻带内容 +```csharp +// 解析原始钻带中的刀具定义行,保存参数信息 +var originalToolDefinitions = new Dictionary(); +var headerLines = new List(); + +var lines = originalDrillTape.Split(new[] { '\r', '\n' }, StringSplitOptions.None); +foreach (var line in lines) +{ + string trimmedLine = line.Trim(); + + if (trimmedLine == "%") + { + break; + } + + // 处理刀具定义行(如 T01C1.049H05000Z+0.000S060.00F105.0U0700.0) + if (Regex.IsMatch(trimmedLine, @"^T(\d+)C")) + { + var match = Regex.Match(trimmedLine, @"^T(\d+)C(.*)$"); + if (match.Success) + { + int originalToolNumber = int.Parse(match.Groups[1].Value); + originalToolDefinitions[originalToolNumber] = match.Groups[2].Value; + } + } + else + { + if (line != "") + headerLines.Add(line); + } +} +``` + +#### 步骤3: 重构刀具定义部分 +```csharp +// 按照新的刀具编号顺序输出刀具定义部分 +var sortedTools = reorderedTools.OrderBy(t => t.ToolNumber).ToList(); +foreach (var tool in sortedTools) +{ + // 查找原始刀具编号 + int originalToolNumber = -1; + foreach (var kvp in toolNumberMapping) + { + if (kvp.Value == tool.ToolNumber) + { + originalToolNumber = kvp.Key; + break; + } + } + + if (originalToolNumber != -1 && originalToolDefinitions.ContainsKey(originalToolNumber)) + { + string toolDef = $"T{tool.ToolNumber:D2}C{originalToolDefinitions[originalToolNumber]}"; + result.Add(toolDef); + } +} +``` + +#### 步骤4: 处理坐标数据和机台码 +```csharp +// 按新刀具编号顺序输出刀具切换行和对应的坐标数据 +var sortedToolsForData = reorderedTools.OrderBy(t => t.ToolNumber).ToList(); + +foreach (var tool in sortedToolsForData) +{ + // 添加刀具切换行 + result.Add($"T{tool.ToolNumber:D2}"); + + // 添加该刀具对应的所有坐标数据 + if (tool.ToolType != ToolType.MachineCode && tool.HoleLocations != null && tool.HoleLocations.Count > 0) + { + foreach (var location in tool.HoleLocations) + { + result.Add(location); + } + } + + // 如果是机台码刀具,添加机台码命令和坐标 + if (tool.ToolType == ToolType.MachineCode) + { + if (!string.IsNullOrEmpty(tool.MachineCodeCommand) && !string.IsNullOrEmpty(tool.MachineCodeType)) + { + result.Add($"{tool.MachineCodeCommand},{tool.MachineCodeType},$S $N"); + + // 添加机台码的坐标数据 + if (tool.HoleLocations != null && tool.HoleLocations.Count > 0) + { + foreach (var location in tool.HoleLocations) + { + result.Add(location); + } + } + } + } +} +``` + +### 3. 关键特性实现 + +#### 机台码处理逻辑 +- 机台码刀具保持原始编号不变 +- 机台码命令(M97/M98)和类型(A*/B*)正确保留 +- 机台码坐标数据完整保留 + +#### 数据映射机制 +- 创建原始编号到新编号的完整映射 +- 支持混合编号场景(部分重新编号) +- 确保坐标数据正确绑定到对应刀具 + +#### 钻带内容重构 +- 完整保留原始刀具参数 +- 按新编号顺序重新组织钻带结构 +- 确保格式和语法正确性 + +### 4. 实现优势 + +1. **功能一致性**: 与 `ReorderAndRenumberTools` 方法功能完全一致 +2. **向后兼容**: 保持原有方法签名,现有调用代码无需修改 +3. **健壮性**: 完整的错误处理和边界条件检查 +4. **可维护性**: 清晰的代码结构和注释 + +### 5. 测试验证 + +需要验证以下场景: +1. 普通刀具重排和重新编号 +2. 机台码刀具保持原始编号 +3. 混合场景(部分机台码、部分普通刀具) +4. 坐标数据正确绑定 +5. 钻带格式正确性 + +## 实现建议 + +建议切换到 Code 模式进行实际的代码修改,因为 Architect 模式只能编辑 Markdown 文件。实现时应严格按照上述方案进行,确保与 `ReorderAndRenumberTools` 方法的行为完全一致。 \ No newline at end of file diff --git a/MainWindow.xaml b/MainWindow.xaml new file mode 100644 index 0000000..2edfbc8 --- /dev/null +++ b/MainWindow.xaml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + +