Files
AohDrllTools/RouParameterReplacer.cs
Mr.Xia b75439cff4 fix: 修复含特殊字符路径的钻带文件读取失败 — 移除 cmd /c type,改为 AnsiFileReader 直接文件IO
问题:路径含 ( ) & % ! ^ @ 等字符时报"文件路径包含不允许的命令行特殊字符",
(D2-f)、(1) 这类正常业务文件名直接命中。

根因:读取统一入口把路径拼进命令行(cmd.exe /d /c type "..."),为"防炸"在 03d9b8b
引入 ForbiddenPathChars 黑名单——并非业务需求,是实现的自我约束。
实测:真正会坏的只有 %VAR%(必坏) 与延迟展开开启时的 !VAR!,而 ( ) & ^ @ 空格在
双引号内均正常;且 type 是字节透传,这层 cmd 对当初的编码问题毫无帮助。

改动:
- 新增 AnsiFileReader.cs:FileStream + StreamReader(默认 936 解码 + BOM 探测,
  与旧 StreamReader 解码路径逐字符等价),不经命令行
- 删除 CommandTypeFileReader.cs 及其 ForbiddenPathChars 字符校验
- 路径只保留"非空"与"文件存在"校验;IO 异常包装为
  InvalidOperationException("读取文件失败:{路径}({原因})")并保留 InnerException
- 15 处调用点同步改名;更新 CLAUDE.md 与 Docs 说明

验证:14 个 demo_drl 样例新旧实现字符串/行数逐字符一致;含特殊字符路径(含 %TEMP% 字面
路径,旧实现 rc=1)可读可解析;UTF-8/UTF-16 BOM 结果与旧实现一致;
dotnet build -p:Platform=x86 0 错误且警告集与基线逐条相同。
2026-09-20 11:30:04 +08:00

