refactor: 通用化 slide 识别和游离节点收集

- findSlides(): 不靠命名,靠结构识别(面积/children/已知模式)
- collectOrphans(): 用 slide 子树 id 集合判断,不靠名字前缀
- 先递归子节点,子节点有 slide 则父节点是容器
- 7月汇报(6 slide) 和 A2A(7 slide) 都正确识别
This commit is contained in:
李进
2026-07-24 16:36:51 +08:00
parent d079a4b4cc
commit a2c7f23107
+55 -15
View File
@@ -318,23 +318,58 @@ function convert(input) {
} }
return results; return results;
} }
// ===== 通用 slide 识别 =====
// 找到所有 slide 容器name 为 slide-N 或 page 的节点 // 收集 slide 容器子树的所有 id(用于游离节点判断
function findSlides(node) { function collectIds(node, set) {
if (!node || !node.name) return []; if (!node) return;
const results = []; if (node.id) set.add(node.id);
if (/^slide-?\d*$/.test(node.name) || node.name === 'page' || node.name.startsWith('slide ')) { if (node.children) for (const c of node.children) collectIds(c, set);
results.push(node);
} }
// 已知 slide 命名模式
const SLIDE_NAME_RE = /^(slide[- ]?|page)/i;
// 结构性节点(不应作为 slide 容器)
const STRUCTURAL_TAGS = new Set(['HTML', 'BODY', 'HEAD', 'CANVAS']);
function isSlideCandidate(node, canvasArea) {
if (!node || node.type !== 'FRAME') return false;
if (!node.rect) return false;
// 排除截图底图
if (node.layerGroup === 'comparison') return false;
// 排除结构性节点
if (STRUCTURAL_TAGS.has(node.tag)) return false;
// 必须有 children
if (!node.children || node.children.length === 0) return false;
// 已知命名模式
if (node.name && SLIDE_NAME_RE.test(node.name.toLowerCase())) return true;
// 结构特征:面积 > 50% canvas
const rw = node.rect.width ?? node.rect.w ?? 0;
const rh = node.rect.height ?? node.rect.h ?? 0;
const area = rw * rh;
if (canvasArea > 0 && area > canvasArea * 0.5) return true;
return false;
}
function findSlides(node, canvasArea) {
if (!node) return [];
const results = [];
// 先递归收集子节点中的 slide candidates
if (node.children && Array.isArray(node.children)) { if (node.children && Array.isArray(node.children)) {
for (const child of node.children) { for (const child of node.children) {
results.push(...findSlides(child)); results.push(...findSlides(child, canvasArea));
} }
} }
// 如果子节点中已有 slide candidates,当前节点是容器,不是 slide
if (results.length > 0) return results;
// 当前节点是 slide candidate
if (isSlideCandidate(node, canvasArea)) {
results.push(node);
}
return results; return results;
} }
const slideContainers = findSlides(inputRoot); const canvasArea = (canvas.width || 0) * (canvas.height || 0);
const slideContainers = findSlides(inputRoot, canvasArea);
console.log(`找到 ${slideContainers.length} 个 slide 容器`); console.log(`找到 ${slideContainers.length} 个 slide 容器`);
const slideGroups = slideContainers.map(s => ({ const slideGroups = slideContainers.map(s => ({
@@ -352,16 +387,21 @@ function convert(input) {
slideGroups.forEach(sg => console.log(` ${sg.name}: ${sg.children.length} 个元素`)); slideGroups.forEach(sg => console.log(` ${sg.name}: ${sg.children.length} 个元素`));
// ===== fallback: 收集不在 slide 子树内但坐标在 slide 范围内的节点 ===== // ===== 通用游离节点收集 =====
// 从根节点收集所有非 slide 节点 // 构建 slide 容器子树的 id 集合
const slideSubtreeIds = new Set();
for (const sc of slideContainers) {
collectIds(sc, slideSubtreeIds);
}
function collectOrphans(node) { function collectOrphans(node) {
if (!node || !node.rect) return []; if (!node || !node.rect) return [];
const results = []; const results = [];
const nr = node.rect; const nr = node.rect;
const rw = nr.width ?? nr.w; const rw = nr.width ?? nr.w;
const rh = nr.height ?? nr.h; const rh = nr.height ?? nr.h;
// 跳过 slide 容器本身和 raster // 用 id 集合判断:不在任何 slide 子树内的节点才是游离节点
if (!node.name?.startsWith('slide-') && !node.name?.startsWith('slide ') && node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') { if (!slideSubtreeIds.has(node.id) && node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') {
results.push({ results.push({
id: node.id, type: node.type, name: node.name, tag: node.tag, id: node.id, type: node.type, name: node.name, tag: node.tag,
rect: { x: nr.x, y: nr.y, w: rw, h: rh }, rect: { x: nr.x, y: nr.y, w: rw, h: rh },
@@ -370,8 +410,8 @@ function convert(input) {
}); });
} }
if (node.children) for (const c of node.children) { if (node.children) for (const c of node.children) {
// 跳过 slide 容器的子树(它们已经被 collectSlideChildren 处理了) // 跳过已在 slide 子树内的节点
if (c.name?.startsWith('slide-') || c.name?.startsWith('slide ') || c.name === 'page') continue; if (slideSubtreeIds.has(c.id)) continue;
results.push(...collectOrphans(c)); results.push(...collectOrphans(c));
} }
return results; return results;