ADR-004: 符号库更新导出修复
状态
已接受 (Accepted)
日期
2026-01-21
上下文
在 v3.0.0 版本中,我们实现了符号库的追加和更新导出功能。然而,在实际使用中发现了一个严重的 bug:当使用更新导出功能时,分体式符号(multi-part symbols)的子符号会被错误地作为独立的顶层符号存在,导致符号库格式破坏。
发现的问题
问题 1:更新模式参数未传递 🔴 严重
问题描述:
- ExportService_Pipeline::mergeSymbolLibrary() 方法中,updateMode 参数没有被正确传递给 exportSymbolLibrary 方法
- 原代码只使用了 overwriteExistingFiles 来设置 appendMode,忽略了 updateMode
- 导致更新导出时,所有已存在的符号都被跳过,而不是被覆盖
影响范围: - 所有使用更新模式的符号库导出 - 用户无法更新已存在的符号
代码位置:
- src/services/ExportService_Pipeline.cpp:395-397
示例:
// 原代码(错误)
bool appendMode = !m_options.overwriteExistingFiles;
bool success = exporter.exportSymbolLibrary(m_symbols, m_options.libName, libraryPath, appendMode);
// 修复后(正确)
bool appendMode = !m_options.overwriteExistingFiles;
bool updateMode = m_options.updateMode;
bool success = exporter.exportSymbolLibrary(m_symbols, m_options.libName, libraryPath, appendMode, updateMode);
问题 2:子符号识别不准确 🔴 严重
问题描述:
- 原代码使用简单的字符串匹配 startsWith(parentSymbolName + "_") 来识别子符号
- 这种方法无法准确区分分体式符号和单体符号的子符号
- 可能误删其他符号的子符号
影响范围: - 混合符号库(同时包含分体式符号和单体符号) - 符号名称包含下划线的符号
代码位置:
- src/core/kicad/ExporterSymbol.cpp:195-206
示例场景:
假设符号库中包含:
- 分体式符号 A:MCU_A,有子符号 MCU_A_1_1, MCU_A_2_1
- 单体符号 B:Resistor_B,有子符号 Resistor_B_0_1
当更新 MCU_A 时,原代码会:
1. 识别 MCU_A 为要覆盖的符号
2. 查找以 MCU_A_ 开头的子符号并标记删除
3. 但如果 Resistor_B_0_1 被错误识别,可能导致格式破坏
问题 3:孤离子符号未删除 🔴 严重
问题描述: - 在某些情况下,分体式符号的子符号可能被错误地作为顶层符号导出 - 原代码只删除了父符号内部的子符号,没有删除作为顶层符号存在的孤离子符号 - 导致更新后,旧的孤离子符号仍然存在
影响范围: - 之前使用错误方式导出的符号库 - 符号库结构不正确的情况
代码位置:
- src/core/kicad/ExporterSymbol.cpp:310-360
示例场景:
假设符号库中包含:
✅ 正确的结构:
- 父符号:MCIMX6Y2CVM08AB
- 子符号:MCIMX6Y2CVM08AB_1_1
- 子符号:MCIMX6Y2CVM08AB_2_1
❌ 实际存在的错误结构:
- 父符号:MCIMX6Y2CVM08AB
- 子符号:MCIMX6Y2CVM08AB_1_1
- 顶层符号:MCIMX6Y2CVM08AB_2_1(错误!)
当更新 MCIMX6Y2CVM08AB 时:
1. MCIMX6Y2CVM08AB 被正确覆盖
2. MCIMX6Y2CVM08AB_1_1 作为子符号被正确删除和重新生成
3. MCIMX6Y2CVM08AB_2_1 作为顶层符号被保留(错误!)
4. 新的 MCIMX6Y2CVM08AB_2_1 作为子符号被生成
5. 结果:符号库中存在两个 MCIMX6Y2CVM08AB_2_1,格式破坏
决策
我们实施了以下修复方案来解决上述问题:
修复 1:传递 updateMode 参数
问题:updateMode 参数没有被正确传递给 exportSymbolLibrary 方法
解决方案:
// 修复前
bool appendMode = !m_options.overwriteExistingFiles;
bool success = exporter.exportSymbolLibrary(m_symbols, m_options.libName, libraryPath, appendMode);
// 修复后
bool appendMode = !m_options.overwriteExistingFiles;
bool updateMode = m_options.updateMode;
qDebug() << "Merge settings - Append mode:" << appendMode << "Update mode:" << updateMode;
bool success = exporter.exportSymbolLibrary(m_symbols, m_options.libName, libraryPath, appendMode, updateMode);
影响: - 更新模式现在能够正确覆盖已存在的符号 - 追加模式仍然保留已存在的符号
修复 2:改进子符号识别逻辑
问题:简单的字符串匹配无法准确区分分体式符号和单体符号的子符号
解决方案:
// 首先分析现有符号,确定哪些是分体式符号(有多个子符号)
QMap<QString, QStringList> parentToSubSymbols; // 父符号名 -> 子符号列表
for (const QString &subSymbolName : subSymbolNames)
{
// 从子符号名称中提取父符号名
// 子符号格式:{parentName}_{unitNumber}_1
int lastUnderscore = subSymbolName.lastIndexOf('_');
if (lastUnderscore > 0)
{
int secondLastUnderscore = subSymbolName.lastIndexOf('_', lastUnderscore - 1);
if (secondLastUnderscore > 0)
{
QString parentName = subSymbolName.left(secondLastUnderscore);
parentToSubSymbols[parentName].append(subSymbolName);
}
}
}
// 对于每个被覆盖的父符号,收集其所有子符号
for (const QString &parentSymbolName : overwrittenSymbolNames)
{
if (parentToSubSymbols.contains(parentSymbolName))
{
// 这是一个分体式符号,删除所有子符号
for (const QString &subSymbolName : parentToSubSymbols[parentSymbolName])
{
subSymbolsToDelete.insert(subSymbolName);
qDebug() << "Marking sub-symbol for deletion (multipart):" << subSymbolName << "(parent:" << parentSymbolName << ")";
}
}
else
{
// 这是一个单体符号,查找其子符号(格式:{parentName}_0_1)
QString expectedSubSymbolName = parentSymbolName + "_0_1";
if (subSymbolNames.contains(expectedSubSymbolName))
{
subSymbolsToDelete.insert(expectedSubSymbolName);
qDebug() << "Marking sub-symbol for deletion (single part):" << expectedSubSymbolName << "(parent:" << parentSymbolName << ")";
}
}
}
影响: - 准确区分分体式符号和单体符号 - 避免误删其他符号的子符号 - 支持混合符号库的正确处理
修复 3:删除孤立的顶层子符号
问题:作为顶层符号存在的孤离子符号没有被删除
解决方案:
// 检查是否是以被覆盖符号名开头的顶层符号(可能是之前错误导出的子符号)
bool isOrphanedSubSymbol = false;
if (!isOverwritten)
{
for (const QString &parentSymbolName : overwrittenSymbolNames)
{
if (symbolName.startsWith(parentSymbolName + "_"))
{
isOrphanedSubSymbol = true;
qDebug() << "Skipping orphaned sub-symbol (top-level):" << symbolName << "(parent:" << parentSymbolName << ")";
break;
}
}
}
// 只导出未被覆盖且不是孤离子符号的符号
if (!isOverwritten && !isOrphanedSubSymbol)
{
// 导出符号内容
out << filteredContent;
qDebug() << "Keeping existing symbol:" << symbolName;
}
影响: - 删除所有以被覆盖父符号名开头的顶层符号 - 确保符号库结构的正确性 - 避免重复的子符号
后果
正面影响
- 修复了更新导出功能
- 更新模式现在能够正确覆盖已存在的符号
-
用户可以更新符号库中的符号
-
准确的子符号处理
- 正确区分分体式符号和单体符号
- 分体式符号:删除所有子符号(
_1_1,_2_1, ...) -
单体符号:只删除
_0_1子符号 -
符号库结构正确性
- 删除孤立的顶层子符号
- 确保子符号只在父符号内部存在
-
避免符号库格式破坏
-
支持混合符号库
- 正确处理同时包含分体式符号和单体符号的符号库
- 避免误删其他符号的子符号
负面影响
- 复杂度增加
- 子符号识别逻辑更加复杂
-
需要分析符号库结构
-
性能影响
- 需要额外的符号分析步骤
-
可能略微增加导出时间
-
向后兼容性
- 可能需要清理之前导出的错误符号库
- 用户可能需要重新导出符号库
替代方案
方案 A:使用正则表达式匹配子符号
描述:使用正则表达式来匹配子符号名称
优点: - 更灵活的模式匹配 - 可以处理更复杂的命名规则
缺点: - 正则表达式性能较差 - 代码可读性降低
选择:未采用,当前方案更简单高效
方案 B:重新导出整个符号库
描述:在更新模式下,完全重新导出符号库
优点: - 实现简单 - 确保符号库结构正确
缺点: - 性能较差 - 无法保留用户修改
选择:未采用,当前方案更高效且保留用户修改
实施细节
修改文件
修复 1:
1. src/services/ExportService_Pipeline.cpp - 传递 updateMode 参数
修复 2:
1. src/core/kicad/ExporterSymbol.cpp - 改进子符号识别逻辑
修复 3:
1. src/core/kicad/ExporterSymbol.cpp - 删除孤立的顶层子符号
测试验证
测试场景: 1. 更新分体式符号 2. 更新单体符号 3. 混合符号库更新 4. 符号库包含孤离子符号的情况
预期结果: - 分体式符号被正确覆盖,所有子符号被正确更新 - 单体符号被正确覆盖,子符号被正确更新 - 混合符号库中,其他符号不受影响 - 孤离子符号被正确删除