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(); } };