224 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace DrillTools
{
/// <summary>
/// ROU 成型参数替换:将参数文件中的 S/F/R/Z/H 参数追加到 .rou 文件的刀径定义行。
/// </summary>
internal static class RouParameterReplacer
{
private const int Gb2312CodePage = 936;
private const double DiameterTolerance = 0.001;
/// <summary>
/// 匹配参数行:T00C<直径>S<参数...>
/// 例:T00C1.800S55.0F5.0R25Z0H600
/// </summary>
private static readonly Regex ParamLinePattern = new(
@"^T00C(?<diameter>\d+(?:\.\d+)?)(?<params>S.+?)\s*$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// 匹配 .rou 刀径定义行:T<刀号>C<直径>
/// 例:T01C1.8 → prefix="T01C", diameter="1.8"
/// </summary>
private static readonly Regex ToolLinePattern = new(
@"^(?<prefix>T\d+C)(?<diameter>\d+(?:\.\d+)?)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// 执行参数替换。
/// </summary>
/// <param name="rouFilePath">.rou 文件完整路径</param>
/// <param name="paramFilePath">参数 .txt 文件完整路径</param>
public static void ReplaceParameters(string rouFilePath, string paramFilePath, out List<double> unmatchedDiameters)
{
// 1. 读取参数文件,构建直径→参数字符串映射
var paramDict = LoadParameterDictionary(paramFilePath);
if (paramDict.Count == 0)
throw new InvalidOperationException("参数文件中未找到有效的参数行(格式:T00C<直径>S...)");
// 2. 读取 .rou 文件
string rouContent = AnsiFileReader.ReadAllText(rouFilePath);
// 3. 执行替换(同时收集未匹配直径)
var unmatchedSet = new HashSet<double>();
string replacedContent = ReplaceParametersInContent(rouContent, paramDict, unmatchedSet);
// 4. 备份原文件
string backupFilePath = rouFilePath + ".bak";
File.Copy(rouFilePath, backupFilePath, true);
// 5. 写回文件
using var writer = new StreamWriter(rouFilePath, false, Encoding.GetEncoding(Gb2312CodePage));
writer.Write(replacedContent);
// 6. 输出未匹配直径
unmatchedDiameters = new List<double>(unmatchedSet);
}
/// <summary>
/// 从参数文件加载直径→参数字符串(从 S 开始到行尾)的映射。
/// </summary>
private static Dictionary<double, string> LoadParameterDictionary(string paramFilePath)
{
var dict = new Dictionary<double, string>();
string content = AnsiFileReader.ReadAllText(paramFilePath);
var lines = DrillTapeLineParser.SplitLines(content);
foreach (var line in lines)
{
string trimmed = line.Text.Trim();
var match = ParamLinePattern.Match(trimmed);
if (!match.Success)
continue;
if (!double.TryParse(match.Groups["diameter"].Value,
NumberStyles.Float, CultureInfo.InvariantCulture, out double diameter))
continue;
// 规范化到3位小数,消除浮点精度差异
diameter = Math.Round(diameter, 3);
string paramString = match.Groups["params"].Value;
// 如果同一直径出现多次,后面的覆盖前面的
dict[diameter] = paramString;
}
return dict;
}
/// <summary>
/// 在 .rou 文件内容中,将 M48~% 区域内的刀径行末尾追加参数。
/// 使用两遍扫描:第一遍按基准直径分组并从参数字典中分配可用键,第二遍执行替换。
/// </summary>
private static string ReplaceParametersInContent(string rouContent, Dictionary<double, string> paramDict, HashSet<double> unmatchedDiameters)
{
var lines = DrillTapeLineParser.SplitLines(rouContent);
// === Pass 1: 收集头部区域所有刀径行,按基准直径分组,分配参数键 ===
bool inHeader = false;
var toolEntries = new List<(int Index, string Prefix, double Diameter)>();
for (int i = 0; i < lines.Count; i++)
{
string trimmed = lines[i].Text.Trim();
if (!inHeader && trimmed == "M48")
{
inHeader = true;
continue;
}
if (inHeader && trimmed == "%")
{
inHeader = false;
continue;
}
if (inHeader)
{
var match = ToolLinePattern.Match(trimmed);
if (match.Success &&
double.TryParse(match.Groups["diameter"].Value,
NumberStyles.Float, CultureInfo.InvariantCulture, out double diameter))
{
diameter = Math.Round(diameter, 3);
string prefix = match.Groups["prefix"].Value;
toolEntries.Add((i, prefix, diameter));
}
}
}
// 按基准直径分组,为每组分配显示直径和参数键
// lineIndex -> (prefix, displayDiameter, assignedParamKey | null)
var toolMap = new Dictionary<int, (string Prefix, double DisplayDiameter, double? ParamKey)>();
foreach (var group in toolEntries.GroupBy(e => e.Diameter))
{
double baseDiam = group.Key;
var occurrences = group.ToList();
// 在 [baseDiam, baseDiam + 0.01) 范围内查找候选参数键
var candidates = paramDict.Keys
.Where(k => k >= baseDiam && k < baseDiam + 0.01)
.OrderBy(k => k)
.ToList();
// 如果没有候选键,尝试容差回退(仅对首个出现)
if (candidates.Count == 0)
{
foreach (var kvp in paramDict)
{
if (Math.Abs(kvp.Key - baseDiam) < DiameterTolerance)
{
candidates.Add(kvp.Key);
break;
}
}
}
// 分配:每个出现的显示直径为 baseDiam + j*0.001(3位小数补零)
// 参数键从候选列表中按序取,超出则为 null(未匹配)
for (int j = 0; j < occurrences.Count; j++)
{
double displayDiameter = Math.Round(baseDiam + j * 0.001, 3);
double? assignedKey = j < candidates.Count ? candidates[j] : null;
toolMap[occurrences[j].Index] = (occurrences[j].Prefix, displayDiameter, assignedKey);
}
}
// === Pass 2: 执行替换(修改直径文本 + 追加参数) ===
var builder = new StringBuilder(rouContent.Length + 1024);
for (int i = 0; i < lines.Count; i++)
{
if (toolMap.TryGetValue(i, out var toolInfo))
{
if (toolInfo.ParamKey.HasValue)
{
string paramString = paramDict[toolInfo.ParamKey.Value];
// 重建行:prefix + 显示直径 + 参数
string displayDiam = toolInfo.DisplayDiameter.ToString("F3", CultureInfo.InvariantCulture);
// 保留行尾空白
string originalLine = lines[i].Text.TrimEnd();
int trailingLength = lines[i].Text.Length - originalLine.Length;
string trailing = trailingLength > 0 ? lines[i].Text[^trailingLength..] : string.Empty;
builder.Append(toolInfo.Prefix);
builder.Append(displayDiam);
builder.Append(paramString);
builder.Append(trailing);
builder.Append(lines[i].LineEnding);
}
else
{
// 未匹配:保留原始行,记录直径
unmatchedDiameters.Add(toolInfo.DisplayDiameter);
builder.Append(lines[i].Text);
builder.Append(lines[i].LineEnding);
}
}
else
{
// 非刀径行原样保留
builder.Append(lines[i].Text);
builder.Append(lines[i].LineEnding);
}
}
return builder.ToString();
}
}
}