feat: 增加钻带涨缩功能,支持倍率/PPM模式缩放坐标

- 新增 ExcellonScaler 引擎:普通孔缩放坐标,G85槽孔保护槽长,刀具定义行原样保留
- 新增 DrillScalingWindow 参数设置窗口:倍率/PPM模式切换,X/Y独立输入
- 集成到启动功能菜单:.dr2/.dpin 文件显示"涨缩"按钮
- 输出覆盖原文件 + .bak 备份,GB2312 编码
This commit is contained in:
2026-06-05 22:17:15 +08:00
parent f050a606ef
commit cacbbe6c33
7 changed files with 505 additions and 2 deletions

View File

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

145
DrillScalingViewModel.cs Normal file
View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace DrillTools
{
/// <summary>
/// 涨缩参数输入模式
/// </summary>
public enum ScalingMode
{
/// <summary>倍率模式</summary>
Ratio,
/// <summary>PPM 模式</summary>
Ppm
}
/// <summary>
/// 钻带涨缩窗口视图模型
/// </summary>
public class DrillScalingViewModel : INotifyPropertyChanged
{
private ScalingMode _mode = ScalingMode.Ratio;
private string _inputX = "1.0";
private string _inputY = "1.0";
/// <summary>
/// 缩放模式(倍率 / PPM
/// </summary>
public ScalingMode Mode
{
get => _mode;
set
{
if (SetProperty(ref _mode, value))
{
OnPropertyChanged(nameof(ModeLabelX));
OnPropertyChanged(nameof(ModeLabelY));
OnPropertyChanged(nameof(ModeDescription));
}
}
}
/// <summary>
/// X 方向输入值(原始字符串)
/// </summary>
public string InputX
{
get => _inputX;
set => SetProperty(ref _inputX, value);
}
/// <summary>
/// Y 方向输入值(原始字符串)
/// </summary>
public string InputY
{
get => _inputY;
set => SetProperty(ref _inputY, value);
}
/// <summary>
/// X 方向标签文本(根据模式变化)
/// </summary>
public string ModeLabelX => Mode == ScalingMode.Ratio ? "倍率 X" : "PPM X";
/// <summary>
/// Y 方向标签文本(根据模式变化)
/// </summary>
public string ModeLabelY => Mode == ScalingMode.Ratio ? "倍率 Y" : "PPM Y";
/// <summary>
/// 模式说明文本
/// </summary>
public string ModeDescription => Mode == ScalingMode.Ratio
? "倍率模式:直接输入缩放倍率(如 1.0005"
: "PPM 模式:输入百万分率(如 500 表示 +500ppm";
/// <summary>
/// 计算后的 X 方向实际缩放倍率
/// </summary>
public double Kx
{
get
{
if (!double.TryParse(_inputX, out double value))
return 1.0;
return Mode == ScalingMode.Ratio ? value : 1.0 + value / 1e6;
}
}
/// <summary>
/// 计算后的 Y 方向实际缩放倍率
/// </summary>
public double Ky
{
get
{
if (!double.TryParse(_inputY, out double value))
return 1.0;
return Mode == ScalingMode.Ratio ? value : 1.0 + value / 1e6;
}
}
/// <summary>
/// 验证输入是否有效
/// </summary>
/// <returns>验证结果null 表示有效,非 null 为错误消息</returns>
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<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.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));
}
}
}

94
DrillScalingWindow.xaml Normal file
View File

