feat: complete web-to-pixso JSON mapping - border sides, image assets, orphan nodes, slide container borders
This commit is contained in:
+146
-15
@@ -279,25 +279,38 @@ function convert(input) {
|
||||
const ctx = { canvasWidth, canvasHeight, assets: input.assets || {} };
|
||||
|
||||
// ===== 直接从 DOM 树中提取每个 slide 的子节点(保留父子关系) =====
|
||||
function collectSlideChildren(node, slideX, slideY) {
|
||||
function collectSlideChildren(node, slideX, slideY, parentW) {
|
||||
if (!node || !node.rect) return [];
|
||||
const results = [];
|
||||
const nr = node.rect;
|
||||
const rw = nr.width ?? nr.w;
|
||||
const rh = nr.height ?? nr.h;
|
||||
|
||||
// 保留所有非 raster 节点
|
||||
if (node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') {
|
||||
results.push({
|
||||
id: node.id, type: node.type, name: node.name, tag: node.tag,
|
||||
rect: { x: nr.x - slideX, y: nr.y - slideY, w: rw, h: rh },
|
||||
styles: node.styles || {}, src: node.src || node.text || '',
|
||||
layerGroup: node.layerGroup || ''
|
||||
layerGroup: node.layerGroup || '',
|
||||
parentW: parentW || rw
|
||||
});
|
||||
}
|
||||
|
||||
// 保留 raster 节点作为图片
|
||||
if (node.type === 'RECTANGLE' && node.layerGroup === 'comparison' && node.backgroundImages && node.backgroundImages.length > 0) {
|
||||
results.push({
|
||||
id: node.id, type: 'IMAGE', name: node.name, tag: 'RASTER',
|
||||
rect: { x: nr.x - slideX, y: nr.y - slideY, w: rw, h: rh },
|
||||
styles: node.styles || {}, src: '',
|
||||
layerGroup: node.layerGroup || '',
|
||||
parentW: parentW || rw,
|
||||
backgroundImages: node.backgroundImages
|
||||
});
|
||||
}
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
results.push(...collectSlideChildren(child, slideX, slideY));
|
||||
results.push(...collectSlideChildren(child, slideX, slideY, rw));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
@@ -335,13 +348,62 @@ function convert(input) {
|
||||
}
|
||||
|
||||
slideGroups.forEach(sg => console.log(` ${sg.name}: ${sg.children.length} 个元素`));
|
||||
|
||||
// ===== fallback: 收集不在 slide 子树内但坐标在 slide 范围内的节点 =====
|
||||
// 从根节点收集所有非 slide 节点
|
||||
function collectOrphans(node) {
|
||||
if (!node || !node.rect) return [];
|
||||
const results = [];
|
||||
const nr = node.rect;
|
||||
const rw = nr.width ?? nr.w;
|
||||
const rh = nr.height ?? nr.h;
|
||||
// 跳过 slide 容器本身和 raster
|
||||
if (!node.name?.startsWith('slide-') && node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') {
|
||||
results.push({
|
||||
id: node.id, type: node.type, name: node.name, tag: node.tag,
|
||||
rect: { x: nr.x, y: nr.y, w: rw, h: rh },
|
||||
styles: node.styles || {}, src: node.src || node.text || '',
|
||||
layerGroup: node.layerGroup || ''
|
||||
});
|
||||
}
|
||||
if (node.children) for (const c of node.children) {
|
||||
// 跳过 slide 容器的子树(它们已经被 collectSlideChildren 处理了)
|
||||
if (c.name?.startsWith('slide-')) continue;
|
||||
results.push(...collectOrphans(c));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
const orphans = collectOrphans(inputRoot);
|
||||
console.log(`游离节点: ${orphans.length} 个`);
|
||||
|
||||
// 把游离节点按 y 坐标归入最近的 slide
|
||||
for (const or of orphans) {
|
||||
if (or.rect.w == null || or.rect.h == null) continue;
|
||||
let best = null;
|
||||
let bestDist = Infinity;
|
||||
for (const sg of slideGroups) {
|
||||
const dist = Math.abs(or.rect.y - sg.slideRect.y);
|
||||
if (dist < sg.slideRect.h && dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = sg;
|
||||
}
|
||||
}
|
||||
if (best) {
|
||||
// 转相对坐标
|
||||
const relNode = { ...or, rect: { ...or.rect } };
|
||||
relNode.rect.x = or.rect.x - best.slideRect.x;
|
||||
relNode.rect.y = or.rect.y - best.slideRect.y;
|
||||
best.children.push(relNode);
|
||||
}
|
||||
}
|
||||
slideGroups.forEach(sg => console.log(` ${sg.name}: ${sg.children.length} 个元素 (含游离)`));
|
||||
// ----------------------------------------------------------
|
||||
// 第二步:每个 section → 一个 slide
|
||||
// ----------------------------------------------------------
|
||||
const slides = [];
|
||||
|
||||
for (const sg of slideGroups) {
|
||||
const objects = [];
|
||||
let objects = [];
|
||||
let slideBg = null;
|
||||
|
||||
// 从 slide 容器节点取背景色
|
||||
@@ -357,13 +419,13 @@ function convert(input) {
|
||||
console.log('处理 ' + sg.name + ': ' + sg.children.length + ' 个节点');
|
||||
|
||||
for (const n of sg.children) {
|
||||
// 跳过 raster 底图、纯容器
|
||||
if (n.type === 'RECTANGLE' || n.layerGroup === 'comparison') { continue; }
|
||||
// 跳过 raster 底图(保留 IMAGE 类型)
|
||||
if (n.type === 'RECTANGLE') { continue; }
|
||||
if (n.tag === 'BODY' || n.tag === 'HTML') { continue; }
|
||||
|
||||
const r = n.rect;
|
||||
if (r == null || r.w == null || r.h == null) { continue; }
|
||||
const S = 1/96;
|
||||
const S = 1/72;
|
||||
const opts = {
|
||||
x: Math.round(r.x * S * 1000) / 1000,
|
||||
y: Math.round(r.y * S * 1000) / 1000,
|
||||
@@ -375,13 +437,18 @@ function convert(input) {
|
||||
if (n.type === 'TEXT' && n.src) {
|
||||
const st = n.styles;
|
||||
const fs = parseFloat(st.fontSize) || 14;
|
||||
opts.fontSize = Math.round(fs * 72 / 96);
|
||||
var pt = Math.round(fs);
|
||||
opts.fontSize = pt;
|
||||
opts.fontFace = (st.fontFamily || 'Arial').split(',')[0].replace(/['"]/g,'').trim();
|
||||
opts.color = rgbaToHex(st.color) || '000000';
|
||||
if (parseInt(st.fontWeight) >= 700) opts.bold = true;
|
||||
if (st.fontStyle === 'italic') opts.italic = true;
|
||||
if (st.textAlign && st.textAlign !== 'start') opts.align = st.textAlign;
|
||||
|
||||
// 宽度优先用父容器,否则用文字自身宽度,再加 20% 余量防止换行
|
||||
var baseW = (n.parentW || r.w);
|
||||
opts.w = Math.round(baseW * 1.2 / 72 * 1000) / 1000;
|
||||
|
||||
objects.push({
|
||||
type: 'text',
|
||||
text: n.src,
|
||||
@@ -389,6 +456,21 @@ function convert(input) {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 图片节点 =====
|
||||
if (n.type === 'IMAGE' && n.backgroundImages && n.backgroundImages.length > 0) {
|
||||
var imgKey = n.backgroundImages[0];
|
||||
var asset = ctx.assets[imgKey];
|
||||
if (asset && asset.data) {
|
||||
objects.push({
|
||||
type: 'image',
|
||||
options: {
|
||||
x: opts.x, y: opts.y, w: opts.w, h: opts.h,
|
||||
data: asset.data
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 提取填充色 =====
|
||||
let fillColor = null;
|
||||
const bg = n.styles.backgroundColor;
|
||||
@@ -406,8 +488,13 @@ function convert(input) {
|
||||
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 brVal = n.styles.borderTopLeftRadius;
|
||||
if (brVal && brVal !== '0px') {
|
||||
if (brVal.includes('%')) {
|
||||
shapeOpts.rectRadius = parseFloat(brVal) / 100;
|
||||
} else {
|
||||
shapeOpts.rectRadius = parseFloat(brVal) / 72;
|
||||
}
|
||||
}
|
||||
|
||||
// 边框
|
||||
@@ -425,10 +512,17 @@ function convert(input) {
|
||||
}
|
||||
}
|
||||
|
||||
objects.push({ type: 'shape', shapeName: 'rect', options: shapeOpts });
|
||||
// 选择形状类型:圆角 >= 50% 且 w≈h 时用 ellipse(正圆),否则用 roundRect
|
||||
var useShape = 'rect';
|
||||
if (shapeOpts.rectRadius) {
|
||||
var brPct = parseFloat(n.styles.borderTopLeftRadius);
|
||||
var isCircle = (brPct >= 50 || (brPct.toString().includes('%') && parseFloat(brPct) >= 50)) && Math.abs(opts.w - opts.h) < 0.05;
|
||||
useShape = isCircle ? 'ellipse' : 'roundRect';
|
||||
}
|
||||
objects.push({ type: 'shape', shapeName: useShape, options: shapeOpts });
|
||||
}
|
||||
|
||||
// ===== 提取四周边框线(非填充) =====
|
||||
// ===== 提取四周边框线 =====
|
||||
var sides = [
|
||||
{ key: 'borderTopWidth', yOff: 0, hOff: 0 },
|
||||
{ key: 'borderBottomWidth', yOff: 1, hOff: 0 },
|
||||
@@ -454,6 +548,38 @@ function convert(input) {
|
||||
|
||||
const slideData = { objects };
|
||||
if (slideBg) slideData.background = slideBg;
|
||||
|
||||
// 提取 slide 容器自身的四周边框
|
||||
if (slideContainer && slideContainer.styles) {
|
||||
var sides = [
|
||||
{ key: 'borderTopWidth', keyC: 'borderTopColor', keyS: 'borderTopStyle', yOff: 0, xOff: 0, isW: false },
|
||||
{ key: 'borderBottomWidth', keyC: 'borderBottomColor', keyS: 'borderBottomStyle', yOff: 1, xOff: 0, isW: false },
|
||||
{ key: 'borderLeftWidth', keyC: 'borderLeftColor', keyS: 'borderLeftStyle', xOff: 0, yOff: 0, isW: true },
|
||||
{ key: 'borderRightWidth', keyC: 'borderRightColor', keyS: 'borderRightStyle', xOff: 1, yOff: 0, isW: true }
|
||||
];
|
||||
for (var si = 0; si < sides.length; si++) {
|
||||
var side = sides[si];
|
||||
var sw = parseFloat(slideContainer.styles[side.key]);
|
||||
var sc = rgbaToHex(slideContainer.styles[side.keyC]);
|
||||
if (sw > 0 && sc && slideContainer.styles[side.keyS] !== 'none') {
|
||||
var sr = sg.slideRect;
|
||||
var lineX = 0, lineY = 0, lineW = 0, lineH = 0;
|
||||
if (side.isW) {
|
||||
lineW = sw / 72;
|
||||
lineH = sr.h / 72;
|
||||
lineX = side.xOff === 0 ? 0 : (sr.w / 72 - sw/72);
|
||||
lineY = 0;
|
||||
} else {
|
||||
lineW = sr.w / 72;
|
||||
lineH = sw / 72;
|
||||
lineX = 0;
|
||||
lineY = side.yOff === 0 ? 0 : (sr.h / 72 - sw/72);
|
||||
}
|
||||
slideData.objects.push({ type: 'shape', shapeName: 'rect', options: { x: lineX, y: lineY, w: lineW, h: lineH, fill: { color: sc } } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slides.push(slideData);
|
||||
}
|
||||
|
||||
@@ -465,8 +591,8 @@ function convert(input) {
|
||||
if (slideGroups.length > 0) {
|
||||
const sr = slideGroups[0].slideRect;
|
||||
if (sr && sr.w && sr.h) {
|
||||
const sw = sr.w / 96;
|
||||
const sh = sr.h / 96;
|
||||
const sw = sr.w / 72;
|
||||
const sh = sr.h / 72;
|
||||
layout = 'CUSTOM';
|
||||
return {
|
||||
presentation: { layout: 'CUSTOM', slideWidth: sw, slideHeight: sh },
|
||||
@@ -549,6 +675,11 @@ if (require.main === module) {
|
||||
x: o.x, y: o.y, w: o.w, h: o.h,
|
||||
fill: o.fill ? { color: o.fill.color } : undefined
|
||||
});
|
||||
} else if (obj.type === 'image') {
|
||||
slide.addImage({
|
||||
x: o.x, y: o.y, w: o.w, h: o.h,
|
||||
data: o.data || o.path
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user