From cacbbe6c33e8e6cc3b6f5446787f597868c8a969 Mon Sep 17 00:00:00 2001
From: "Mr.Xia" <1424473282@qq.com>
Date: Fri, 5 Jun 2026 22:17:15 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E9=92=BB=E5=B8=A6?=
=?UTF-8?q?=E6=B6=A8=E7=BC=A9=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E5=80=8D=E7=8E=87/PPM=E6=A8=A1=E5=BC=8F=E7=BC=A9=E6=94=BE?=
=?UTF-8?q?=E5=9D=90=E6=A0=87?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 ExcellonScaler 引擎:普通孔缩放坐标,G85槽孔保护槽长,刀具定义行原样保留
- 新增 DrillScalingWindow 参数设置窗口:倍率/PPM模式切换,X/Y独立输入
- 集成到启动功能菜单:.dr2/.dpin 文件显示"涨缩"按钮
- 输出覆盖原文件 + .bak 备份,GB2312 编码
---
App.xaml.cs | 43 +++++++++
DrillScalingViewModel.cs | 145 +++++++++++++++++++++++++++++++
DrillScalingWindow.xaml | 94 ++++++++++++++++++++
DrillScalingWindow.xaml.cs | 57 ++++++++++++
ExcellonScaler.cs | 154 +++++++++++++++++++++++++++++++++
StartupSelectionWindow.xaml | 3 +-
StartupSelectionWindow.xaml.cs | 11 ++-
7 files changed, 505 insertions(+), 2 deletions(-)
create mode 100644 DrillScalingViewModel.cs
create mode 100644 DrillScalingWindow.xaml
create mode 100644 DrillScalingWindow.xaml.cs
create mode 100644 ExcellonScaler.cs
diff --git a/App.xaml.cs b/App.xaml.cs
index e41972d..138c7a7 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -48,6 +48,8 @@ namespace DrillTools
bool canClearParameters = DrillTapeParameterCleaner.CanClearParameters(content);
bool canGeneratePpDrillTape = viewModel.CanGeneratePpDrillTape;
+ string extension = Path.GetExtension(filePath).ToLowerInvariant();
+ bool canScaleDrillTape = extension == ".dr2" || extension == ".dpin";
double minDrill = viewModel.MinDrillDiameter;
double minSlot = viewModel.MinSlotDiameter;
double minEA = viewModel.MinEADiameter;
@@ -62,6 +64,7 @@ namespace DrillTools
filePath,
canClearParameters,
canGeneratePpDrillTape,
+ canScaleDrillTape,
minDrill,
minSlot,
minEA,
@@ -91,6 +94,10 @@ namespace DrillTools
PerformPpDrillTapeGeneration(filePath);
Shutdown();
break;
+ case StartupAction.ScaleDrillTape:
+ PerformDrillScaling(filePath);
+ Shutdown();
+ break;
default:
Shutdown();
break;
@@ -191,6 +198,42 @@ namespace DrillTools
}
}
+ private static void PerformDrillScaling(string filePath)
+ {
+ try
+ {
+ var scalingWindow = new DrillScalingWindow();
+ scalingWindow.ShowDialog();
+
+ if (scalingWindow.ScalingResult == null)
+ return;
+
+ var (kx, ky) = scalingWindow.ScalingResult.Value;
+
+ string content = CommandTypeFileReader.ReadAllText(filePath);
+ string scaledContent = Integration.ExcellonScaler.Scale(content, kx, ky);
+
+ // 备份原文件
+ string backupFilePath = filePath + ".bak";
+ File.Copy(filePath, backupFilePath, true);
+
+ // 写入缩放后的内容
+ using var writer = new StreamWriter(filePath, false, Encoding.GetEncoding(936));
+ writer.Write(scaledContent);
+
+ System.Windows.MessageBox.Show(
+ $"涨缩完成,原文件已备份为:\n{Path.GetFileName(filePath)}.bak",
+ "涨缩完成",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ System.Windows.MessageBox.Show($"涨缩失败:\n{ex.Message}",
+ "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
private static void RegisterShellContextMenu()
{
try
diff --git a/DrillScalingViewModel.cs b/DrillScalingViewModel.cs
new file mode 100644
index 0000000..534f6e6
--- /dev/null
+++ b/DrillScalingViewModel.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace DrillTools
+{
+ ///
+ /// 涨缩参数输入模式
+ ///
+ public enum ScalingMode
+ {
+ /// 倍率模式
+ Ratio,
+ /// PPM 模式
+ Ppm
+ }
+
+ ///
+ /// 钻带涨缩窗口视图模型
+ ///
+ public class DrillScalingViewModel : INotifyPropertyChanged
+ {
+ private ScalingMode _mode = ScalingMode.Ratio;
+ private string _inputX = "1.0";
+ private string _inputY = "1.0";
+
+ ///
+ /// 缩放模式(倍率 / PPM)
+ ///
+ public ScalingMode Mode
+ {
+ get => _mode;
+ set
+ {
+ if (SetProperty(ref _mode, value))
+ {
+ OnPropertyChanged(nameof(ModeLabelX));
+ OnPropertyChanged(nameof(ModeLabelY));
+ OnPropertyChanged(nameof(ModeDescription));
+ }
+ }
+ }
+
+ ///
+ /// X 方向输入值(原始字符串)
+ ///
+ public string InputX
+ {
+ get => _inputX;
+ set => SetProperty(ref _inputX, value);
+ }
+
+ ///
+ /// Y 方向输入值(原始字符串)
+ ///
+ public string InputY
+ {
+ get => _inputY;
+ set => SetProperty(ref _inputY, value);
+ }
+
+ ///
+ /// X 方向标签文本(根据模式变化)
+ ///
+ public string ModeLabelX => Mode == ScalingMode.Ratio ? "倍率 X" : "PPM X";
+
+ ///
+ /// Y 方向标签文本(根据模式变化)
+ ///
+ public string ModeLabelY => Mode == ScalingMode.Ratio ? "倍率 Y" : "PPM Y";
+
+ ///
+ /// 模式说明文本
+ ///
+ public string ModeDescription => Mode == ScalingMode.Ratio
+ ? "倍率模式:直接输入缩放倍率(如 1.0005)"
+ : "PPM 模式:输入百万分率(如 500 表示 +500ppm)";
+
+ ///
+ /// 计算后的 X 方向实际缩放倍率
+ ///
+ public double Kx
+ {
+ get
+ {
+ if (!double.TryParse(_inputX, out double value))
+ return 1.0;
+ return Mode == ScalingMode.Ratio ? value : 1.0 + value / 1e6;
+ }
+ }
+
+ ///
+ /// 计算后的 Y 方向实际缩放倍率
+ ///
+ public double Ky
+ {
+ get
+ {
+ if (!double.TryParse(_inputY, out double value))
+ return 1.0;
+ return Mode == ScalingMode.Ratio ? value : 1.0 + value / 1e6;
+ }
+ }
+
+ ///
+ /// 验证输入是否有效
+ ///
+ /// 验证结果,null 表示有效,非 null 为错误消息
+ public string? Validate()
+ {
+ if (!double.TryParse(_inputX, out double xVal))
+ return "X 值不是有效的数字";
+ if (!double.TryParse(_inputY, out double yVal))
+ return "Y 值不是有效的数字";
+
+ if (Mode == ScalingMode.Ratio)
+ {
+ if (xVal <= 0)
+ return "倍率 X 必须大于 0";
+ if (yVal <= 0)
+ return "倍率 Y 必须大于 0";
+ }
+
+ return null;
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value))
+ return false;
+
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/DrillScalingWindow.xaml b/DrillScalingWindow.xaml
new file mode 100644
index 0000000..e425789
--- /dev/null
+++ b/DrillScalingWindow.xaml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DrillScalingWindow.xaml.cs b/DrillScalingWindow.xaml.cs
new file mode 100644
index 0000000..62ce52d
--- /dev/null
+++ b/DrillScalingWindow.xaml.cs
@@ -0,0 +1,57 @@
+using System.Windows;
+
+namespace DrillTools
+{
+ ///
+ /// 钻带涨缩参数设置窗口
+ ///
+ public partial class DrillScalingWindow : Window
+ {
+ private readonly DrillScalingViewModel _viewModel;
+
+ ///
+ /// 用户确认后的缩放结果(kx, ky)
+ ///
+ public (double Kx, double Ky)? ScalingResult { get; private set; }
+
+ public DrillScalingWindow()
+ {
+ InitializeComponent();
+ _viewModel = new DrillScalingViewModel();
+ DataContext = _viewModel;
+ }
+
+ private void ModeRadioButton_Changed(object sender, RoutedEventArgs e)
+ {
+ if (_viewModel == null)
+ return;
+
+ if (RatioRadioButton.IsChecked == true)
+ _viewModel.Mode = ScalingMode.Ratio;
+ else if (PpmRadioButton.IsChecked == true)
+ _viewModel.Mode = ScalingMode.Ppm;
+ }
+
+ private void ExecuteButton_Click(object sender, RoutedEventArgs e)
+ {
+ var error = _viewModel.Validate();
+ if (error != null)
+ {
+ ErrorText.Text = error;
+ ErrorText.Visibility = Visibility.Visible;
+ return;
+ }
+
+ ScalingResult = (_viewModel.Kx, _viewModel.Ky);
+ DialogResult = true;
+ Close();
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ ScalingResult = null;
+ DialogResult = false;
+ Close();
+ }
+ }
+}
diff --git a/ExcellonScaler.cs b/ExcellonScaler.cs
new file mode 100644
index 0000000..bac7046
--- /dev/null
+++ b/ExcellonScaler.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace DrillTools.Integration
+{
+ ///
+ /// Excellon 钻带涨缩引擎
+ /// 对钻孔坐标进行 X/Y 方向独立缩放,保持槽孔长度不变
+ ///
+ internal static class ExcellonScaler
+ {
+ // 坐标模式:X 后跟可选符号的数字(可能含小数点),Y 同理
+ private static readonly Regex CoordinatePattern = new(
+ @"^X([+-]?\d+\.?\d*)Y([+-]?\d+\.?\d*)$",
+ RegexOptions.Compiled);
+
+ // G85 槽孔模式:X...Y...G85X...Y...
+ private static readonly Regex SlotPattern = new(
+ @"^(X[+-]?\d+\.?\d*Y[+-]?\d+\.?\d*)G85(X[+-]?\d+\.?\d*Y[+-]?\d+\.?\d*)$",
+ RegexOptions.Compiled);
+
+ // 解析 X 或 Y 坐标值的子模式
+ private static readonly Regex XValuePattern = new(
+ @"X([+-]?\d+\.?\d*)", RegexOptions.Compiled);
+ private static readonly Regex YValuePattern = new(
+ @"Y([+-]?\d+\.?\d*)", RegexOptions.Compiled);
+
+ // 刀具定义行模式(在头部,如 T01C0.799H05000Z+0.000S060.00)
+ private static readonly Regex ToolDefinitionPattern = new(
+ @"^T\d+C", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ ///
+ /// 对钻带内容执行涨缩
+ ///
+ /// 原始钻带内容
+ /// X 方向缩放倍率
+ /// Y 方向缩放倍率
+ /// 缩放后的钻带内容
+ public static string Scale(string content, double kx, double ky)
+ {
+ if (content == null)
+ throw new ArgumentNullException(nameof(content));
+
+ var lines = content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
+ var result = new List(lines.Length);
+
+ foreach (var line in lines)
+ {
+ string trimmed = line.Trim();
+
+ // 空行直接保留
+ if (string.IsNullOrEmpty(trimmed))
+ {
+ result.Add(line);
+ continue;
+ }
+
+ // 刀具定义行(TxxC...):严格原样保留
+ if (ToolDefinitionPattern.IsMatch(trimmed))
+ {
+ result.Add(line);
+ continue;
+ }
+
+ // G85 槽孔行:缩放中心点,保持槽长不变
+ var slotMatch = SlotPattern.Match(trimmed);
+ if (slotMatch.Success)
+ {
+ result.Add(ScaleSlotLine(trimmed, kx, ky));
+ continue;
+ }
+
+ // 普通坐标行:X...Y...
+ var coordMatch = CoordinatePattern.Match(trimmed);
+ if (coordMatch.Success)
+ {
+ result.Add(ScaleCoordinateLine(trimmed, kx, ky));
+ continue;
+ }
+
+ // 其余行原样保留(M48, %, Txx, M30 等)
+ result.Add(line);
+ }
+
+ return string.Join("\r\n", result);
+ }
+
+ ///
+ /// 缩放普通坐标行
+ ///
+ private static string ScaleCoordinateLine(string line, double kx, double ky)
+ {
+ var xMatch = XValuePattern.Match(line);
+ var yMatch = YValuePattern.Match(line);
+
+ if (!xMatch.Success || !yMatch.Success)
+ return line;
+
+ double xOrig = double.Parse(xMatch.Groups[1].Value);
+ double yOrig = double.Parse(yMatch.Groups[1].Value);
+
+ long xNew = (long)Math.Round(xOrig * kx);
+ long yNew = (long)Math.Round(yOrig * ky);
+
+ return $"X{xNew}Y{yNew}";
+ }
+
+ ///
+ /// 缩放 G85 槽孔行
+ /// 缩放中心点,保持原始矢量恢复起止点(槽长不变)
+ ///
+ private static string ScaleSlotLine(string line, double kx, double ky)
+ {
+ var slotMatch = SlotPattern.Match(line);
+ if (!slotMatch.Success)
+ return line;
+
+ // 解析起点坐标
+ var startPart = slotMatch.Groups[1].Value;
+ var xStartMatch = XValuePattern.Match(startPart);
+ var yStartMatch = YValuePattern.Match(startPart);
+ double xStart = double.Parse(xStartMatch.Groups[1].Value);
+ double yStart = double.Parse(yStartMatch.Groups[1].Value);
+
+ // 解析终点坐标
+ var endPart = slotMatch.Groups[2].Value;
+ var xEndMatch = XValuePattern.Match(endPart);
+ var yEndMatch = YValuePattern.Match(endPart);
+ double xEnd = double.Parse(xEndMatch.Groups[1].Value);
+ double yEnd = double.Parse(yEndMatch.Groups[1].Value);
+
+ // 计算中心点
+ double cx = (xStart + xEnd) / 2.0;
+ double cy = (yStart + yEnd) / 2.0;
+
+ // 缩放中心点
+ long cxNew = (long)Math.Round(cx * kx);
+ long cyNew = (long)Math.Round(cy * ky);
+
+ // 保持原始矢量(槽长不变)
+ long dx = (long)Math.Round((xEnd - xStart) / 2.0);
+ long dy = (long)Math.Round((yEnd - yStart) / 2.0);
+
+ // 恢复起止点
+ long newStartX = cxNew - dx;
+ long newStartY = cyNew - dy;
+ long newEndX = cxNew + dx;
+ long newEndY = cyNew + dy;
+
+ return $"X{newStartX}Y{newStartY}G85X{newEndX}Y{newEndY}";
+ }
+ }
+}
diff --git a/StartupSelectionWindow.xaml b/StartupSelectionWindow.xaml
index 8b33214..91276d7 100644
--- a/StartupSelectionWindow.xaml
+++ b/StartupSelectionWindow.xaml
@@ -97,7 +97,8 @@
-
+
+
diff --git a/StartupSelectionWindow.xaml.cs b/StartupSelectionWindow.xaml.cs
index d9df851..006ed8a 100644
--- a/StartupSelectionWindow.xaml.cs
+++ b/StartupSelectionWindow.xaml.cs
@@ -9,7 +9,8 @@ namespace DrillTools
AdjustToolOrder,
ExportHoleCount,
ClearParameters,
- GeneratePpDrillTape
+ GeneratePpDrillTape,
+ ScaleDrillTape
}
public partial class StartupSelectionWindow : Window
@@ -17,6 +18,7 @@ namespace DrillTools
public StartupAction SelectedAction { get; private set; } = StartupAction.None;
public StartupSelectionWindow(string filePath, bool canClearParameters = false, bool canGeneratePpDrillTape = false,
+ bool canScaleDrillTape = false,
double minDrillDiameter = 0, double minSlotDiameter = 0, double minEADiameter = 0,
bool isPpDrillTape = false, double ppXSpacing = 0, double ppYSpacing = 0,
bool hasOuter3175Spacing = false, double outer3175XSpacing = 0, double outer3175YSpacing = 0)
@@ -37,6 +39,7 @@ namespace DrillTools
};
ClearParametersButton.Visibility = canClearParameters ? Visibility.Visible : Visibility.Collapsed;
GeneratePpDrillTapeButton.Visibility = canGeneratePpDrillTape ? Visibility.Visible : Visibility.Collapsed;
+ ScaleDrillTapeButton.Visibility = canScaleDrillTape ? Visibility.Visible : Visibility.Collapsed;
}
private void AdjustToolOrder_Click(object sender, RoutedEventArgs e)
@@ -62,5 +65,11 @@ namespace DrillTools
SelectedAction = StartupAction.GeneratePpDrillTape;
Close();
}
+
+ private void ScaleDrillTape_Click(object sender, RoutedEventArgs e)
+ {
+ SelectedAction = StartupAction.ScaleDrillTape;
+ Close();
+ }
}
}