@@ -0,0 +1,94 @@
<Window x:Class="DrillTools.DrillScalingWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="钻带涨缩"
Width="350"
SizeToContent="Height"
ResizeMode="NoResize"
Topmost="True"
WindowStartupLocation="CenterScreen">
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 模式选择 -->
<GroupBox Grid.Row="0" Header="输入模式" Margin="0,0,0,10">
<StackPanel>
<RadioButton Name="RatioRadioButton"
Content="倍率模式"
IsChecked="True"
Margin="0,5,0,5"
Checked="ModeRadioButton_Changed" />
<RadioButton Name="PpmRadioButton"
Content="PPM 模式"
Margin="0,0,0,5"
Checked="ModeRadioButton_Changed" />
<TextBlock Text="{Binding ModeDescription}"
Foreground="Gray"
FontSize="11"
Margin="20,0,0,5" />
</StackPanel>
</GroupBox>
<!-- X 方向参数 -->
<Grid Grid.Row="1" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding ModeLabelX}"
VerticalAlignment="Center"
Margin="0,0,10,0" />
<TextBox Grid.Column="1"
Text="{Binding InputX, UpdateSourceTrigger=PropertyChanged}"
Height="25" />
</Grid>
<!-- Y 方向参数 -->
<Grid Grid.Row="2" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding ModeLabelY}"
VerticalAlignment="Center"
Margin="0,0,10,0" />
<TextBox Grid.Column="1"
Text="{Binding InputY, UpdateSourceTrigger=PropertyChanged}"
Height="25" />
</Grid>
<!-- 提示信息 -->
<TextBlock Grid.Row="3"
Name="ErrorText"
Foreground="Red"
FontSize="11"
Text=""
Margin="0,0,0,10"
Visibility="Collapsed" />
<!-- 按钮区域 -->
<StackPanel Grid.Row="4"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button Width="80"
Height="30"
Content="取消"
IsCancel="True"
Margin="0,0,10,0"
Click="CancelButton_Click" />
<Button Width="80"
Height="30"
Content="执行"
IsDefault="True"
Click="ExecuteButton_Click" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,57 @@
using System.Windows;
namespace DrillTools
{
/// <summary>
/// 钻带涨缩参数设置窗口
/// </summary>
public partial class DrillScalingWindow : Window
{
private readonly DrillScalingViewModel _viewModel;
/// <summary>
/// 用户确认后的缩放结果kx, ky
/// </summary>
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();
}
}
}

154
ExcellonScaler.cs Normal file
View File

@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace DrillTools.Integration
{
/// <summary>
/// Excellon 钻带涨缩引擎
/// 对钻孔坐标进行 X/Y 方向独立缩放,保持槽孔长度不变
/// </summary>
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);
/// <summary>
/// 对钻带内容执行涨缩
/// </summary>
/// <param name="content">原始钻带内容</param>
/// <param name="kx">X 方向缩放倍率</param>
/// <param name="ky">Y 方向缩放倍率</param>
/// <returns>缩放后的钻带内容</returns>
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<string>(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);
}
/// <summary>
/// 缩放普通坐标行
/// </summary>
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}";
}
/// <summary>
/// 缩放 G85 槽孔行
/// 缩放中心点,保持原始矢量恢复起止点(槽长不变)
/// </summary>
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}";
}
}
}

View File

@@ -97,7 +97,8 @@
<Button Width="120" Height="30" Content="调整刀序" Margin="0,0,15,0" Click="AdjustToolOrder_Click"/>
<Button Width="120" Height="30" Content="导出孔数" Margin="0,0,15,0" Click="ExportHoleCount_Click"/>
<Button Name="ClearParametersButton" Width="120" Height="30" Content="清空参数" Margin="0,0,15,0" Click="ClearParameters_Click"/>
<Button Name="GeneratePpDrillTapeButton" Width="120" Height="30" Content="生成PP钻带" Click="GeneratePpDrillTape_Click"/>
<Button Name="GeneratePpDrillTapeButton" Width="120" Height="30" Content="生成PP钻带" Margin="0,0,15,0" Click="GeneratePpDrillTape_Click"/>
<Button Name="ScaleDrillTapeButton" Width="120" Height="30" Content="涨缩" Click="ScaleDrillTape_Click"/>
</StackPanel>
</Grid>
</Window>

View File

@@ -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();
}
}
}