You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
4.8 KiB
132 lines
4.8 KiB
figma.showUI(__html__, { width: 320, height: 180 });
|
|
|
|
const NODE_LIMIT = 500;
|
|
|
|
async function runExport() {
|
|
const selection = figma.currentPage.selection;
|
|
|
|
if (selection.length === 0) {
|
|
figma.ui.postMessage({ type: 'error', message: 'Please select a frame or node.' });
|
|
return;
|
|
}
|
|
|
|
let nodeCount = 0;
|
|
function countNodes(node: SceneNode) {
|
|
nodeCount++;
|
|
if ("children" in node) {
|
|
for (const child of node.children) countNodes(child);
|
|
}
|
|
}
|
|
|
|
for (const node of selection) countNodes(node);
|
|
|
|
if (nodeCount > NODE_LIMIT) {
|
|
figma.ui.postMessage({ type: 'error', message: `Selection exceeds limit. Selected: ${nodeCount}. Limit: ${NODE_LIMIT}.` });
|
|
return;
|
|
}
|
|
|
|
let markdownOutput = "# AI DATA EXTRACTION GUIDE\n\n";
|
|
markdownOutput += "This archive contains structural design data and visual assets extracted from Figma specifically for machine learning interpretation.\n\n";
|
|
markdownOutput += "## Structural Architecture\n";
|
|
markdownOutput += "- data.md: This document contains the hierarchical tree of design nodes, text strings, and layout properties.\n";
|
|
markdownOutput += "- Folders: Named after the top-level selected nodes, containing the associated PNG and SVG image files.\n\n";
|
|
markdownOutput += "## Interpretation Rules for AI\n";
|
|
markdownOutput += "1. Tree Hierarchy: Indentation levels indicate parent-child node relationships.\n";
|
|
markdownOutput += "2. Asset Mapping: Visual exports (PNG and SVG formats) are located inside the corresponding top-level folder. Nodes are exported visually if they contain photographic fills or child text layers requiring structural context.\n";
|
|
markdownOutput += "3. Text Handling: Independent text nodes skip visual image export because their complete textual data is captured inline within this document.\n";
|
|
markdownOutput += "4. Property Metadata: The Raw Properties object provides precise dimensions, positions, fills, and styling definitions parsed directly from the Figma canvas API.\n\n";
|
|
markdownOutput += "--- \n\n";
|
|
markdownOutput += "## Hierarchical Tree Data\n\n";
|
|
|
|
const exportedImages: Array<{ folder: string; name: string; data: Uint8Array }> = [];
|
|
const allFolders: string[] = [];
|
|
|
|
function getSafeName(node: SceneNode): string {
|
|
let cleanName = node.name.replace(/[^a-z0-9]/gi, '_');
|
|
if (cleanName.length > 30) {
|
|
cleanName = cleanName.substring(0, 30);
|
|
}
|
|
const cleanId = node.id.replace(/[^a-z0-9]/gi, '_');
|
|
return `${cleanName}_${cleanId}`;
|
|
}
|
|
|
|
function hasText(node: SceneNode): boolean {
|
|
if (node.type === "TEXT") return true;
|
|
if ("children" in node) {
|
|
for (const child of node.children) {
|
|
if (hasText(child)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function traverseNode(node: SceneNode, depth: number, currentFolderPath: string) {
|
|
allFolders.push(currentFolderPath);
|
|
|
|
const indent = " ".repeat(depth);
|
|
markdownOutput += `${indent}- Name: ${node.name} (Type: ${node.type}, Path: ${currentFolderPath})\n`;
|
|
|
|
if (node.type === "TEXT") {
|
|
markdownOutput += `${indent} - Text Content: ${node.characters}\n`;
|
|
}
|
|
|
|
const rawProps = {
|
|
id: node.id,
|
|
width: node.width,
|
|
height: node.height,
|
|
x: node.x,
|
|
y: node.y,
|
|
fills: "fills" in node ? node.fills : null,
|
|
strokes: "strokes" in node ? node.strokes : null,
|
|
effects: "effects" in node ? node.effects : null,
|
|
opacity: "opacity" in node ? node.opacity : null,
|
|
rotation: "rotation" in node ? node.rotation : null
|
|
};
|
|
|
|
markdownOutput += `${indent} - Raw Properties: ${JSON.stringify(rawProps)}\n`;
|
|
|
|
const isTextNode = node.type === "TEXT";
|
|
|
|
if (!isTextNode) {
|
|
try {
|
|
const pngBuffer = await node.exportAsync({ format: 'PNG' });
|
|
const safeNodeName = getSafeName(node);
|
|
exportedImages.push({ folder: currentFolderPath, name: `${safeNodeName}.png`, data: pngBuffer });
|
|
|
|
const nodeHasText = hasText(node);
|
|
if (!nodeHasText) {
|
|
const svgBuffer = await node.exportAsync({ format: 'SVG' });
|
|
exportedImages.push({ folder: currentFolderPath, name: `${safeNodeName}.svg`, data: svgBuffer });
|
|
}
|
|
} catch (e) {
|
|
console.error(`Failed to export ${node.name}`, e);
|
|
}
|
|
}
|
|
|
|
if ("children" in node) {
|
|
for (const child of node.children) {
|
|
const childSafeName = getSafeName(child);
|
|
const childFolderPath = `${currentFolderPath}/children/${childSafeName}`;
|
|
await traverseNode(child, depth + 1, childFolderPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const node of selection) {
|
|
const safeFolderName = getSafeName(node);
|
|
await traverseNode(node, 0, safeFolderName);
|
|
}
|
|
|
|
figma.ui.postMessage({
|
|
type: 'export',
|
|
markdown: markdownOutput,
|
|
images: exportedImages,
|
|
folders: allFolders
|
|
});
|
|
}
|
|
|
|
figma.ui.onmessage = (msg) => {
|
|
if (msg.type === 'run-extraction') {
|
|
runExport();
|
|
}
|
|
};
|