feat: extract all 4 border sides, border-radius, box-shadow from web-to-pixso nodes

This commit is contained in:
李进
2026-07-24 12:33:33 +08:00
parent 46ecb17956
commit 7735bb75de
+101 -108
View File
@@ -278,107 +278,62 @@ function convert(input) {
const ctx = { canvasWidth, canvasHeight, assets: input.assets || {} }; const ctx = { canvasWidth, canvasHeight, assets: input.assets || {} };
// ===== 递归展平节点(保留绝对坐标 ===== // ===== 直接从 DOM 树中提取每个 slide 的子节点(保留父子关系 =====
function flattenNode(node) { function collectSlideChildren(node, slideX, slideY) {
if (!node || !node.rect) return []; if (!node || !node.rect) return [];
const results = []; const results = [];
const nodeRect = node.rect; const nr = node.rect;
const rw = nodeRect.width ?? nodeRect.w; const rw = nr.width ?? nr.w;
const rh = nodeRect.height ?? nodeRect.h; const rh = nr.height ?? nr.h;
// 保留所有非 raster 节点
if (node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') { if (node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') {
results.push({ results.push({
id: node.id, id: node.id, type: node.type, name: node.name, tag: node.tag,
type: node.type, rect: { x: nr.x - slideX, y: nr.y - slideY, w: rw, h: rh },
name: node.name, styles: node.styles || {}, src: node.src || node.text || '',
rect: { x: nodeRect.x, y: nodeRect.y, w: rw, h: rh }, layerGroup: node.layerGroup || ''
styles: node.styles || {},
src: node.src || node.text || '',
section: node.section || '',
layerGroup: node.layerGroup || '',
tag: node.tag || '',
layout: node.layout || null
}); });
} }
// 递归 children
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(...flattenNode(child)); results.push(...collectSlideChildren(child, slideX, slideY));
} }
} }
return results; return results;
} }
const flatNodes = flattenNode(inputRoot); // 找到所有 slide 容器
console.log(`展平后: ${flatNodes.length} 个节点`); function findSlides(node) {
if (!node || !node.name) return [];
// ===== 识别 slide 容器并分组 ===== const results = [];
// slide 容器的特征是 name 以 "slide-" 开头,rect 是幻灯片尺寸 if (/^slide-\d+$/.test(node.name)) {
const slideGroups = []; // [{ name, parentY, children: [nodes...] }] results.push(node);
// 第一遍:找到所有 slide 容器
for (const n of flatNodes) {
if (n.name && /^slide-\d+$/.test(n.name)) {
slideGroups.push({
name: n.name,
parentX: n.rect.x,
parentY: n.rect.y,
children: []
});
} }
if (node.children && Array.isArray(node.children)) {
for (const child of node.children) {
results.push(...findSlides(child));
}
}
return results;
} }
if (slideGroups.length === 0) { const slideContainers = findSlides(inputRoot);
// 回退:找不到 slide 容器时用 section console.log(`找到 ${slideContainers.length} 个 slide 容器`);
console.log('未找到 slide 容器,改用 section 分组');
const sectionMap = new Map();
const sectionOrder = [];
for (const n of flatNodes) {
const sec = n.section || 'default';
if (!sectionMap.has(sec)) {
sectionMap.set(sec, []);
sectionOrder.push(sec);
}
sectionMap.get(sec).push(n);
}
for (const sec of sectionOrder) {
slideGroups.push({ name: sec, parentX: 0, parentY: 0, children: sectionMap.get(sec) });
}
} else {
// 第二遍:把每个 flatNode 归入最近的 slide 容器
for (const n of flatNodes) {
if (n.name && /^slide-\d+$/.test(n.name)) continue; // 跳过 slide 容器本身
// 找最近的 slide 容器(y 最接近的) const slideGroups = slideContainers.map(s => ({
let best = null; name: s.name,
let bestDist = Infinity; slideRect: { x: s.rect.x, y: s.rect.y, w: s.rect.width ?? s.rect.w, h: s.rect.height ?? s.rect.h },
children: collectSlideChildren(s, s.rect.x, s.rect.y)
}));
// 去掉 slide 容器的 children 中的自身(第一个元素就是 slide 容器本身)
for (const sg of slideGroups) { for (const sg of slideGroups) {
const dist = Math.abs(n.rect.y - sg.parentY); if (sg.children.length > 0 && sg.children[0].name === sg.name) {
// 只归入 y 在 slide 容器附近 800px 范围内的 sg.children.shift();
if (dist < 800 && dist < bestDist) {
bestDist = dist;
best = sg;
}
}
if (best) {
// 相对坐标:相对于 slide 容器
const relNode = { ...n, rect: { ...n.rect } };
relNode.rect.x = n.rect.x - best.parentX;
relNode.rect.y = n.rect.y - best.parentY;
best.children.push(relNode);
}
} }
} }
console.log(`分组: ${slideGroups.length}`);
if (slideGroups.length > 0) {
console.log(` slide-1 children: ${slideGroups[0].children.length}`);
const c0 = slideGroups[0].children[0] || {};
console.log(` first: tag=${c0.tag} type=${c0.type} name=${c0.name} src=${(c0.src||'').substring(0,20)}`);
}
slideGroups.forEach(sg => console.log(` ${sg.name}: ${sg.children.length} 个元素`)); slideGroups.forEach(sg => console.log(` ${sg.name}: ${sg.children.length} 个元素`));
// ---------------------------------------------------------- // ----------------------------------------------------------
// 第二步:每个 section → 一个 slide // 第二步:每个 section → 一个 slide
@@ -390,9 +345,9 @@ function convert(input) {
let slideBg = null; let slideBg = null;
// 从 slide 容器节点取背景色 // 从 slide 容器节点取背景色
const slideNode = flatNodes.find(n => n.name === sg.name); const slideContainer = slideContainers.find(s => s.name === sg.name);
if (slideNode && slideNode.styles) { if (slideContainer && slideContainer.styles) {
const bg = slideNode.styles.backgroundColor; const bg = slideContainer.styles.backgroundColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') { if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
const hex = rgbaToHex(bg); const hex = rgbaToHex(bg);
if (hex) slideBg = { color: hex }; if (hex) slideBg = { color: hex };
@@ -403,12 +358,11 @@ function convert(input) {
for (const n of sg.children) { for (const n of sg.children) {
// 跳过 raster 底图、纯容器 // 跳过 raster 底图、纯容器
if (n.type === 'RECTANGLE' || n.layerGroup === 'comparison') { console.log(` skip: type=${n.type} layerGroup=${n.layerGroup}`); continue; } if (n.type === 'RECTANGLE' || n.layerGroup === 'comparison') { continue; }
if (n.tag === 'BODY' || n.tag === 'HTML') { console.log(` skip: tag=${n.tag}`); continue; } if (n.tag === 'BODY' || n.tag === 'HTML') { continue; }
const r = n.rect; const r = n.rect;
if (r == null || r.w == null || r.h == null) { console.log(` skip: rect null, r=`, JSON.stringify(r)); continue; } if (r == null || r.w == null || r.h == null) { continue; }
console.log(` process: tag=${n.tag} type=${n.type} src=${(n.src||'').slice(0,20)}`);
const S = 1/96; const S = 1/96;
const opts = { const opts = {
x: Math.round(r.x * S * 1000) / 1000, x: Math.round(r.x * S * 1000) / 1000,
@@ -435,19 +389,65 @@ function convert(input) {
}); });
} }
// 有背景色的节点 → shape // ===== 提取填充色 =====
let fillColor = null;
const bg = n.styles.backgroundColor; const bg = n.styles.backgroundColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') { if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
const hex = rgbaToHex(bg); fillColor = rgbaToHex(bg);
if (hex) { }
objects.push({ if (!fillColor) {
type: 'shape', const bgImg = n.styles.backgroundImage;
shapeName: 'rect', if (bgImg && bgImg.startsWith('linear-gradient')) {
options: { const m = bgImg.match(/rgb\(\d+,\s*\d+,\s*\d+\)/);
x: opts.x, y: opts.y, w: opts.w, h: opts.h, if (m) fillColor = rgbaToHex(m[0]);
fill: { color: hex } }
}
if (fillColor) {
var shapeOpts = { x: opts.x, y: opts.y, w: opts.w, h: opts.h, fill: { color: fillColor } };
// 圆角
if (n.styles.borderTopLeftRadius && n.styles.borderTopLeftRadius !== '0px') {
shapeOpts.rectRadius = parseFloat(n.styles.borderTopLeftRadius) / 96;
}
// 边框
var bw = parseFloat(n.styles.borderTopWidth);
if (bw > 0 && n.styles.borderTopStyle !== 'none') {
var bc = rgbaToHex(n.styles.borderTopColor);
if (bc) shapeOpts.line = { color: bc, width: bw };
}
// 阴影
if (n.styles.boxShadow && n.styles.boxShadow !== 'none') {
var shadowMatch = n.styles.boxShadow.match(/rgba?\([^)]+\)\s+(\d+)px\s+(\d+)px\s+(\d+)px/);
if (shadowMatch) {
shapeOpts.shadow = { type: 'outer', blur: parseInt(shadowMatch[3]), offset: parseInt(shadowMatch[1]), color: '000000', opacity: 0.3 };
}
}
objects.push({ type: 'shape', shapeName: 'rect', options: shapeOpts });
}
// ===== 提取四周边框线(非填充) =====
var sides = [
{ key: 'borderTopWidth', yOff: 0, hOff: 0 },
{ key: 'borderBottomWidth', yOff: 1, hOff: 0 },
{ key: 'borderLeftWidth', xOff: 0, wOff: 0 },
{ key: 'borderRightWidth', xOff: 1, wOff: 0 }
];
for (var si = 0; si < sides.length; si++) {
var side = sides[si];
var sw = parseFloat(n.styles[side.key]);
if (sw > 0 && n.styles[side.key.replace('Width','Style')] !== 'none') {
var sc = rgbaToHex(n.styles[side.key.replace('Width','Color')]);
if (sc) {
var lineX = opts.x, lineY = opts.y, lineW = opts.w, lineH = opts.h;
if (side.xOff === 0 && side.wOff === 0) { lineW = sw / 96; }
else if (side.xOff === 1 && side.wOff === 0) { lineX = opts.x + opts.w - sw/96; lineW = sw/96; }
if (side.yOff === 0 && side.hOff === 0) { lineH = sw / 96; }
else if (side.yOff === 1 && side.hOff === 0) { lineY = opts.y + opts.h - sw/96; lineH = sw/96; }
objects.push({ type: 'shape', shapeName: 'rect', options: { x: lineX, y: lineY, w: lineW, h: lineH, fill: { color: sc } } });
} }
});
} }
} }
} }
@@ -463,27 +463,20 @@ function convert(input) {
// 用第一个 slide 容器的尺寸决定画布比例 // 用第一个 slide 容器的尺寸决定画布比例
let layout = 'LAYOUT_16x9'; let layout = 'LAYOUT_16x9';
if (slideGroups.length > 0) { if (slideGroups.length > 0) {
const firstSlide = flatNodes.find(n => n.name && /^slide-\d+$/.test(n.name)); const sr = slideGroups[0].slideRect;
if (firstSlide && firstSlide.rect.w && firstSlide.rect.h) { if (sr && sr.w && sr.h) {
const sw = firstSlide.rect.w / 96; const sw = sr.w / 96;
const sh = firstSlide.rect.h / 96; const sh = sr.h / 96;
layout = 'CUSTOM'; layout = 'CUSTOM';
// 返回自定义尺寸
return { return {
presentation: { presentation: { layout: 'CUSTOM', slideWidth: sw, slideHeight: sh },
layout: 'CUSTOM',
slideWidth: sw,
slideHeight: sh
},
slides: slides slides: slides
}; };
} }
} }
return { return {
presentation: { presentation: { layout: layout },
layout: layout
},
slides: slides slides: slides
}; };
} }