1128 lines
38 KiB
JavaScript
1128 lines
38 KiB
JavaScript
const PLUGIN_WIDTH = 380;
|
|
const PLUGIN_HEIGHT = 560;
|
|
const DEFAULT_FONT = { family: "Inter", style: "Regular" };
|
|
|
|
function getPixso() {
|
|
return typeof pixso !== "undefined" ? pixso : figma;
|
|
}
|
|
|
|
const app = getPixso();
|
|
|
|
function notify(message, type = "info") {
|
|
if (app.notification?.show) {
|
|
app.notification.show(message, type);
|
|
} else if (app.notify) {
|
|
app.notify(message);
|
|
}
|
|
}
|
|
|
|
function rgbToPaint(color, fallback = { r: 1, g: 1, b: 1, a: 1 }) {
|
|
const parsed = parseCssColor(color) || fallback;
|
|
if (!parsed) return null;
|
|
return {
|
|
type: "SOLID",
|
|
color: {
|
|
r: clamp01(parsed.r),
|
|
g: clamp01(parsed.g),
|
|
b: clamp01(parsed.b)
|
|
},
|
|
opacity: clamp01(parsed.a == null ? 1 : parsed.a)
|
|
};
|
|
}
|
|
|
|
function parseCssColor(value) {
|
|
if (!value || value === "transparent" || value === "rgba(0, 0, 0, 0)") return null;
|
|
|
|
const rgba = String(value).match(/rgba?\(([^)]+)\)/i);
|
|
if (rgba) {
|
|
const parts = rgba[1].split(",").map(part => part.trim());
|
|
return {
|
|
r: Number(parts[0]) / 255,
|
|
g: Number(parts[1]) / 255,
|
|
b: Number(parts[2]) / 255,
|
|
a: parts[3] == null ? 1 : Number(parts[3])
|
|
};
|
|
}
|
|
|
|
const hex = String(value).trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
|
|
if (!hex) return null;
|
|
|
|
let body = hex[1];
|
|
if (body.length === 3) {
|
|
body = body.split("").map(char => char + char).join("");
|
|
}
|
|
|
|
return {
|
|
r: parseInt(body.slice(0, 2), 16) / 255,
|
|
g: parseInt(body.slice(2, 4), 16) / 255,
|
|
b: parseInt(body.slice(4, 6), 16) / 255,
|
|
a: body.length === 8 ? parseInt(body.slice(6, 8), 16) / 255 : 1
|
|
};
|
|
}
|
|
|
|
function clamp01(value) {
|
|
return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 1));
|
|
}
|
|
|
|
function parsePixels(value, fallback = 0) {
|
|
const parsed = parseFloat(value);
|
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
}
|
|
|
|
function clampNumber(value, min, max, fallback = 0) {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return fallback;
|
|
return Math.max(min, Math.min(max, number));
|
|
}
|
|
|
|
function parseZIndex(value) {
|
|
const parsed = parseInt(value, 10);
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
|
|
function intersectRects(a, b) {
|
|
if (!a || !b) return null;
|
|
const x1 = Math.max(a.x || 0, b.x || 0);
|
|
const y1 = Math.max(a.y || 0, b.y || 0);
|
|
const x2 = Math.min((a.x || 0) + (a.width || 0), (b.x || 0) + (b.width || 0));
|
|
const y2 = Math.min((a.y || 0) + (a.height || 0), (b.y || 0) + (b.height || 0));
|
|
if (x2 <= x1 || y2 <= y1) return null;
|
|
return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
|
|
}
|
|
|
|
function overlapRatio(a, b) {
|
|
const intersection = intersectRects(a, b);
|
|
if (!intersection) return 0;
|
|
const area = Math.max(1, Math.min(
|
|
(a.width || 1) * (a.height || 1),
|
|
(b.width || 1) * (b.height || 1)
|
|
));
|
|
return (intersection.width * intersection.height) / area;
|
|
}
|
|
|
|
function clipsChildren(styles = {}) {
|
|
const overflow = `${styles.overflow || ""} ${styles.overflowX || ""} ${styles.overflowY || ""}`;
|
|
return /(hidden|clip|scroll|auto)/i.test(overflow);
|
|
}
|
|
|
|
function getRenderRect(captureNode) {
|
|
const rect = captureNode?.rect;
|
|
const visible = captureNode?.visibleRect;
|
|
if (!rect) return null;
|
|
if (!visible) return rect;
|
|
|
|
const rectArea = Math.max(1, (rect.width || 1) * (rect.height || 1));
|
|
const visibleArea = Math.max(1, (visible.width || 1) * (visible.height || 1));
|
|
const isHugeOverflow = rect.width > visible.width * 1.5 || rect.height > visible.height * 1.5 || rectArea > visibleArea * 2;
|
|
return captureNode.clipped && isHugeOverflow ? visible : rect;
|
|
}
|
|
|
|
function rectsDiffer(a, b, tolerance = 0.5) {
|
|
if (!a || !b) return false;
|
|
return Math.abs((a.x || 0) - (b.x || 0)) > tolerance ||
|
|
Math.abs((a.y || 0) - (b.y || 0)) > tolerance ||
|
|
Math.abs((a.width || 0) - (b.width || 0)) > tolerance ||
|
|
Math.abs((a.height || 0) - (b.height || 0)) > tolerance;
|
|
}
|
|
|
|
function shouldUseClipWrapper(captureNode) {
|
|
if (!captureNode || captureNode.type === "TEXT") return false;
|
|
if (!captureNode.rect || !captureNode.visibleRect) return false;
|
|
if (!rectsDiffer(captureNode.rect, captureNode.visibleRect)) return false;
|
|
return Boolean(captureNode.clipped || hasImageFill(captureNode) || captureNode.backgroundImages?.length || captureNode.type === "RECTANGLE");
|
|
}
|
|
|
|
function paintFromBackgroundImage(value) {
|
|
if (!value || !String(value).includes("gradient")) return null;
|
|
const colors = String(value).match(/rgba?\([^)]+\)|#[0-9a-f]{3,8}/gi);
|
|
if (!colors?.length) return null;
|
|
const stops = colors.map((color, index) => {
|
|
const paint = rgbToPaint(color, null);
|
|
if (!paint) return null;
|
|
return {
|
|
position: colors.length === 1 ? 0 : index / (colors.length - 1),
|
|
color: {
|
|
...paint.color,
|
|
a: paint.opacity == null ? 1 : paint.opacity
|
|
}
|
|
};
|
|
}).filter(Boolean);
|
|
|
|
if (stops.length < 2) {
|
|
return rgbToPaint(colors[0], null);
|
|
}
|
|
|
|
return {
|
|
type: "GRADIENT_LINEAR",
|
|
gradientStops: stops,
|
|
gradientTransform: [
|
|
[1, 0, 0],
|
|
[0, 1, 0]
|
|
]
|
|
};
|
|
}
|
|
|
|
function parseBoxShadow(value, scale) {
|
|
if (!value || value === "none") return null;
|
|
const colorMatch = String(value).match(/rgba?\([^)]+\)|#[0-9a-f]{3,8}/i);
|
|
const color = rgbToPaint(colorMatch?.[0] || "rgba(0,0,0,.18)", { r: 0, g: 0, b: 0, a: 0.18 });
|
|
const numericPart = String(value).replace(/rgba?\([^)]+\)|#[0-9a-f]{3,8}/ig, "");
|
|
const numbers = numericPart.match(/-?\d*\.?\d+px/g)?.map(parseFloat) || [];
|
|
if (!numbers.length) return null;
|
|
|
|
return {
|
|
type: "DROP_SHADOW",
|
|
color: {
|
|
...color.color,
|
|
a: color.opacity == null ? 1 : color.opacity
|
|
},
|
|
offset: {
|
|
x: (numbers[0] || 0) * scale,
|
|
y: (numbers[1] || 0) * scale
|
|
},
|
|
radius: Math.max(0, (numbers[2] || 0) * scale),
|
|
spread: (numbers[3] || 0) * scale,
|
|
visible: true,
|
|
blendMode: "NORMAL"
|
|
};
|
|
}
|
|
|
|
function parseDropShadowFilter(value, scale) {
|
|
if (!value || value === "none") return null;
|
|
const match = String(value).match(/drop-shadow\(([^)]+)\)/i);
|
|
if (!match) return null;
|
|
return parseBoxShadow(match[1], scale);
|
|
}
|
|
|
|
function setSize(node, width, height) {
|
|
const safeWidth = Math.max(1, Math.round(width || 1));
|
|
const safeHeight = Math.max(1, Math.round(height || 1));
|
|
if (typeof node.resize === "function") {
|
|
node.resize(safeWidth, safeHeight);
|
|
} else {
|
|
node.width = safeWidth;
|
|
node.height = safeHeight;
|
|
}
|
|
}
|
|
|
|
function setPosition(node, rect, parentRect, scale) {
|
|
node.x = Math.round(((rect?.x || 0) - (parentRect?.x || 0)) * scale);
|
|
node.y = Math.round(((rect?.y || 0) - (parentRect?.y || 0)) * scale);
|
|
setSize(node, (rect?.width || 1) * scale, (rect?.height || 1) * scale);
|
|
}
|
|
|
|
function applyTransform(node, captureNode) {
|
|
const transform = String(captureNode.styles?.transform || "");
|
|
if (!transform || transform === "none" || !("rotation" in node)) return;
|
|
|
|
const rotate = transform.match(/rotate\((-?\d*\.?\d+)deg\)/i);
|
|
if (rotate) {
|
|
node.rotation = Number(rotate[1]) || 0;
|
|
return;
|
|
}
|
|
|
|
const matrix = transform.match(/matrix\(([^)]+)\)/i);
|
|
if (!matrix) return;
|
|
const parts = matrix[1].split(",").map(part => Number(part.trim()));
|
|
if (parts.length < 4 || parts.some(value => !Number.isFinite(value))) return;
|
|
const angle = Math.atan2(parts[1], parts[0]) * 180 / Math.PI;
|
|
const hasRotation = Math.abs(angle) > 0.1 && Math.abs(angle) < 89.9;
|
|
const hasSkew = Math.abs(parts[1]) > 0.001 || Math.abs(parts[2]) > 0.001;
|
|
if (hasRotation && hasSkew) {
|
|
node.rotation = angle;
|
|
}
|
|
}
|
|
|
|
function mapPrimaryAxisAlign(value) {
|
|
const normalized = String(value || "").toLowerCase();
|
|
if (normalized.includes("space-between")) return "SPACE_BETWEEN";
|
|
if (normalized.includes("center")) return "CENTER";
|
|
if (normalized.includes("end") || normalized.includes("right") || normalized.includes("bottom")) return "MAX";
|
|
return "MIN";
|
|
}
|
|
|
|
function mapCounterAxisAlign(value) {
|
|
const normalized = String(value || "").toLowerCase();
|
|
if (normalized.includes("center")) return "CENTER";
|
|
if (normalized.includes("end") || normalized.includes("right") || normalized.includes("bottom")) return "MAX";
|
|
if (normalized.includes("stretch")) return "MIN";
|
|
return "MIN";
|
|
}
|
|
|
|
function trySetLayoutProperty(node, key, value) {
|
|
try {
|
|
if (key in node) {
|
|
node[key] = value;
|
|
return true;
|
|
}
|
|
} catch {
|
|
// Pixso/Figma API compatibility differs between editor versions.
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function applyAutoLayout(node, captureNode, scale) {
|
|
const layout = captureNode?.layout;
|
|
if (!layout || captureNode.type === "TEXT") return false;
|
|
if (layout.type !== "flex" && layout.type !== "inferred" && layout.type !== "grid") return false;
|
|
|
|
const direction = String(layout.direction || "").toLowerCase();
|
|
const isGrid = layout.type === "grid";
|
|
const isColumn = !isGrid && direction.includes("column");
|
|
let applied = false;
|
|
|
|
applied = trySetLayoutProperty(node, "layoutMode", isColumn ? "VERTICAL" : "HORIZONTAL") || applied;
|
|
applied = trySetLayoutProperty(node, "primaryAxisAlignItems", mapPrimaryAxisAlign(layout.justifyContent)) || applied;
|
|
applied = trySetLayoutProperty(node, "counterAxisAlignItems", mapCounterAxisAlign(layout.alignItems)) || applied;
|
|
applied = trySetLayoutProperty(node, "primaryAxisSizingMode", "FIXED") || applied;
|
|
applied = trySetLayoutProperty(node, "counterAxisSizingMode", "FIXED") || applied;
|
|
|
|
const itemSpacing = isColumn
|
|
? layout.rowGap || layout.gap
|
|
: layout.columnGap || layout.gap;
|
|
applied = trySetLayoutProperty(node, "itemSpacing", Math.round(clampNumber(itemSpacing, 0, 400, 0) * scale)) || applied;
|
|
|
|
const padding = layout.padding || {};
|
|
applied = trySetLayoutProperty(node, "paddingTop", Math.round(clampNumber(padding.top, 0, 400, 0) * scale)) || applied;
|
|
applied = trySetLayoutProperty(node, "paddingRight", Math.round(clampNumber(padding.right, 0, 400, 0) * scale)) || applied;
|
|
applied = trySetLayoutProperty(node, "paddingBottom", Math.round(clampNumber(padding.bottom, 0, 400, 0) * scale)) || applied;
|
|
applied = trySetLayoutProperty(node, "paddingLeft", Math.round(clampNumber(padding.left, 0, 400, 0) * scale)) || applied;
|
|
|
|
if (isGrid || String(layout.wrap || "").toLowerCase().includes("wrap")) {
|
|
applied = trySetLayoutProperty(node, "layoutWrap", "WRAP") || applied;
|
|
applied = trySetLayoutProperty(node, "counterAxisSpacing", Math.round(clampNumber(layout.rowGap || layout.gap, 0, 400, 0) * scale)) || applied;
|
|
}
|
|
|
|
if (typeof node.setPluginData === "function") {
|
|
try {
|
|
node.setPluginData("webToPixsoLayout", JSON.stringify(layout));
|
|
if (captureNode.section) {
|
|
node.setPluginData("webToPixsoSection", String(captureNode.section));
|
|
}
|
|
} catch {
|
|
// Layout metadata is a bonus; failing to store it should not block import.
|
|
}
|
|
}
|
|
|
|
return applied;
|
|
}
|
|
|
|
function getPage() {
|
|
return app.currentPage || app.state?.currentPage;
|
|
}
|
|
|
|
async function loadDefaultFont() {
|
|
if (typeof app.loadFontAsync !== "function") return;
|
|
try {
|
|
await app.loadFontAsync(DEFAULT_FONT);
|
|
} catch {
|
|
try {
|
|
await app.loadFontAsync({ family: "Arial", style: "Regular" });
|
|
} catch {
|
|
// Pixso may already have a default font loaded.
|
|
}
|
|
}
|
|
}
|
|
|
|
function hasVisualStyle(captureNode) {
|
|
if (!captureNode || captureNode.type === "TEXT") return true;
|
|
const styles = captureNode.styles || {};
|
|
const hasImage = Boolean(hasImageFill(captureNode) || captureNode.backgroundImages?.length);
|
|
const hasFill = Boolean(
|
|
shouldRenderBackgroundFill(captureNode) &&
|
|
(hasVisibleColor(styles.backgroundColor) || hasCssBackgroundImage(styles))
|
|
);
|
|
const borderWidth = getBorderWidth(styles);
|
|
const meaningfulRadius = hasRadius(styles) && !isLargePlainLayoutWrapper(captureNode);
|
|
return hasImage || hasFill || borderWidth > 0 || hasShadow(styles) || meaningfulRadius;
|
|
}
|
|
|
|
function shouldSkipImportNode(captureNode) {
|
|
const styles = captureNode?.styles || {};
|
|
if ((captureNode?.tag === "::before" || captureNode?.tag === "::after") &&
|
|
!captureNode.text &&
|
|
!captureNode.src &&
|
|
!captureNode.backgroundImages?.length) {
|
|
return true;
|
|
}
|
|
if ((captureNode?.tag === "::before" || captureNode?.tag === "::after") &&
|
|
captureNode?.backgroundImages?.length &&
|
|
styles.backgroundRepeat &&
|
|
styles.backgroundRepeat !== "no-repeat") {
|
|
return true;
|
|
}
|
|
if (captureNode?.tag === "PICTURE" &&
|
|
captureNode.src &&
|
|
captureNode.children?.some(child => child.tag === "IMG" && child.src)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isRasterFallbackNode(captureNode) {
|
|
return captureNode?.tag === "RASTER_FALLBACK" || captureNode?.attributes?.["data-raster-fallback"] === "true";
|
|
}
|
|
|
|
function getRasterMode(captureNode) {
|
|
return captureNode?.rasterMode || captureNode?.attributes?.["data-raster-mode"] || "";
|
|
}
|
|
|
|
function getLayerGroup(captureNode) {
|
|
const explicit = captureNode?.layerGroup || captureNode?.attributes?.["data-layer-group"];
|
|
const normalized = String(explicit || "").toLowerCase();
|
|
if (normalized === "comparison" || normalized === "fallback") return "comparison";
|
|
if (normalized === "text") return "text";
|
|
if (normalized === "editable" || normalized === "element") return "editable";
|
|
if (isRasterFallbackNode(captureNode)) return "comparison";
|
|
if (captureNode?.type === "TEXT") return "text";
|
|
return "editable";
|
|
}
|
|
|
|
function getLayerPriority(captureNode) {
|
|
const explicit = Number(captureNode?.layerPriority);
|
|
if (Number.isFinite(explicit)) return explicit;
|
|
const group = getLayerGroup(captureNode);
|
|
if (group === "comparison") return 0;
|
|
if (group === "text") return 200;
|
|
return 100;
|
|
}
|
|
|
|
function hasImageFill(captureNode) {
|
|
if (!captureNode?.src) return false;
|
|
if (isRasterFallbackNode(captureNode)) return true;
|
|
return /^(IMG|PICTURE|VIDEO|CANVAS|SOURCE)$/i.test(captureNode.tag || "");
|
|
}
|
|
|
|
function hasVisibleColor(value) {
|
|
return Boolean(parseCssColor(value));
|
|
}
|
|
|
|
function isLightOpaqueColor(value) {
|
|
const color = parseCssColor(value);
|
|
return Boolean(color && color.a > 0.92 && color.r > 0.92 && color.g > 0.92 && color.b > 0.92);
|
|
}
|
|
|
|
function getBorderWidth(styles = {}) {
|
|
return Math.max(
|
|
parsePixels(styles.borderTopWidth),
|
|
parsePixels(styles.borderRightWidth),
|
|
parsePixels(styles.borderBottomWidth),
|
|
parsePixels(styles.borderLeftWidth)
|
|
);
|
|
}
|
|
|
|
function hasRadius(styles = {}) {
|
|
return Boolean(
|
|
parsePixels(styles.borderTopLeftRadius) ||
|
|
parsePixels(styles.borderTopRightRadius) ||
|
|
parsePixels(styles.borderBottomRightRadius) ||
|
|
parsePixels(styles.borderBottomLeftRadius)
|
|
);
|
|
}
|
|
|
|
function hasShadow(styles = {}) {
|
|
return Boolean(styles.boxShadow && styles.boxShadow !== "none") ||
|
|
Boolean(styles.filter && /drop-shadow/i.test(styles.filter));
|
|
}
|
|
|
|
function hasCssBackgroundImage(styles = {}) {
|
|
return Boolean(styles.backgroundImage && styles.backgroundImage !== "none");
|
|
}
|
|
|
|
function isContainerTag(captureNode) {
|
|
return /^(HTML|BODY|DIV|SECTION|MAIN|ARTICLE|HEADER|NAV|FOOTER|ASIDE|UL|OL|LI|FORM)$/i.test(captureNode?.tag || "");
|
|
}
|
|
|
|
function getNodeArea(captureNode) {
|
|
const rect = getRenderRect(captureNode) || captureNode?.rect || {};
|
|
return Math.max(0, rect.width || 0) * Math.max(0, rect.height || 0);
|
|
}
|
|
|
|
function isLargePlainLayoutWrapper(captureNode) {
|
|
if (!captureNode || captureNode.type === "TEXT" || isRasterFallbackNode(captureNode)) return false;
|
|
if (!captureNode.children?.length || !isContainerTag(captureNode)) return false;
|
|
|
|
const styles = captureNode.styles || {};
|
|
if (hasImageFill(captureNode) || captureNode.backgroundImages?.length || hasCssBackgroundImage(styles)) return false;
|
|
if (!isLightOpaqueColor(styles.backgroundColor)) return false;
|
|
|
|
const rect = getRenderRect(captureNode) || captureNode.rect || {};
|
|
const clipWidth = captureNode.clipRect?.width || captureNode.visibleRect?.width || rect.width || 0;
|
|
const area = getNodeArea(captureNode);
|
|
const isWideSection = clipWidth > 0 && (rect.width || 0) >= clipWidth * 0.55 && (rect.height || 0) >= 96;
|
|
const isHugeContainer = area >= 180000;
|
|
if (!isWideSection && !isHugeContainer) return false;
|
|
|
|
const hasDecor = getBorderWidth(styles) > 0 || hasShadow(styles) || hasRadius(styles);
|
|
return !hasDecor || area >= 300000;
|
|
}
|
|
|
|
function shouldRenderBackgroundFill(captureNode) {
|
|
if (!captureNode || isRasterFallbackNode(captureNode)) return true;
|
|
if (captureNode.visualRole === "layout-wrapper" && isLargePlainLayoutWrapper(captureNode)) return false;
|
|
return !isLargePlainLayoutWrapper(captureNode);
|
|
}
|
|
|
|
function createNodeForCapture(captureNode) {
|
|
if (captureNode.type === "TEXT") {
|
|
return app.createText();
|
|
}
|
|
if (captureNode.type === "RECTANGLE" || hasImageFill(captureNode) || captureNode.backgroundImages?.length) {
|
|
return app.createRectangle();
|
|
}
|
|
return app.createFrame();
|
|
}
|
|
|
|
function createNodeForGroupedCapture(captureNode) {
|
|
if (captureNode.type === "TEXT") {
|
|
return app.createText();
|
|
}
|
|
const hasChildren = Boolean(captureNode.children?.length);
|
|
const isLeafMedia = hasImageFill(captureNode) && !hasChildren;
|
|
const isLeafImageBackground = captureNode.backgroundImages?.length && !hasChildren;
|
|
if (captureNode.type === "RECTANGLE" && (isLeafMedia || isLeafImageBackground)) {
|
|
return app.createRectangle();
|
|
}
|
|
return app.createFrame();
|
|
}
|
|
|
|
function dataUrlToBytes(dataUrl) {
|
|
const value = String(dataUrl || "");
|
|
const base64 = value.includes(",") ? value.split(",")[1] : value;
|
|
if (!base64) return null;
|
|
|
|
if (typeof app.base64Decode === "function") {
|
|
return app.base64Decode(base64);
|
|
}
|
|
|
|
if (typeof atob !== "function") {
|
|
return decodeBase64(base64);
|
|
}
|
|
|
|
const binary = atob(base64);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let index = 0; index < binary.length; index += 1) {
|
|
bytes[index] = binary.charCodeAt(index);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
function decodeBase64(base64) {
|
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
const clean = String(base64).replace(/[\r\n\s=]/g, "");
|
|
const bytes = [];
|
|
let buffer = 0;
|
|
let bits = 0;
|
|
|
|
for (let index = 0; index < clean.length; index += 1) {
|
|
const value = chars.indexOf(clean[index]);
|
|
if (value < 0) continue;
|
|
buffer = (buffer << 6) | value;
|
|
bits += 6;
|
|
if (bits >= 8) {
|
|
bits -= 8;
|
|
bytes.push((buffer >> bits) & 0xff);
|
|
}
|
|
}
|
|
|
|
return new Uint8Array(bytes);
|
|
}
|
|
|
|
function normalizeUrl(url) {
|
|
if (!url) return "";
|
|
try {
|
|
const parsed = new URL(url);
|
|
parsed.hash = "";
|
|
return parsed.href;
|
|
} catch {
|
|
return String(url).split("#")[0];
|
|
}
|
|
}
|
|
|
|
function resolveAsset(assets, url) {
|
|
if (!assets || !url) return null;
|
|
if (assets[url]) return assets[url];
|
|
|
|
const normalized = normalizeUrl(url);
|
|
for (const [assetUrl, asset] of Object.entries(assets)) {
|
|
if (normalizeUrl(assetUrl) === normalized) {
|
|
return asset;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function createImagePaint(asset, stats, scaleMode = "FILL") {
|
|
if (!asset?.data || typeof app.createImage !== "function") return null;
|
|
try {
|
|
const bytes = dataUrlToBytes(asset.data);
|
|
if (!bytes) return null;
|
|
const image = app.createImage(bytes);
|
|
const imageHash = image.hash || image.imageHash || image;
|
|
if (!imageHash || typeof imageHash !== "string") {
|
|
throw new Error("Pixso 没有返回有效的 imageHash");
|
|
}
|
|
if (stats) stats.imagesImported += 1;
|
|
return { type: "IMAGE", scaleMode, imageHash, opacity: 1 };
|
|
} catch (error) {
|
|
console.warn("[Web to Pixso] Image fill failed", error);
|
|
if (stats) stats.imageFailures += 1;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getImageCandidates(captureNode) {
|
|
const styles = captureNode.styles || {};
|
|
return [
|
|
hasImageFill(captureNode) ? captureNode.src : null,
|
|
...(captureNode.backgroundImages || []),
|
|
...[
|
|
styles.maskImage,
|
|
styles.webkitMaskImage
|
|
].flatMap(value => {
|
|
if (!value || value === "none") return [];
|
|
const urls = [];
|
|
const re = /url\((["']?)(.*?)\1\)/g;
|
|
let match;
|
|
while ((match = re.exec(value))) {
|
|
if (match[2]) urls.push(match[2]);
|
|
}
|
|
return urls;
|
|
})
|
|
].filter(Boolean);
|
|
}
|
|
|
|
function applyVisualStyles(node, captureNode, assets, stats, scale) {
|
|
const styles = captureNode.styles || {};
|
|
const fills = [];
|
|
const background = rgbToPaint(styles.backgroundColor, null) || paintFromBackgroundImage(styles.backgroundImage);
|
|
if (background && shouldRenderBackgroundFill(captureNode)) {
|
|
fills.push(background);
|
|
} else if (background && stats) {
|
|
stats.transparentWrappers += 1;
|
|
}
|
|
|
|
const imageUrls = getImageCandidates(captureNode);
|
|
if (imageUrls.length && stats) stats.imageCandidates += 1;
|
|
const imageScaleMode = /contain/i.test(styles.backgroundSize || styles.objectFit || "") ? "FIT" : "FILL";
|
|
const imagePaint = imageUrls
|
|
.map(url => createImagePaint(resolveAsset(assets, url), stats, imageScaleMode))
|
|
.find(Boolean);
|
|
if (imagePaint) {
|
|
fills.push(imagePaint);
|
|
}
|
|
|
|
if (fills.length) {
|
|
node.fills = fills;
|
|
} else if ("fills" in node && captureNode.type !== "TEXT") {
|
|
node.fills = [];
|
|
}
|
|
|
|
const borderWidth = Math.max(
|
|
parsePixels(styles.borderTopWidth),
|
|
parsePixels(styles.borderRightWidth),
|
|
parsePixels(styles.borderBottomWidth),
|
|
parsePixels(styles.borderLeftWidth)
|
|
);
|
|
|
|
if (borderWidth > 0 && styles.borderTopStyle !== "none") {
|
|
node.strokes = [rgbToPaint(styles.borderTopColor, { r: 0, g: 0, b: 0, a: 1 })];
|
|
node.strokeWeight = Math.max(1, borderWidth * scale);
|
|
}
|
|
|
|
const radius = Math.max(
|
|
parsePixels(styles.borderTopLeftRadius),
|
|
parsePixels(styles.borderTopRightRadius),
|
|
parsePixels(styles.borderBottomRightRadius),
|
|
parsePixels(styles.borderBottomLeftRadius)
|
|
);
|
|
if ("cornerRadius" in node) {
|
|
node.cornerRadius = radius * scale;
|
|
}
|
|
|
|
if ("opacity" in node) {
|
|
node.opacity = clamp01(Number(styles.opacity || 1));
|
|
}
|
|
|
|
const shadow = parseBoxShadow(styles.boxShadow, scale) || parseDropShadowFilter(styles.filter, scale);
|
|
if (shadow && "effects" in node) {
|
|
node.effects = [shadow];
|
|
}
|
|
|
|
if ("clipsContent" in node) {
|
|
node.clipsContent = clipsChildren(styles);
|
|
}
|
|
}
|
|
|
|
function applyTextStyles(node, captureNode, scale) {
|
|
const styles = captureNode.styles || {};
|
|
const color = rgbToPaint(styles.color, { r: 0, g: 0, b: 0, a: 1 });
|
|
if ("textAutoResize" in node) {
|
|
node.textAutoResize = "NONE";
|
|
}
|
|
node.characters = captureNode.text || "";
|
|
node.fontSize = Math.max(1, parsePixels(styles.fontSize, 14) * scale);
|
|
node.fills = [color];
|
|
if ("fontWeight" in node) {
|
|
node.fontWeight = styles.fontWeight || "normal";
|
|
}
|
|
|
|
if ("textAlignHorizontal" in node) {
|
|
const align = String(styles.textAlign || "left").toUpperCase();
|
|
node.textAlignHorizontal = align === "CENTER" || align === "RIGHT" || align === "JUSTIFY"
|
|
? (align === "JUSTIFY" ? "JUSTIFIED" : align)
|
|
: "LEFT";
|
|
}
|
|
|
|
if ("lineHeight" in node) {
|
|
const lineHeight = parsePixels(styles.lineHeight);
|
|
if (lineHeight > 0) {
|
|
node.lineHeight = { value: lineHeight * scale, unit: "PIXELS" };
|
|
}
|
|
}
|
|
|
|
if ("letterSpacing" in node) {
|
|
const letterSpacing = parsePixels(styles.letterSpacing);
|
|
if (letterSpacing) {
|
|
node.letterSpacing = { value: letterSpacing * scale, unit: "PIXELS" };
|
|
}
|
|
}
|
|
|
|
const decoration = String(styles.textDecorationLine || styles.textDecoration || "");
|
|
if ("textDecoration" in node) {
|
|
if (/line-through/i.test(decoration)) {
|
|
node.textDecoration = "STRIKETHROUGH";
|
|
} else if (/underline/i.test(decoration)) {
|
|
node.textDecoration = "UNDERLINE";
|
|
}
|
|
}
|
|
}
|
|
|
|
function expandTextBounds(node, captureNode, rect, scale) {
|
|
if (!rect || typeof node.resize !== "function") return;
|
|
const fontSize = parsePixels(captureNode.styles?.fontSize, 14) * scale;
|
|
const widthPadding = Math.max(4, Math.min(14, fontSize * 0.45));
|
|
const heightPadding = Math.max(2, Math.min(8, fontSize * 0.25));
|
|
setSize(node, rect.width * scale + widthPadding, rect.height * scale + heightPadding);
|
|
}
|
|
|
|
function getNodeIdentityText(captureNode) {
|
|
return [
|
|
captureNode?.tag,
|
|
captureNode?.name,
|
|
captureNode?.attributes?.id,
|
|
captureNode?.attributes?.class,
|
|
captureNode?.attributes?.role
|
|
].filter(Boolean).join(" ");
|
|
}
|
|
|
|
function hasLayoutSignal(captureNode) {
|
|
const layout = captureNode?.layout;
|
|
if (!layout) return false;
|
|
if (layout.type === "flex" || layout.type === "grid") return true;
|
|
if (layout.type === "inferred" && layout.confidence >= 0.7) return true;
|
|
return false;
|
|
}
|
|
|
|
function isHighValueLayoutContainer(captureNode) {
|
|
if (!captureNode || captureNode.type === "TEXT") return false;
|
|
if (isRasterFallbackNode(captureNode)) return false;
|
|
if (!captureNode.rect || !captureNode.children?.length) return false;
|
|
if (!hasLayoutSignal(captureNode)) return false;
|
|
if (/^(HTML|BODY)$/i.test(captureNode.tag || "")) return false;
|
|
|
|
const rect = getRenderRect(captureNode) || captureNode.rect;
|
|
if (!rect || rect.width < 24 || rect.height < 16) return false;
|
|
|
|
const identity = getNodeIdentityText(captureNode);
|
|
const semanticMatch = /header|nav|navigation|navbar|menu|toolbar|tabs|actions|buttons|button-group|btns|grid|cards|card-list|product|goods|footer|links|columns|list|row|form|search/i.test(identity);
|
|
const tagMatch = /^(HEADER|NAV|FOOTER|UL|OL|FORM)$/i.test(captureNode.tag || "");
|
|
const layout = captureNode.layout || {};
|
|
const childCount = layout.childCount || captureNode.children.length;
|
|
const isReasonableFlex = layout.type === "flex" && childCount >= 2 && childCount <= 18 && rect.height <= 480;
|
|
const isReasonableGrid = layout.type === "grid" && childCount >= 2 && childCount <= 60;
|
|
const isFooterColumns = /footer/i.test(identity) && childCount >= 2;
|
|
|
|
return tagMatch || semanticMatch || isReasonableFlex || isReasonableGrid || isFooterColumns;
|
|
}
|
|
|
|
function markSubtreeIds(captureNode, ids, options = {}) {
|
|
if (!captureNode) return ids;
|
|
const includeText = options.includeText === true;
|
|
if (captureNode.id && (includeText || getLayerGroup(captureNode) !== "text")) {
|
|
ids.add(captureNode.id);
|
|
}
|
|
for (const child of captureNode.children || []) {
|
|
markSubtreeIds(child, ids, options);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
function collectLayoutRoots(captureNode, output = [], state = { order: 0 }, insideLayout = false) {
|
|
if (!captureNode) return output;
|
|
captureNode.__layoutOrder = state.order;
|
|
state.order += 1;
|
|
|
|
const isCandidate = isHighValueLayoutContainer(captureNode);
|
|
if (isCandidate && !insideLayout) {
|
|
output.push(captureNode);
|
|
return output;
|
|
}
|
|
|
|
for (const child of captureNode.children || []) {
|
|
collectLayoutRoots(child, output, state, insideLayout || isCandidate);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function sortLayoutRoots(nodes) {
|
|
return nodes.sort((a, b) => {
|
|
const ar = getRenderRect(a) || a.rect || {};
|
|
const br = getRenderRect(b) || b.rect || {};
|
|
const ya = ar.y || 0;
|
|
const yb = br.y || 0;
|
|
if (Math.abs(ya - yb) > 8) return ya - yb;
|
|
const xa = ar.x || 0;
|
|
const xb = br.x || 0;
|
|
if (Math.abs(xa - xb) > 8) return xa - xb;
|
|
return (a.__layoutOrder || 0) - (b.__layoutOrder || 0);
|
|
});
|
|
}
|
|
|
|
function shouldRenderGroupedNode(captureNode) {
|
|
if (!captureNode) return false;
|
|
if (shouldSkipImportNode(captureNode)) return false;
|
|
if (captureNode.type === "TEXT") return true;
|
|
if (isRasterFallbackNode(captureNode)) return false;
|
|
if (hasVisualStyle(captureNode)) return true;
|
|
if (captureNode.children?.length) return true;
|
|
return false;
|
|
}
|
|
|
|
function getLayoutGroupName(captureNode, depth) {
|
|
const base = captureNode.name || captureNode.tag || "Group";
|
|
if (depth !== 0) return base;
|
|
const section = String(captureNode.section || "").toLowerCase();
|
|
const sectionLabel = section
|
|
? section.charAt(0).toUpperCase() + section.slice(1)
|
|
: "Module";
|
|
const layoutType = captureNode.layout?.type ? ` ${captureNode.layout.type}` : "";
|
|
return `Layout - ${sectionLabel}${layoutType} - ${base}`;
|
|
}
|
|
|
|
function compareVisualRowMajor(a, b) {
|
|
const ar = getRenderRect(a) || a.rect || {};
|
|
const br = getRenderRect(b) || b.rect || {};
|
|
const ay = ar.y || 0;
|
|
const by = br.y || 0;
|
|
if (Math.abs(ay - by) > 8) return ay - by;
|
|
const ax = ar.x || 0;
|
|
const bx = br.x || 0;
|
|
if (Math.abs(ax - bx) > 8) return ax - bx;
|
|
return 0;
|
|
}
|
|
|
|
function compareVisualColumnMajor(a, b) {
|
|
const ar = getRenderRect(a) || a.rect || {};
|
|
const br = getRenderRect(b) || b.rect || {};
|
|
const ax = ar.x || 0;
|
|
const bx = br.x || 0;
|
|
if (Math.abs(ax - bx) > 8) return ax - bx;
|
|
const ay = ar.y || 0;
|
|
const by = br.y || 0;
|
|
if (Math.abs(ay - by) > 8) return ay - by;
|
|
return 0;
|
|
}
|
|
|
|
function getOrderedLayoutChildren(captureNode) {
|
|
const children = [...(captureNode.children || [])];
|
|
const layout = captureNode.layout || {};
|
|
if (!children.length || !layout.type) return children;
|
|
if (layout.type === "grid") return children.sort(compareVisualRowMajor);
|
|
const direction = String(layout.direction || "").toLowerCase();
|
|
if (direction.includes("column")) {
|
|
return children.sort((a, b) => compareVisualColumnMajor(a, b) || compareVisualRowMajor(a, b));
|
|
}
|
|
return children.sort((a, b) => compareVisualRowMajor(a, b) || compareVisualColumnMajor(a, b));
|
|
}
|
|
|
|
async function renderGroupedSubtree(captureNode, parent, parentRect, assets, stats, scale, depth = 0) {
|
|
if (!shouldRenderGroupedNode(captureNode)) return null;
|
|
if (getLayerGroup(captureNode) === "text") return null;
|
|
const renderRect = getRenderRect(captureNode);
|
|
if (!renderRect || renderRect.width <= 0 || renderRect.height <= 0) return null;
|
|
|
|
const node = createNodeForGroupedCapture(captureNode);
|
|
node.name = getLayoutGroupName(captureNode, depth);
|
|
setPosition(node, renderRect, parentRect, scale);
|
|
|
|
if (captureNode.type === "TEXT") {
|
|
applyTextStyles(node, captureNode, scale);
|
|
expandTextBounds(node, captureNode, renderRect, scale);
|
|
} else {
|
|
applyVisualStyles(node, captureNode, assets, stats, scale);
|
|
if (applyAutoLayout(node, captureNode, scale) && stats) {
|
|
stats.layoutFrames += 1;
|
|
}
|
|
}
|
|
applyTransform(node, captureNode);
|
|
|
|
parent.appendChild(node);
|
|
stats.nodesCreated += 1;
|
|
if (depth === 0) {
|
|
stats.layoutGroups += 1;
|
|
}
|
|
|
|
const canContainChildren = typeof node.appendChild === "function" && captureNode.children?.length;
|
|
if (!canContainChildren) return node;
|
|
|
|
for (const child of getOrderedLayoutChildren(captureNode)) {
|
|
try {
|
|
await renderGroupedSubtree(child, node, renderRect, assets, stats, scale, depth + 1);
|
|
} catch (error) {
|
|
console.warn("[Web to Pixso] Grouped layer import skipped", error);
|
|
stats.nodesSkipped += 1;
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function collectRenderableNodes(captureNode, output = [], state = { order: 0 }) {
|
|
if (!captureNode) return output;
|
|
if (
|
|
captureNode.tag !== "BODY" &&
|
|
captureNode.tag !== "HTML" &&
|
|
!shouldSkipImportNode(captureNode) &&
|
|
hasVisualStyle(captureNode)
|
|
) {
|
|
captureNode.__importOrder = state.order;
|
|
output.push(captureNode);
|
|
}
|
|
state.order += 1;
|
|
for (const child of captureNode.children || []) {
|
|
collectRenderableNodes(child, output, state);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function sortRenderableNodes(nodes) {
|
|
return nodes.sort((a, b) => {
|
|
const pa = getLayerPriority(a);
|
|
const pb = getLayerPriority(b);
|
|
if (pa !== pb) return pa - pb;
|
|
|
|
const za = parseZIndex(a.styles?.zIndex);
|
|
const zb = parseZIndex(b.styles?.zIndex);
|
|
if (za !== zb) return za - zb;
|
|
const ya = a.rect?.y || 0;
|
|
const yb = b.rect?.y || 0;
|
|
if (Math.abs(ya - yb) > 2000) return ya - yb;
|
|
return (a.__importOrder || 0) - (b.__importOrder || 0);
|
|
});
|
|
}
|
|
|
|
function createLayerFrame(name, width, height) {
|
|
const layer = app.createFrame();
|
|
layer.name = name;
|
|
layer.x = 0;
|
|
layer.y = 0;
|
|
setSize(layer, width, height);
|
|
layer.fills = [];
|
|
if ("clipsContent" in layer) {
|
|
layer.clipsContent = true;
|
|
}
|
|
return layer;
|
|
}
|
|
|
|
async function renderCaptureNode(captureNode, parent, rootRect, assets, stats, scale) {
|
|
const renderRect = getRenderRect(captureNode);
|
|
if (!renderRect) return null;
|
|
if (renderRect.width <= 0 || renderRect.height <= 0) return null;
|
|
|
|
if (shouldUseClipWrapper(captureNode)) {
|
|
const wrapper = app.createFrame();
|
|
wrapper.name = `${captureNode.name || captureNode.tag || "Layer"} clip`;
|
|
setPosition(wrapper, captureNode.visibleRect, rootRect, scale);
|
|
wrapper.fills = [];
|
|
if ("clipsContent" in wrapper) {
|
|
wrapper.clipsContent = true;
|
|
}
|
|
|
|
const node = createNodeForCapture(captureNode);
|
|
node.name = captureNode.name || captureNode.tag || "Layer";
|
|
setPosition(node, captureNode.rect, captureNode.visibleRect, scale);
|
|
applyVisualStyles(node, captureNode, assets, stats, scale);
|
|
if (applyAutoLayout(node, captureNode, scale) && stats) {
|
|
stats.layoutFrames += 1;
|
|
}
|
|
applyTransform(node, captureNode);
|
|
wrapper.appendChild(node);
|
|
parent.appendChild(wrapper);
|
|
|
|
if (stats) {
|
|
stats.nodesCreated += 2;
|
|
stats.nodesClipped += 1;
|
|
}
|
|
return wrapper;
|
|
}
|
|
|
|
const node = createNodeForCapture(captureNode);
|
|
node.name = captureNode.name || captureNode.tag || "Layer";
|
|
setPosition(node, renderRect, rootRect, scale);
|
|
if (captureNode.clipped && stats) stats.nodesClipped += 1;
|
|
|
|
if (captureNode.type === "TEXT") {
|
|
applyTextStyles(node, captureNode, scale);
|
|
expandTextBounds(node, captureNode, renderRect, scale);
|
|
} else {
|
|
applyVisualStyles(node, captureNode, assets, stats, scale);
|
|
if (applyAutoLayout(node, captureNode, scale) && stats) {
|
|
stats.layoutFrames += 1;
|
|
}
|
|
}
|
|
applyTransform(node, captureNode);
|
|
|
|
parent.appendChild(node);
|
|
stats.nodesCreated += 1;
|
|
|
|
return node;
|
|
}
|
|
|
|
async function importCapture(data) {
|
|
if (!data || (data.format !== "pixso-design-capture" && data.schema !== "web-to-pixso-capture")) {
|
|
throw new Error("不是有效的 Web to Pixso 采集文件");
|
|
}
|
|
|
|
await loadDefaultFont();
|
|
|
|
const page = getPage();
|
|
if (!page) {
|
|
throw new Error("没有找到当前 Pixso 页面");
|
|
}
|
|
|
|
const frame = app.createFrame();
|
|
const sourceWidth = Math.max(data.source?.selection?.width || data.canvas?.width || data.source?.actualViewportWidth || data.source?.requestedViewportWidth || data.source?.viewport?.width || 1440, 1);
|
|
const scale = 1;
|
|
const width = Math.min(Math.max(sourceWidth, 1), 20000);
|
|
const height = Math.min(Math.max(data.canvas?.height || data.source?.selection?.height || data.source?.document?.height || 900, 1), 30000);
|
|
frame.name = data.source?.title || "Web Capture";
|
|
frame.x = 0;
|
|
frame.y = 0;
|
|
setSize(frame, width, height);
|
|
frame.fills = [rgbToPaint(data.canvas?.backgroundColor, { r: 1, g: 1, b: 1, a: 1 })];
|
|
if ("clipsContent" in frame) {
|
|
frame.clipsContent = true;
|
|
}
|
|
|
|
page.appendChild(frame);
|
|
|
|
const rootRect = {
|
|
x: data.canvas?.x || data.source?.selection?.x || 0,
|
|
y: data.canvas?.y || data.source?.selection?.y || 0,
|
|
width: sourceWidth,
|
|
height: data.canvas?.height || data.source?.selection?.height || data.source?.document?.height || 900
|
|
};
|
|
const stats = {
|
|
nodesCreated: 0,
|
|
imageCandidates: 0,
|
|
imagesImported: 0,
|
|
imageFailures: 0,
|
|
nodesSkipped: 0,
|
|
nodesClipped: 0,
|
|
layoutGroups: 0,
|
|
layoutFrames: 0,
|
|
transparentWrappers: 0
|
|
};
|
|
|
|
const fallbackLayerName = `01 兜底截图层 / 对比底图-${Math.round(sourceWidth)}px`;
|
|
const fallbackLayer = createLayerFrame(fallbackLayerName, width, height);
|
|
const editableLayer = createLayerFrame("02 可编辑元素层", width, height);
|
|
const textLayer = createLayerFrame("03 文字编辑层", width, height);
|
|
frame.appendChild(fallbackLayer);
|
|
frame.appendChild(editableLayer);
|
|
frame.appendChild(textLayer);
|
|
|
|
const consumedLayoutIds = new Set();
|
|
if (data.nodes) {
|
|
const layoutRoots = sortLayoutRoots(collectLayoutRoots(data.nodes));
|
|
for (const layoutRoot of layoutRoots) {
|
|
try {
|
|
await renderGroupedSubtree(layoutRoot, editableLayer, rootRect, data.assets || {}, stats, scale);
|
|
markSubtreeIds(layoutRoot, consumedLayoutIds);
|
|
} catch (error) {
|
|
console.warn("[Web to Pixso] Layout group import skipped", error);
|
|
stats.nodesSkipped += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data.nodes) {
|
|
const renderableNodes = sortRenderableNodes(collectRenderableNodes(data.nodes));
|
|
for (const nodeData of renderableNodes) {
|
|
try {
|
|
if (!isRasterFallbackNode(nodeData) && consumedLayoutIds.has(nodeData.id)) {
|
|
continue;
|
|
}
|
|
const layerGroup = getLayerGroup(nodeData);
|
|
const parentLayer = layerGroup === "comparison"
|
|
? fallbackLayer
|
|
: layerGroup === "text"
|
|
? textLayer
|
|
: editableLayer;
|
|
await renderCaptureNode(nodeData, parentLayer, rootRect, data.assets || {}, stats, scale);
|
|
} catch (error) {
|
|
console.warn("[Web to Pixso] Layer import skipped", error);
|
|
stats.nodesSkipped += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ("selection" in app) {
|
|
app.selection = [frame];
|
|
} else if (app.state) {
|
|
app.state.currentSelection = [frame];
|
|
}
|
|
|
|
if (app.viewport?.scrollAndZoomIntoView) {
|
|
app.viewport.scrollAndZoomIntoView([frame]);
|
|
} else if (app.viewport?.zoomToFit) {
|
|
app.viewport.zoomToFit();
|
|
}
|
|
|
|
return {
|
|
nodeCount: data.diagnostics?.nodeCount || 0,
|
|
assetCount: data.diagnostics?.assetCount || 0,
|
|
importWidth: width,
|
|
...stats
|
|
};
|
|
}
|
|
|
|
function postStatus(text, status = "info") {
|
|
app.ui?.postMessage?.({ text, status });
|
|
}
|
|
|
|
app.showUI(__html__, { width: PLUGIN_WIDTH, height: PLUGIN_HEIGHT });
|
|
|
|
app.ui.onmessage = async message => {
|
|
if (message?.type === "CLOSE") {
|
|
app.closePlugin?.();
|
|
return;
|
|
}
|
|
|
|
if (message?.type !== "IMPORT_CAPTURE") return;
|
|
|
|
try {
|
|
postStatus("正在创建画板和图层...", "loading");
|
|
const stats = await importCapture(message.data, message.options || {});
|
|
const text = `导入完成:创建 ${stats.nodesCreated}/${stats.nodeCount} 层,布局组 ${stats.layoutGroups} 个,自动布局 ${stats.layoutFrames} 组,裁剪 ${stats.nodesClipped} 层,跳过 ${stats.nodesSkipped} 层,宽度 ${stats.importWidth}px,图片 ${stats.imagesImported}/${stats.imageCandidates}`;
|
|
postStatus(text, "success");
|
|
notify(text, "success");
|
|
} catch (error) {
|
|
const text = error.message || String(error);
|
|
postStatus(text, "error");
|
|
notify(text, "error");
|
|
}
|
|
};
|