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 @@ + + + + + + + + + + + + + + + +