feat: extract all 4 border sides, border-radius, box-shadow from web-to-pixso nodes
This commit is contained in:
+105
-112
@@ -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 [];
|
||||||
|
const results = [];
|
||||||
|
if (/^slide-\d+$/.test(node.name)) {
|
||||||
|
results.push(node);
|
||||||
|
}
|
||||||
|
if (node.children && Array.isArray(node.children)) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
results.push(...findSlides(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 识别 slide 容器并分组 =====
|
const slideContainers = findSlides(inputRoot);
|
||||||
// slide 容器的特征是 name 以 "slide-" 开头,rect 是幻灯片尺寸
|
console.log(`找到 ${slideContainers.length} 个 slide 容器`);
|
||||||
const slideGroups = []; // [{ name, parentY, children: [nodes...] }]
|
|
||||||
|
|
||||||
// 第一遍:找到所有 slide 容器
|
const slideGroups = slideContainers.map(s => ({
|
||||||
for (const n of flatNodes) {
|
name: s.name,
|
||||||
if (n.name && /^slide-\d+$/.test(n.name)) {
|
slideRect: { x: s.rect.x, y: s.rect.y, w: s.rect.width ?? s.rect.w, h: s.rect.height ?? s.rect.h },
|
||||||
slideGroups.push({
|
children: collectSlideChildren(s, s.rect.x, s.rect.y)
|
||||||
name: n.name,
|
}));
|
||||||
parentX: n.rect.x,
|
|
||||||
parentY: n.rect.y,
|
// 去掉 slide 容器的 children 中的自身(第一个元素就是 slide 容器本身)
|
||||||
children: []
|
for (const sg of slideGroups) {
|
||||||
});
|
if (sg.children.length > 0 && sg.children[0].name === sg.name) {
|
||||||
|
sg.children.shift();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slideGroups.length === 0) {
|
|
||||||
// 回退:找不到 slide 容器时用 section
|
|
||||||
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 最接近的)
|
|
||||||
let best = null;
|
|
||||||
let bestDist = Infinity;
|
|
||||||
for (const sg of slideGroups) {
|
|
||||||
const dist = Math.abs(n.rect.y - sg.parentY);
|
|
||||||
// 只归入 y 在 slide 容器附近 800px 范围内的
|
|
||||||
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user