一、题目在算什么(业务背景)
1.1 一句话定义
这是一个基于 DFX MetaLab 平台和 vSDK 接口开发的 PCB 顶层走线跨网络间距与短路自动检测工具。
程序按任务书先将每条 TOP 线段的物理外轮廓向外扩 0.1mm,再对不同网络的外扩图形执行相交与距离检测,
以动态链接库形式集成在 DFX MetaLab 软件中,通过用户工具菜单触发,
实测约 1.55 秒完成整板 5331 条走线、492 个有效网络的检测,
找出 5400 组唯一违规,并支持表格展示与跳转定位。
1.2 PCB 是什么 / 走线 / 网络
PCB(印制电路板):电子产品里那块布满铜线的板子,连接芯片、电阻、电容等元件。
走线(Line):TOP 层上的铜线。每根走线有起点、终点、线宽、线头类型(圆头/方头)。
网络(Net):电气上连通的所有点的集合。同一网络靠在一起是正常的(本来就连通);不同网络之间必须绝缘,否则短路。
为什么间距是生死攸关的(评委必问业务价值):
- 制造风险:不同网络铜图形间距不足会降低制造裕量,可能造成蚀刻残铜、绝缘不足或短路。本题统一采用 0.1mm 规则;实际工程阈值应由板厂工艺能力和设计规则确定,不能一概而论。
- 信号干扰:高速信号线靠太近会产生电磁耦合(串扰),信号失真,设备工作异常。
所以工厂生产前必须用工具逐条检查所有走线间距——这正是我这个工具要干的事。
1.3 违规判定规则(任务书第 5 页第 6 条)
| 条件 | 含义 |
| ① 两图形相交 | 短路(设计错误,两根线重叠了) |
| ② 最小距离 < 0.1mm | 按本题规则判定为间距不足,制造裕量降低、风险增加 |
注意:只有不同网络的走线才检测。同一网络的走线靠在一起不算违规。
1.4 为什么要"外扩 0.1mm"
任务书要求先外扩再算,为什么不直接算距离?
因为这是任务书明确规定的数据预处理步骤,不是自行替换判定条件。
每条线保持中心线起点、终点和角度不变,将外轮廓向外扩 0.1mm;圆头和矩形线头均按实际物理轮廓扩展。
后续相交判断和最小距离计算都必须针对外扩后的图形执行。
因此不能简单说成“外扩后相交”等价于原图形距离小于 0.1mm——两条图形都外扩后,几何关系已经改变,仍须按任务书继续执行相交与距离两项检测。
外扩前(细): 外扩后(粗,外扩0.1mm):
╭──────╮ ╭────────╮
╱ ╲ ╱ ╲
│ ────── │ │ ────── │ ← 线宽 +0.2(两侧各扩0.1)
╲ ╱ ╲ ╱
╰──────╯ ╰────────╯
↑中心线不变↑ ↑外轮廓等距外扩↑
关键:中心线起止坐标和角度不变;宽度增加0.2mm,线头轮廓同步向外扩展
设计思路:任务二的工程化设计原则
本任务的目标是基于 DFX MetaLab 平台和 vSDK 接口,开发一个 PCB 顶层走线跨网络间距与短路自动检测工具,
按任务书规则对 TOP 层不同网络的图形判定"相交或最小距离小于 0.1mm",并以表格形式展示结果、支持点击跳转定位。
四条设计原则
整体设计遵循分层组织、预处理先行、粗筛—精算分离、结果去重可审计四条原则:
- 分层组织:interface 只负责窗口交互、表格填充和错误提示;非界面功能全部放在 logic 类,包括上下文获取、线段读取、外扩分组、候选筛选、结果收集和跳转数据;几何运算、图形创建、数据读取等底层能力复用 vSDK,不自行重写几何引擎。
- 预处理先行:按任务书在精算之前完成每条线的外轮廓外扩 0.1mm,保证后续判定严格针对"外扩后图形",而不是直接在原始图形上替换判定条件。
- 粗筛—精算分离:AABB 只做 4 次坐标比较,用于把跨网络线段对压缩到候选网络对;真正的几何运算交给 vSDK,避免在工业级几何内核之外做不严谨的近似计算。
- 结果去重可审计:PairKey 用较小 ObjectID 在高位、较大 ObjectID 在低位组成 64 位无序键,消除正反顺序重复,保证 GetTouch 与 GetDistance 两类结果合并后不重复计数;关键步骤输出 audit 日志。
六步主线流水线
| 步骤 | 设计意图 |
| 1. 上下文获取 | 按 Job→Pcb→Board→Layer 逐级取得 TOP 层,并校验 LayerSide=1、LayerType=0 确认是正面信号层 |
| 2. 线段读取 | 遍历 TOP 层对象只保留 Line 类型,读出起止坐标、线宽和线头类型 |
| 3. 外扩分组 | 中心线起止坐标不变,外轮廓向外扩 0.1mm;圆头线段按 vSDK 约定令 LineLength=LineWidth;按 NetID 分入 492 个 ShapeConter 容器 |
| 4. AABB 粗筛 | 对所有跨网络线段做轻量比较,只把可能相交或距离不足的网络对提升为候选 |
| 5. vSDK 精算 | 对每个候选网络对分别调用 GetTouch 和 GetDistance(0,0.1) |
| 6. 去重输出 | 按 ObjectID 无序对取并集、排序后填入表格 |
设计动机
为什么外扩在前?任务书明确要求"先外扩再算",这是数据预处理步骤而不是自行替换判定条件。因此不能简化为"原图形距离小于 0.1mm 等价于外扩后相交"。
为什么要粗筛—精算分离?5331 条线产生 13,966,489 对跨网络线段组合,若不筛选则 492 个网络容器两两组合为 120,786 对、241,572 次精确调用。AABB 把它压缩为 1975 个候选网络对、3950 次精确调用,减少 237,622 次。
为什么用 PairKey?无序对键同时解决两个问题:同一对图形无论 SDK 以哪个方向返回都得到同一个键;GetTouch 与 GetDistance 两类结果合并时不会重复计数。
性能与正确性
性能:粗筛边界为 lineWidth/2+0.1+0.05,对 DEMO2 的圆头线段可保证不漏掉阈值内候选,同时允许保留假候选再由精算剔除;精算只处理候选网络对,把理论上的 241,572 次精确调用压缩为 3,950 次,实测约 1.55 秒。
正确性:程序在关键步骤输出 audit 日志,记录线段数、有效网络数、跨网络线段对数、相交命中数、距离命中数和最终唯一违规数,便于现场与原始 ODB++ 文件、独立几何复算相互印证。
工程完整性
每次点击 Run 都会刷新当前 Job、Pcb、Board 上下文,避免继续使用旧工程;并在读取失败、几何失败或资源分配失败时返回明确错误。
ShapeConter、临时 Shape 和结果列表在正常结束和失败路径中统一释放,避免内存泄漏。
interface 与 logic 职责分层,符合任务书"非界面功能写在 logic 类"的要求。
一句话总结
本工具的设计思路不是"调用一个 SDK 接口得到答案",而是把题目要求拆成可验证的工程阶段:
业务规则驱动外扩方式、分组策略和判定口径;空间索引驱动性能;PairKey 去重和 audit 日志驱动正确性;分层结构和资源释放驱动工程完整性。
二、数据怎么导入
2.1 DEMO2.tgz 是什么
是 ODB++ 格式的 PCB 制造数据包——PCB 行业最通用的数据交换格式之一。
内部是层级目录结构:job → steps/pcb → layers/top(我们要的TOP层)。
2.2 DFX MetaLab 读入 + vSDK 访问的三级指针
用户拖入 DEMO2.tgz 后,软件解析构建内存对象模型。我的代码通过 vSDK 逐级获取上下文:
DEMO2.tgz
↓ (DFX MetaLab 读入)
Job (作业:整个PCB项目)
↓ vSDK_Job_GetCurrentPcb
Pcb (单块板子)
↓ vSDK_Pcb_GetBoard
Board (板子数据:包含所有层)
↓ vSDK_Board_GetLayerByName("TOP")
Layer (TOP层:包含所有对象)
2.3 TOP 层校验
不仅按名称找,还校验两个属性确保是正面信号层(任务书第 3 条):
| 属性 | 值 | 含义 |
| LayerSide | 1 | 正面(顶层) |
| LayerType | 0 | SG(信号层) |
三、算法全流程(核心,答辩重头戏)
3.1 整体流程图(背熟这条)
DEMO2.tgz
│ (DFX MetaLab 读入)
▼
Board 上下文
│
▼ 按"TOP" + Side=1 + Type=0 定位
TOP 层
│
▼ 遍历 9410 个 TOP 对象,筛线段(类型=5) ← 步骤1
筛得 5331 条 LineRecord
│
▼ ① 每条线外扩 0.1mm (CreateShapeByLine) ← 步骤2
▼ ② 按 NetID 分组进 ShapeConter ← 步骤3
492 个 ShapeConter 容器
│
▼ ① AABB 粗筛 (4次比较/对) ← 步骤4
▼ ② GetTouch + GetDistance 精算 ← 步骤5
5400 对违规 (QHash 去重) ← 步骤6
│
▼ 填入表格 + 点击跳转 (GotoResult)
界面展示
3.2 步骤详解
步骤 1:遍历 TOP 层,筛出所有线段
for (objectIndex = 0; objectIndex < 总数; objectIndex++) {
GetLayerObjectType(..., objectIndex) → 用0基索引判断类型
if (类型 == 5 /*Line*/) {
GetLayerObjectInfosEx(...) → 读起点/终点/符号尺寸/线头名称
GetLayerObjectNetID(..., objectIndex+1) → 用1基ObjectID读NetID
存入 LineRecord 容器
}
}
结果:5331 条线段进入内存。
步骤 2:每条线段外扩 0.1mm
expandedWidth = lineWidth + 0.2;
expandedLength = isRectangle ? capLength + 0.2 : expandedWidth;
CreateShapeByLine(原起点, 原终点, expandedLength, expandedWidth, isRectangle, ...)
关键接口约定:圆头线段的 LineLength 必须与 LineWidth 取相同值(vSDK.h 第879行);矩形线头时 LineLength 表示线头长度。当前 SDK 曾返回状态码3但同时给出有效 Shape,所以程序以“返回值>0 且指针非空”判断可用,并严格按头文件传参。
数据覆盖边界:DEMO2 的 5331 条 TOP 线段全部使用圆形 aperture,因此本赛题数据实际走圆头分支;矩形分支已实现,但不能声称已被 DEMO2 实测覆盖。
步骤 3:按网络分组进 ShapeConter 容器
ShapeConter 是 vSDK 提供的几何容器,专门用来批量计算。按 NetID 分组:
NetID=1 的所有线段 → ShapeConter_1
NetID=2 的所有线段 → ShapeConter_2
...
共 492 个容器
步骤 4:AABB 粗筛(性能关键)
问题:5331 条线中共有 13,966,489 对跨网络线段组合。若不做空间筛选,492 个网络容器两两组合为 C(492,2)=120,786 对,而相交与距离各调用一次,最多需要 241,572 次精确 SDK 调用。
解决:先对 13,966,489 对跨网络线段做轻量 AABB(Axis-Aligned Bounding Box,轴对齐包围盒)比较。判断两个包围盒是否分离只需要 4 项边界比较;只要某条线段对的扩展包围盒可能接近,才把对应网络对加入候选集合:
if (A.maxX < B.minX || B.maxX < A.minX ||
A.maxY < B.minY || B.maxY < A.minY) {
// 不可能相交,跳过
}
效果:最终得到 1975 个候选网络对,执行 1975×2=3950 次精确 SDK 调用;相比无筛选的 241,572 次,减少 237,622 次,约减少 98.36%。13,966,489 是轻量线段对比较次数,不是 GetDistance 调用次数。
为什么这次 AABB 粗筛不会漏掉真实违规?
对 DEMO2 实际覆盖的圆头线段,外扩后胶囊形相对中心线的半径为 lineWidth/2+0.1。
距离判定还要求小于 0.1mm,所以代码把每条中心线 AABB 再额外分担 0.05mm,总边界为
lineWidth/2+0.1+0.05。两个图形各承担0.05mm,合计覆盖0.1mm距离阈值。
如果这两个保守AABB仍然分离,则外扩图形在至少一个坐标轴方向上的间隔不小于0.1mm,不可能满足本题违规条件。
因此粗筛允许保留假候选,但对DEMO2的圆头数据不会漏掉阈值内候选,最后仍由vSDK精算确认。
适用边界:DEMO2的5331条TOP线段全部为圆形aperture。矩形线头的图形创建分支虽然已实现,但若未来出现线头长度大于线宽的矩形样本,
为获得通用的不漏检保证,粗筛边界应保守地基于 max(lineWidth,capLength) 计算。答辩时不能把本数据集结论无条件推广到任意矩形线头。
步骤 5:精算(GetTouch + GetDistance)
// 5a. GetTouch:显式取得外扩后相交的图形对
// 5b. GetDistance(0.0, 0.1):取得外扩后最小距离位于 [0.0,0.1) 的图形对
// 5c. 将两类结果按无序 ObjectID 对去重合并
严谨口径:任务书要求“相交或最小距离小于 0.1mm”,因此程序保留两次接口调用并取去重并集。
在 DEMO2 本次实测中,GetTouch 的 3873 对全部包含在 GetDistance(0,0.1) 返回的 5400 对中,故并集仍为 5400;这是本数据集的实测结果,不应脱离接口语义泛化。
步骤 6:去重 + 输出
用 PairKey = (较小id << 32) | 较大id 组成 64 位无序对键,存入 QHash 去重。
最终输出 5400 对唯一违规,其中外扩后相交 3873 对,不相交但外扩后最小距离小于 0.1mm 的 1527 对。
四、函数是自己写的还是调 SDK 的(必问)
核心原则(背熟):vSDK 提供底层几何能力(读数据、创建图形、计算相交与距离),
提交工程中的 logic 层负责把这些能力组织成完整流程:筛选、外扩、按网络分组、AABB 粗筛、结果去重和审计。
答辩时既不要把几何内核说成自己重写,也不要把工程工作缩减成“只调了一个接口”;应按代码逐项说明实现边界。
4.1 提交代码中实现的工程逻辑(答辩要讲清)
| 我的函数 | 作用 | 答辩要点 |
logic::RunDetection | 主编排函数——决定先做什么后做什么 | 整个流水线的"总指挥" |
logic::ReadTopLines | 读 TOP 层筛线段 | 只保留 Line 类型,校验坐标并处理0基/1基对象编号 |
logic::BuildExpandedNetContainers | 外扩 + 分组 | 按 NetID 建立 492 个 ShapeConter 容器 |
logic::CollectResultPairs | 收集结果 | 遍历 SDK 结果并存入 QHash 无序对集合 |
logic::GotoResult | 跳转定位 | 按任务要求跳到 ObjectID 较小图形的中心坐标 |
4.2 关键工程设计(应能现场解释)
| 设计点 | 需要讲明的内容 |
| AABB 粗筛 | SDK 不负责候选筛选;代码先进行线段包围盒比较,再把命中提升为候选网络对 |
| PairKey 去重 | 将两个 ObjectID 排序后编码为 64 位键,避免正反顺序和两类接口重复计数 |
| ObjectID 1基/0基处理 | 类型遍历用0基索引,NetID与界面对象编号用1基ObjectID |
| 返回码与指针双校验 | 当前SDK的正返回码不只1;必须同时验证返回值>0和Shape指针非空 |
| 资源释放路径 | 正常结束、Build失败和Collect失败均释放ShapeConter,临时Shape加入容器后立即销毁 |
4.3 调 vSDK 库的(诚实承认)
SDK 一共调了 27 个不同的接口,分四类:
| 类别 | 代表函数 | 关键诚实点 |
| 数据读取 | GetLayerObjectType / GetLayerObjectInfosEx / GetLayerObjectNetID | 我决定调用顺序与字段选择 |
| 几何运算 | CreateShapeByLine / GetTouch / GetDistance | ⚠️ 几何运算本身是 SDK 做的,我提供参数 |
| 资源管理 | Create/Delete_ShapeConter 等 | 我设计释放路径 |
| 界面控制 | Display_SetPartDisplay / Display_Goto | 按任务书第 15 条调用 |
4.4 从DFX菜单到检测结果的完整调用链
DFX MetaLab“用户工具”菜单
↓ 调用导出的 vSDK_Developer(dllpath,parent,pointer)
创建 Interface 非模态窗口并绑定按钮/表格信号
↓ 用户点击 Run
Interface::RunDetection
↓ 清空旧表格、禁用按钮、显示等待光标
logic::RunDetection
├─ RefreshContext:重新取得当前 Job/Pcb/Board
├─ ReadTopLines:定位并读取 TOP 线段
├─ BuildExpandedNetContainers:外扩并按 NetID 分组
├─ AABB粗筛:生成候选网络对
├─ CollectResultPairs:GetTouch/GetDistance并取并集
└─ 生成排序后的 DetectionResult
↓
Interface::FillTable 显示5400行
↓ 点击表格行
logic::GotoResult → vSDK_Display_Goto
4.5 核心数据结构与复杂度
| 数据结构 | 作用 |
LineRecord | 保存ObjectID、NetID、起终点、线宽、线头长度和线头类型 |
QVector<LineRecord> | 顺序保存5331条TOP线段 |
QHash<int,LineRecord> | 按ObjectID快速回查线段和中心坐标 |
QHash<int,void*> | NetID到ShapeConter的映射,本数据集共492个 |
QHash<quint64,bool> | 保存候选网络无序对和最终违规ObjectID无序对 |
QVector<DetectionResult> | 向UI提供最终表格数据 |
复杂度口径:当前粗筛仍遍历跨网络线段对,理论时间复杂度为 O(L²),本数据集实际完成13,966,489次轻量AABB比较;
精算只处理1975个候选网络对。空间主要用于线段记录、492个网络容器、候选网络对和5400个结果对。若数据规模继续增长,可使用均匀网格、R树或扫描线替代全线段两两粗筛。
4.6 异常处理、资源生命周期与界面线程
上下文刷新:每次点击Run都重新执行 RefreshContext,获取当前Job、Pcb和Board,避免继续使用旧工程上下文。
完整性检查:没有活动Job、找不到TOP、TOP属性不符、对象读取不完整、vSdk.dll或动态接口缺失、几何计算失败时均返回明确错误,由UI弹窗显示。
资源释放:临时Shape加入容器后立即销毁;每个计算结果列表使用后删除;ShapeConter在正常结束、Build失败和Collect失败路径中统一释放。
当前线程模型:检测同步运行在UI线程中。Run期间禁用按钮并显示等待光标,防止重复触发。DEMO2约1.55秒可以接受;若处理更大数据,应迁移到工作线程并通过信号槽更新进度。
五、audit 日志与 0基/1基(关键故事)
5.1 audit 日志是什么
audit = "体检报告"。一次检测跑完后,把所有关键数字记录下来,打印到 Qt Creator 的"应用程序输出"面板。
struct DetectionAudit {
int lineCount; // 线段总数
int networkCount; // 网络总数
qint64 crossNetworkPairCount; // 跨网络对数
int touchPairCount; // 相交命中数
int distancePairCount; // 距离命中数
int uniqueViolationCount; // 最终违规数
};
实际输出:Distance detection audit: lines=5331 nets=492 crossNetPairs=13966489 touchHits=3873 distanceHits=5400 uniqueViolations=5400
5.2 0基 / 1基是什么
对象A 对象B 对象C
0基序号(index): 0 1 2 ← C/C++数组习惯
1基ID(ID): 1 2 3 ← 日常习惯(第1个、第2个)
↑差1 ↑差1 ↑差1
同一个对象,在两套体系下数字差 1
5.3 vSDK 的坑:两套体系混用
| 接口函数 | 参数名 | 用几基 |
GetLayerObjectType(..., int IObjectIndex) | IObjectIndex | 0基(Index 这个词) |
GetLayerObjectNetID(..., int kLayerObjectId) | kLayerObjectId | 1基(Id 这个词) |
5.4 我踩的坑(错误代码)
line.objectId = objectIndex; // ❌ 错!存成了 0基
GetLayerObjectNetID(board, layer, line.objectId, line.netId); // ❌ 错!要 1基
结果:每条线查到的是"前一个对象"的网络,所有网络归属全错位。
本该同网络的线被分到不同网络 → 产生虚假的"跨网络违规"。
网络数虚增:本该 492,变成 515。
5.5 怎么发现的——audit 立功
| 阶段 | audit 输出 | 判断 |
| 第一次(bug版) | nets=515 | ❌ 不对!应为 492 |
| 改成 1基后 | nets=492 | ✅ 对上了!bug 定位成功 |
5.6 正确的修复
line.objectId = objectIndex + 1; // ✅ 改成 1基!
GetLayerObjectNetID(board, layer, line.objectId, line.netId); // ✅
这个 1基的 objectId 贯穿全程:查 NetID、AddShape 编号、表格显示、GotoResult 比较,都用它,保证一致。
"开发过程中我特别注重可观测性。我在检测流程里埋了 audit 日志,记录线段数、网络数、相交命中、距离命中等中间数字。
第一次跑完,audit 显示网络数为 515。结合接口参数语义、对象抽样对照以及修复后的逐线 NetID 统计,我判断网络归属存在系统性错位。需要注意:ODB++ 全局 netlist 的 527 个定义网络与 TOP 线段实际引用的 NetID 不是同一统计口径,不能直接用 527 推导 492。
回头查代码,定位到 GetLayerObjectNetID。我发现 vSDK 头文件里,有的参数叫 IObjectIndex(0基序号),有的叫 kLayerObjectId(1基ID),两套标识混用。
改成 1基后,网络数精确降到 492,违规数从 6093 修正到 5400。这个经历让我深刻体会到:工程问题不能靠猜,要用数据说话。audit 日志就是我用的数据。"
六、正确性如何验证(含 ODB++ 独立解析)
6.1 原始几何数据复核:TOP 线段数为 5331
将 DEMO2.tgz 解压后直接解析 ODB++ 的 TOP/features 文件,不经过插件的对象读取流程,重新统计原始图形记录:
ODB++ 原始文件: 5331 条线段
代码 audit: 5331 条线段
→ 数量完全一致,并对首条线段坐标进行抽样核对;这为读取完整性提供了强证据,但不把单一计数夸大为“所有字段 100% 正确”。
首条线段坐标也对得上:
ODB++ 原始:(96.361, 3.543)→(97.015, 2.889)
代码读出: (96.3607, 3.5433)→(97.015, 2.88899)(精度差异)
6.2 网络数 492 vs 527(口径不同,都合理)
| 来源 | 网络数 | 口径 |
| 代码 audit | 492 | TOP 层有走线的网络数 |
| ODB++ netlist 文件 | 527 | 全板定义的网络数(含其他层、过孔) |
527 是 ODB++ 全局 netlist 中定义的网络编号数;492 是对 vSDK 返回的 5331 条 TOP 线段 NetID 去重后的数量。两者统计对象不同,因此不能用 527−492=35 直接断言这些网络只存在于哪一层。后续算法应使用与 TOP 线段一一对应的 492 个有效 NetID 分组。
6.3 5400 组结果的逐对复核
插件审计:5331 条线段、492 个有效网络、13,966,489 对跨网络线段组合;GetTouch 命中 3873 对,GetDistance 命中 5400 对,最终唯一违规 5400 对。
独立几何复算:对同一批线段记录分别使用解析几何算法和 Shapely/GEOS 进行逐 ObjectID 对复算,两种方法均得到 3873 对相交、1527 对距离不足、合计 5400 对,且 ObjectID 对集合完全一致。集合 SHA-256 为:
b79a5ad42d3653ea94405fe7cface580566def508ef2bd335aca9d66f4d9fd1
证据边界:ODB++ 原始 features 独立证明几何记录数量和坐标;每条 TOP 线段对应的 NetID 采用 DFX MetaLab/vSDK 导入模型返回值。不能宣称仅靠全局 cadnet/netlist 文件就直接推出 492。
6.4 答辩时的验证顺序
先展示插件 audit 的六个关键数字,再说明原始 ODB++ 对 9410 个 TOP 对象和 5331 条 L 记录的复核,最后说明解析几何与 Shapely/GEOS 的逐对结果集合一致。验证脚本属于开发期自查材料,不是任务书要求的提交文件;现场以可复现的统计口径和结果集合为主。
七、答辩话术集锦(按评委提问分类)
🎯 开场自述(30 秒)
"我做的是一个 PCB 走线间距自动检测工具。PCB 顶层有 5331 条走线,分属 492 个网络。
按任务书要求,程序先将每条线的物理外轮廓向外扩0.1mm,再检测不同网络外扩图形是否相交或最小距离小于0.1mm,最终得到5400对唯一违规。
算法分六步:先用 vSDK 读入 TOP 层并筛出线段;再按任务书将每条线的外轮廓向外扩 0.1mm;
随后按 NetID 分组,用 AABB 对跨网络线段进行粗筛,再对候选网络容器调用 SDK 完成相交和距离精算,最后按 ObjectID 无序对去重。
性能上,13,966,489 次轻量线段对筛选最终只产生 1975 个候选网络对,精确 SDK 调用从理论上的 241,572 次降至 3950 次,实测约 1.55 秒;
正确性上,原始 ODB++ 复核得到 5331 条 TOP 线段,解析几何与 Shapely/GEOS 逐对复算均得到相同的 5400 组 ObjectID 对。"
📌 Q1:这些算法都是你自己写的吗?
整体算法的设计和编排是我自己写的,但底层的几何运算调用了 vSDK 接口。
具体来说,AABB 粗筛、网络分组策略、PairKey 去重、资源释放路径设计、ObjectID 基址处理这些都是我写的。
而实际的相交判断和距离计算调用了 SDK 的 GetTouch 和 GetDistance,
这两个是 vSDK 提供的核心几何引擎,我没必要也不可能自己重写一遍工业级的几何运算库。
我的工作是把它们正确地组织起来,解决业务问题。
📌 Q2:那你核心贡献是什么?
我的核心贡献有三个:
第一,流程设计。 SDK 给的是零件,我把它们组装成一条完整的检测流水线——从读数据、外扩、分组、粗筛、精算到输出。
第二,性能优化。 13,966,489 是跨网络线段对的轻量 AABB 比较规模;492 个网络容器无筛选时需做 120,786 个网络对、241,572 次精确接口调用。AABB 将其压缩为 1975 个候选网络对、3950 次精确调用,实测约 1.55 秒。由于没有保存未优化版本的同机基准,答辩时不声称“从分钟级降到秒级”。
第三,bug 定位与正确性保障。 我通过 audit 日志独立定位了 ObjectID 1基/0基的错误(网络数从 515 修正到 492),
以及 SDK 返回码 3 的容错处理。这些问题需要结合vSDK头文件、培训材料、参数命名和实际运行结果交叉确认,不能只凭单一接口返回值判断。
📌 Q3:为什么 ObjectID 用 1基,而遍历用 0基?
SDK 里有两套对象标识,我从头文件参数命名区分出来的:
GetLayerObjectType 参数叫 IObjectIndex(0基序号)
GetLayerObjectNetID 参数叫 kLayerObjectId(1基ID)
所以我循环用 0基 objectIndex 调用 GetLayerObjectType,
但存进 LineRecord 的 objectId 用 objectIndex+1。
这个 1基的值贯穿全程:查 NetID、AddShape 编号、表格显示、跳转比较都用它,保证一致。
追问:你怎么发现的?
通过 audit 日志发现异常:改之前按线段 NetID 去重得到 515。随后核对头文件参数名和对象编号语义,并对样本对象逐项检查,确认 GetLayerObjectType 使用 0 基索引,而 GetLayerObjectNetID 使用 1 基 ObjectID。
改成 objectIndex+1 后,网络数稳定为 492,违规数从 6093 修正为 5400。这里不把全局 netlist 的 527 当作直接判据。
📌 Q4:为什么圆头线段的 LineLength 要等于 LineWidth?
这是 vSDK 的硬性约定。头文件 vSDK.h 第 879 行注释明确写:
IsRectangle:false 为圆头线段,LineLength 与 LineWidth 赋相同值。
几何含义:圆头线段是一个胶囊形(中心线 + 两端半圆),SDK 用 LineWidth 同时作为宽度和两端半圆的直径。
LineLength 参数只在矩形线头时才用。所以圆头时两者必须相等。
头文件明确要求圆头线段的 LineLength 与 LineWidth 取相同值。调试中曾观察到返回码 3;在当前 SDK 版本中,只要返回值大于 0 且 Shape 指针有效,该次创建仍可使用,因此最终代码同时检查返回值和指针,并严格按头文件约定传参,不把返回码 3 简单描述为“硬失败”。
📌 Q5:为什么要做 AABB 粗筛?直接两两算不行吗?
性能考虑。5331 条线产生 13,966,489 对跨网络线段组合,程序只在这一层做代价很低的 AABB 比较,并将命中的线段对提升为候选网络对。
如果完全不筛选,492 个网络容器两两组合为 120,786 对;GetTouch 与 GetDistance 各调用一次,共 241,572 次精确 SDK 调用。AABB 最终只保留 1975 个候选网络对,即 3950 次精确调用,减少 237,622 次(约 98.36%)。
注意:crossNetPairs=13966489 是轻量线段对比较数,不是 GetDistance 调用数。
AABB 是计算几何里最经典的 broad phase(粗筛)技术,代价极低(只需 4 次坐标比较),
能高效剔除绝大多数不相交的组合。这借鉴了游戏引擎和物理引擎里的碰撞检测思路。
📌 Q6:5400 这个数字怎么验证是对的?
我采用了三层证据链。
第一层,原始数据:直接解析 ODB++ TOP/features,得到 9410 个对象、其中 5331 条 L 线段,并抽样核对坐标;这与插件读取结果一致。
第二层,网络口径:492 是 5331 条 TOP 线段通过 vSDK 得到的 NetID 去重数;527 是全局 cadnet/netlist 的定义网络数。二者统计对象不同,不能用全局文件直接证明 492。
第三层,逐对几何:将同一批线段记录分别交给解析几何算法与 Shapely/GEOS 复算,两种方法都得到 3873 对外扩后相交、1527 对距离不足,合计 5400 对,而且 ObjectID 对集合完全一致。插件 audit 也为 5400。
因此我的结论不是“凭感觉相信 5400”,而是运行结果、原始数据复核和两套独立几何计算相互印证。
📌 Q7:为什么用 GetDistance 而不是 GetTouch?
两个都用,因为任务书明确要求判断“相交或最小距离小于 0.1mm”。
GetTouch 显式取得相交对;GetDistance(0,0.1) 取得距离位于 [0,0.1) 的图形对;最后按 ObjectID 无序对取并集。
在 DEMO2 的实测结果中,3873 个 GetTouch 对全部包含在 5400 个 GetDistance 对中,所以去重并集仍为 5400,其中距离不足但不相交的有 1527 对。保留 GetTouch 既对应任务书的相交条件,也便于 audit 分类。
📌 Q8:外扩后几何怎么变?圆头和方头有区别吗?
任务书要求:线段起点、终点坐标不变,外轮廓向外扩 0.1mm。
圆头线段:外扩后仍为胶囊形,线宽增加0.2mm,两端圆头半径各增加0.1mm。DEMO2 的5331条TOP线段均为圆形 aperture,实际全部走这一分支。
矩形线头:宽度与线头长度均增加0.2mm。该分支已在代码中实现,但DEMO2没有矩形线头样本,所以答辩时应明确“已实现但未被本数据集覆盖”。
代码实现:调用 CreateShapeByLine,起止坐标保持原值;宽度统一改为 lineWidth+0.2。圆头线段按 SDK 约定令扩展后的 LineLength=expandedWidth;矩形线头则令 LineLength=capLength+0.2。因此两种线头都按各自物理轮廓向外扩 0.1mm。
📌 Q9:代码量多少?
logic.cpp 542 行 ← 核心算法
logic.h 88 行
interface.cpp 269 行 ← UI交互
interface.h 72 行
interface.ui 149 行(XML)
任务书要求非界面相关功能写在 logic 类。工程采用
逻辑层与界面层分离:logic 负责读取、外扩、分组、检测和跳转数据;Interface 负责按钮、表格和消息提示。这里更准确的说法是“职责分层”,不夸大为严格完整的 MVC 架构。
📌 Q10:最尖锐——这代码是不是 AI 写的?
如果评委问到,我会依据比赛规则如实说明辅助工具的使用情况,不回避也不夸大个人完成范围。
更重要的是,我能够现场说明并验证全部提交内容:包括 ObjectID 的 0基/1基差异、圆头和矩形线头的外扩参数、492 与 527 的统计口径、AABB 为什么不会漏掉阈值内候选、5400 对结果如何逐对复核,以及如何在 Qt Creator 中重新构建并在 DFX MetaLab 中演示。
答辩重点不是一句“谁写的”,而是提交者是否真正理解、能够复现、验证和维护自己的提交成果。
📌 Q11:GetDistance 不是你写的,你只是调了一下?
GetDistance 这个函数实现确实是 SDK 的,但我做的远不止"调一下":
第一,调用参数是我设计的——我传 DMin=0、DMax=0.1,这来自我对任务书"小于 0.1mm"的理解(开区间)。
第二,调用对象是我组织的——GetDistance 接收两个 ShapeConter 容器,
这 492 个容器是我按网络分组、逐条外扩、塞进 ShapeConter 的。
第三,调用时机经过空间筛选——13,966,489 对跨网络线段只做轻量 AABB 比较,由此得到 1975 个候选网络对。随后对每个候选网络对各调用一次 GetTouch 和 GetDistance,共 3950 次;不是对 1400 万条线段对逐一调用 SDK。
第四,结果是我处理的——SDK 返回的原始结果列表,我遍历、用 PairKey 去重、组装成 DetectionResult、填进表格。
所以 GetDistance 只是我流水线里的一环,前面的数据准备和后面的结果处理都是我写的。
📌 Q12:性能怎么样?还能优化吗?
当前在本机一次记录中的检测耗时约为 1.552 秒。主要优化点:
✅ 已做:AABB 粗筛,精确 SDK 调用从理论上的 241,572 次降为 3950 次,减少 237,622 次
✅ 已做:按网络分组,同网络线段不进入违规检测
可做:网格分箱(Grid Partitioning)替代两两遍历,降到 O(n·k)
可做:多线程并行(不同网络对独立计算)
📌 Q13:DLL 是怎么被 DFX MetaLab 加载的?
入口函数:vSDK_Developer(dllpath, parent, pointer)——这是 extern "C" 导出的 C 函数,
DFX MetaLab 通过 QLibrary 加载 DLL 后调用它。函数作用:创建 Interface 窗口实例,设置位置、窗口属性、显示。
另外通过遍历顶层窗口的 objectName 判断窗口是否已存在,避免重复打开多个 Distance Detection 窗口。
这是 Qt 非模态窗口单例的常用做法。
📌 Q14:点击运行时还做了什么?
任务书第 15 条要求点击运行时还要做三件事,我在
RunDetection 开头都做了:
vSDK_Display_SetPartDisplay(false) // 关闭元件显示
vSDK_Display_SetSelectMode(4) // 选择模式→对象模式
vSDK_Display_SetObjectMode(3) // 绘制模式→轮廓模式
📌 Q15:表格坐标格式怎么保证的?
任务书第 12 条要求:坐标为图形中心点,六位小数,圆括号包裹,逗号分隔,单位 mm 不显示。
我的 FormatPosition 函数:"(%1,%2)" + 'f', 6 位小数。
中心点取 ((startX+endX)/2, (startY+endY)/2),外扩不改变中心,所以中心点稳定。
📌 Q16:AABB粗筛为什么不会漏检?
对DEMO2全部为圆头的线段,外扩图形半径是 lineWidth/2+0.1;距离阈值0.1mm由两个图形各承担0.05mm,所以代码使用
lineWidth/2+0.1+0.05 扩展中心线AABB。若两个保守AABB仍分离,外扩图形不可能相交或小于0.1mm。
粗筛可以产生假候选,但本数据集不会漏掉真实候选。该证明以DEMO2的圆形aperture为边界;对任意矩形线头不能无条件照搬。
📌 Q17:约1.552秒是怎么测出来的?
这是本机一次运行记录中的观察值,用于说明当前数据规模下大约是秒级响应,不是严格性能基准。
当前提交代码没有用 QElapsedTimer 输出正式统计,所以答辩时不把1.552秒说成跨机器稳定值。
如果做正式基准,应在相同硬件和Release构建下预热,多次运行并报告平均值、最小值和波动范围。
📌 Q18:没有导入DEMO2或SDK接口失败怎么办?
每次Run先刷新当前Job、Pcb和Board;没有活动Job、TOP不存在或属性错误时立即停止。
对象读取不完整、vSdk.dll或动态接口缺失、Shape创建、容器加入、相交/距离计算失败都会返回具体错误字符串,Interface通过消息框提示,不会把部分结果当成完整结果展示。
失败路径同时释放已经创建的ShapeConter和结果列表。
📌 Q19:为什么每次表格顺序稳定?
QHash本身不保证遍历顺序,所以代码不会直接按QHash顺序输出。
候选网络PairKey和最终违规PairKey都会先取出为列表并执行 std::sort,再按升序组装DetectionResult。
PairKey内部先将两个ID排序,因此同一对图形无论SDK以哪个方向返回,键和值及最终表格顺序都保持确定。
📌 Q20:工程怎么构建?为什么最终只提交五个开发文件?
开发环境按任务书使用Qt Creator、Qt 5.15.2和MinGW 64位,由官方 emptyExampleWithUI 模板工程生成动态链接库,再由DFX MetaLab用户工具加载。
最终提交的五个文件是 logic.h/.cpp、interface.h/.cpp 和 interface.ui:logic保存非界面算法,interface与ui保存交互和布局。
模板入口、vSDK封装、资源和构建配置属于官方初始化工程及运行环境依赖,不是本题要求重复提交的自定义功能源码。现场应能从模板工程重新构建并演示绑定。
📌 Q21:两位参赛者如何分工?
这一题必须按真实工作填写,不要临场虚构。答辩前补全并相互确认:
高旭辉:____________________________
王宇轩:____________________________
共同完成:需求核对、DEMO2现场运行、5400结果验证和答辩复盘等________________。
即使有主要分工,两个人都应能解释完整主链:5331条线→492个网络→13,966,489对跨网络线段→1975个候选网络对→3950次精算→5400组唯一违规。
八、速记卡(打印带去答辩)
🔑 核心数字(背熟)
| 指标 | 数值 | 说明 |
| 线段数 | 5331 | TOP 层线段总数(ODB++ 独立验证一致 ✅) |
| 有效网络数 | 492 | 5331 条 TOP 线段的 vSDK NetID 去重数;全局 netlist 定义数为 527 |
| 违规数 | 5400 | 外扩后相交 3873 对 + 不相交但距离<0.1mm 的 1527 对 |
| 阈值 | 0.1mm | 对外扩后图形采用严格小于;接口区间为 [0.0,0.1) |
| 外扩值 | 0.1mm | 中心线不变;宽度增加 0.2mm,线头轮廓同步外扩 |
| 跨网络线段对 | 13,966,489 | 轻量 AABB 比较规模,不是 SDK 调用次数 |
| 候选网络对 | 1975 | 粗筛后进入精算的网络对 |
| 精确 SDK 调用 | 3950 | 1975 对 × GetTouch/GetDistance 各一次;减少 237,622 次 |
| 实测耗时 | 约1.552秒 | 一次 Qt 应用程序输出记录,不同机器会有波动 |
🔑 三个关键决策(必答)
┌──────────────────────────────────────────────────────────┐
│ 1. ObjectID 1基 vs 0基 │
│ - GetLayerObjectType 要 0基 (IObjectIndex) │
│ - GetLayerObjectNetID 要 1基 (kLayerObjectId) │
│ - 破案关键: audit 显示 nets=515(应492) → 定位到基址错 │
├──────────────────────────────────────────────────────────┤
│ 2. 圆头线段 LineLength = LineWidth │
│ - vSDK.h:879 硬性约定 │
│ - 状态码与Shape指针双校验,不能把3简单当硬失败 │
├──────────────────────────────────────────────────────────┤
│ 3. AABB 粗筛 + SDK 精算 │
│ - 粗筛: 4 次坐标比较剔除空间不相交对 │
│ - 1975个候选网络对,共3950次精确SDK调用 │
│ - 两类结果按ObjectID无序对取并集;本数据集并集为5400 │
└──────────────────────────────────────────────────────────┘
🔑 分工清单(自己写的 vs 调库的)
┌─────────────────────────────────────────────────────┐
│ 工程中实现的逻辑 │ vSDK 提供的底层能力 │
├──────────────────────────┼──────────────────────────┤
│ ✅ 整体流程编排 │ 🔧 几何运算内核 │
│ (RunDetection) │ (GetTouch/GetDistance)│
│ ✅ AABB 粗筛算法 │ 🔧 图形创建 │
│ (4次比较判包围盒) │ (CreateShapeByLine) │
│ ✅ 网络分组策略 │ 🔧 数据读取 │
│ (按NetID分492容器) │ (GetLayerObject*) │
│ ✅ PairKey 去重 │ 🔧 资源管理 │
│ (位运算64位键) │ (Create/Delete) │
│ ✅ ObjectID 基址处理 │ 🔧 界面控制 │
│ (1基 vs 0基) │ (Display_*) │
│ ✅ 返回码容错(=3也算成功)│ │
│ ✅ 资源释放三路径 │ │
└──────────────────────────┴──────────────────────────┘
🚫 绝对不能说的两句话
❌ "算法都是 SDK 做的,我就是调了一下" —— 这等于自认没贡献
❌ "全部都是我自己写的,SDK 没参与" —— 一查代码就穿帮
正确姿态:SDK 提供底层能力,我提供算法设计与工程组织。
在本题指定的DFX MetaLab与vSDK开发环境中,复用平台提供的几何内核并完成数据组织、候选筛选和结果管理,是合理的工程分工;关键是能否正确使用接口解决业务问题并验证结果。
🎬 现场演示脚本(约60—90秒)
| 步骤 | 操作 | 同时要说清楚 |
| 1 | 在DFX MetaLab导入DEMO2.tgz | 数据由平台负责读入,插件通过vSDK访问当前内存对象 |
| 2 | 打开“工具→用户工具→Distance Detection” | DLL入口创建非模态Interface窗口,并避免重复打开 |
| 3 | 指出初始Total=0、表格为空 | 窗口打开时不提前计算,符合任务书要求 |
| 4 | 点击Run | 先刷新上下文,关闭元件显示,切换对象选择与轮廓模式 |
| 5 | 展示Total=5400和六列表格 | 六列名称、中心坐标格式和ObjectID均符合任务书 |
| 6 | 展示Qt应用程序输出 | 依次读出5331、492、13,966,489、3873、5400、5400 |
| 7 | 点击任意一行结果 | 跳转目标取两个ObjectID较小者的中心坐标 |
| 8 | 回到流程图或核心代码 | 用“读取→外扩→分组→粗筛→精算→去重”收束演示 |
现场异常备用话术:如果临时环境、路径或DFX状态导致演示无法运行,不要反复乱点。先说明错误提示来自哪一层,再用保存的audit输出和验证证据解释预期结果;同时明确现场环境问题不等于算法结论。最好在答辩前用最终提交源码重新构建并完整走一遍上述流程。
⏱ 5 分钟时间分配建议
| 时间 | 内容 |
| 0:00–0:30 | 自我介绍 + 一句话定义 |
| 0:30–2:00 | 核心数据链 + 四模块讲解(用流程图) |
| 2:00–4:00 | 重点讲 Q3(ObjectID 1基/0基)+ Q5(AABB),最能体现你懂的两个决策 |
| 4:00–5:00 | 留给评委提问(把上面的 qa 答辩话术用上) |
🛡 防身护身符
被问到不确定的,别硬编。说"这个我当时主要关注了 XX,你说的这个点我可以后续深入研究"——比胡说强百倍。
诚实是最大的加分项。评委欣赏能说清楚"我没做什么"的人,远胜过"我什么都做了"的吹嘘。