// Shows the UI figma.showUI(__html__, { width: 400, height: 600 }); // Helper to log to UI function log(msg, level = 'info') { figma.ui.postMessage({ type: 'log', message: msg, level: level }); } // Helper to find a page or create it function getOrCreatePage(name) { let page = figma.root.children.find(p => p.name === name); if (!page) { page = figma.createPage(); page.name = name; log(`Created new page: "${name}"`, 'info'); } return page; } // Helper to find a component by name (recursive search or search in local components) // Note: In a real plugin, you might need to search libraries, but here we assume local components // or components available in the file. async function findComponent(name) { // Search local components first // This is a simplified search. In a large file, this might be slow. // We can optimize by caching components map. const localComponents = figma.root.findAll(n => n.type === 'COMPONENT' || n.type === 'COMPONENT_SET'); for (const node of localComponents) { if (node.name === name) return node; if (node.type === 'COMPONENT_SET') { const variant = node.children.find(c => c.name === name); // This might need robust variant matching if (variant) return variant; } } return null; } // Helper to load image async function createImagePaint(imageBytes, scaleMode = 'FILL') { const image = figma.createImage(imageBytes); return { type: 'IMAGE', scaleMode: scaleMode, imageHash: image.hash, }; } async function processRows(mode, data, images) { const { rows, metadata, headers } = data; log(`Starting ${mode === 'create' ? 'Automation' : 'Image Replacement'}...`, 'info'); log(`Received ${rows.length} rows and ${Object.keys(images).length} images.`, 'info'); const pageY = { 'Ports': 0, 'ATF': 0, 'BTF': 0, 'BTF_Banner': 0 }; const SPACING = 50; // Store created/updated instances to export later const processedInstances = []; // Helper to generate name // Format: Cycle_CodeName_ProductName_Color_Variation_FeatureAssociation_BTF/ATF_Dimension.PNG const generateName = (row, type, dimensionOverride = null) => { const parts = []; // 1. Cycle if (metadata.cycle) parts.push(metadata.cycle); // 2. CodeName if (metadata.codeName) parts.push(metadata.codeName); // 3. ProductName if (metadata.productName) parts.push(metadata.productName); // 4. Color if (metadata.color) parts.push(metadata.color); // 5. Variation // "if there’s data on C column... add this; if not, omit" const label = row[0]; // FeatureAssociation const variation = row[2]; if (variation) { parts.push(variation); } // 6. FeatureAssociation (Col A) if (label) parts.push(label); // 7. BTF/ATF // "id the asset being exported Is from data on column E, it should be “ATF”; all others, “BTF”" parts.push(type === 'ATF' ? 'ATF' : 'BTF'); // 8. Dimension if (label === 'Ports') { parts.push('2000x1000'); } else if (dimensionOverride) { parts.push(dimensionOverride); } else { parts.push('UnknownDim'); } return parts.join('_'); }; for (let i = 0; i < rows.length; i++) { const row = rows[i]; // New Column Mapping: // New Column Mapping (Indices 0-11 based on request): // A=0: Label (Feature Number) // C=2: Variation // F=5: ATF Trigger // G=6: BTF Trigger // H=7: BTF_Banner Trigger // I=8: Image Filename // J=9: Screen Replacement Filename // K=10: Overlay Component // L=11: Template const label = row[0]; const variation = row[2]; const colATF = row[5]; // ATF (Col F) const colBTF = row[6]; // BTF (Col G) const colBTFBanner = row[7]; // BTF_Banner (Col H) const imageFilename = row[8]; // Image (Col I) const screenRepFilename = row[9]; // Screen Replacement (Col J) const overlayName = row[10]; // Overlay (Col K) const template = row[11]; // Template (Col L) // Helper to find image data const getImageData = (name) => { if (!name) return null; let key = Object.keys(images).find(k => k === name); if (!key) key = Object.keys(images).find(k => k.includes(name)); return key ? images[key] : null; }; const imageBytes = getImageData(imageFilename); const screenRepBytes = getImageData(screenRepFilename); // If replacing images, we generally need at least one image to do something, // or we might be just updating overlays? // User: "only with the images provided... If there's a screen replacement... grab file name". // If NO main image but YES screen replacement, we proceed? Yes. if (mode === 'replace' && !imageBytes && !screenRepBytes) { continue; } // Determine Scale Mode // "If template (Col L) is RS or LF -> FIT. All others -> FILL" let scaleMode = 'FILL'; if (template === 'RS' || template === 'LF') { scaleMode = 'FIT'; } // Helper to process a row item const processItem = async (pageName, componentBaseName, type, dimension, isPorts = false) => { const page = getOrCreatePage(pageName); let targetInstance = null; let instanceName = ""; if (isPorts) { // Ports Logic: Find existing "PB3.0_Ports" (or already renamed one) const generatedName = generateName(row, 'BTF'); // 1. Try to find if we already processed/renamed it targetInstance = page.findOne(n => n.name === generatedName); // 2. If not, find the template "PB3.0_Ports" if (!targetInstance) { targetInstance = page.findOne(n => n.name === 'PB3.0_Ports'); } if (targetInstance) { if (mode === 'create') { targetInstance.name = generatedName; // Rename for export consistency log(`Found/Renamed PB3.0_Ports: ${generatedName}`, 'info'); } else { log(`Found PB3.0_Ports: ${targetInstance.name}`, 'info'); } processedInstances.push(targetInstance); } else { log(`Warning: Target 'PB3.0_Ports' not found on page ${pageName}`, 'warning'); return; } instanceName = targetInstance.name; } else { // Standard Logic for other items instanceName = generateName(row, type, dimension); if (mode === 'replace') { // Find existing instance/frame/group by name targetInstance = page.findOne(n => n.name === instanceName); if (targetInstance) { log(`Found existing asset: ${instanceName}`, 'info'); processedInstances.push(targetInstance); } else { log(`Warning: Asset "${instanceName}" not found for replacement.`, 'warning'); return; } } else { // Create Mode const component = await findComponent(componentBaseName); if (!component) { log(`Error: Component "${componentBaseName}" not found.`, 'error'); return; } let instance = component.createInstance(); instance = instance.detachInstance(); instance.name = instanceName; log(`Created instance: ${instanceName}`, 'success'); // Fill Image if (imageBytes) { const imageNode = instance.findOne(n => n.name === "Image"); if (imageNode) { imageNode.fills = [await createImagePaint(imageBytes, scaleMode)]; log(` - Filled image (Mode: ${scaleMode})`, 'info'); } } // Overlay Logic if (overlayName) { const overlayComp = await findComponent(overlayName); if (overlayComp) { let overlayInst = overlayComp.createInstance(); overlayInst = overlayInst.detachInstance(); const imageNode = instance.findOne(n => n.name === "Image"); if (imageNode) { // ... (Overlay placement logic) ... let currentNode = imageNode; let dx = 0; let dy = 0; while (currentNode && currentNode.id !== instance.id) { dx += currentNode.x; dy += currentNode.y; currentNode = currentNode.parent; } // Scaling const maxW = instance.width * 0.20; if (overlayInst.width > maxW) { const scale = maxW / overlayInst.width; overlayInst.resize(maxW, overlayInst.height * scale); } // Centering const centerX = dx + (imageNode.width - overlayInst.width) / 2; const centerY = dy + (imageNode.height - overlayInst.height) / 2; overlayInst.x = centerX; overlayInst.y = centerY; // Group page.appendChild(instance); page.appendChild(overlayInst); const group = figma.group([instance, overlayInst], page); group.name = instanceName; group.y = pageY[pageName]; pageY[pageName] += group.height + SPACING; processedInstances.push(group); log(` - Added overlay and grouped`, 'success'); return; // Done for this item } } } // If no overlay page.appendChild(instance); instance.y = pageY[pageName]; pageY[pageName] += instance.height + SPACING; processedInstances.push(instance); targetInstance = instance; } } // Update Main Image if (imageBytes && targetInstance) { // Search for "Image" layer. Might be direct child or inside a group/frame. const imageNode = targetInstance.findOne(n => n.name === "Image"); if (imageNode) { imageNode.fills = [await createImagePaint(imageBytes, scaleMode)]; // Center logic (Ports only - from previous code) if (isPorts && imageNode.parent && imageNode.parent.type === 'FRAME') { const parent = imageNode.parent; imageNode.x = (parent.width - imageNode.width) / 2; imageNode.y = (parent.height - imageNode.height) / 2; } log(` - Updated Main Image for ${instanceName}`, 'success'); } else { log(` - Warning: "Image" layer not found in ${instanceName}`, 'warning'); } } // Update Screen Replacement if (screenRepBytes && targetInstance) { // "place it on Figma on top of all the main image element" // Strategy: Find "Image" node. Create new Rectangle/Frame named "ScreenReplacement" // same size/pos as "Image" (or fitting?), insert it AFTER "Image" in parent so it renders on top. const imageNode = targetInstance.findOne(n => n.name === "Image"); if (imageNode) { const parent = imageNode.parent; if (parent) { // Check if we already have a replacement layer to update let screenLayer = parent.children.find(c => c.name === "ScreenReplacement"); if (!screenLayer) { // Create new screenLayer = figma.createRectangle(); screenLayer.name = "ScreenReplacement"; parent.appendChild(screenLayer); // Appends to end (top) // Match dimensions/position of Image node screenLayer.resize(imageNode.width, imageNode.height); screenLayer.x = imageNode.x; screenLayer.y = imageNode.y; // Ensure it is strictly above ImageNode in Z-order // (parent.appendChild puts it at top, which is fine, but let's be safe if other things exist) // If we want it *just* on top of Image, we could use insertChild. // But "on top of all the main image" implies potentially covering it. } screenLayer.fills = [await createImagePaint(screenRepBytes, "FIT")]; // Using FIT for screens usually? default to FIT or FILL? User didnt specify. FIT is safer for screens. log(` - Applied Screen Replacement for ${instanceName}`, 'success'); } } else { log(` - Warning: Cannot do Screen Replacement, "Image" node not found.`, 'warning'); } } // Handle Overlay (Only Create Mode usually, per previous logic "does not create instances" in replace) // If Create Mode AND Overlay exists if (mode === 'create' && overlayName && !isPorts) { // Ports didnt have overlay logic before const overlayComp = await findComponent(overlayName); if (overlayComp) { const overlayInst = overlayComp.createInstance(); const detachedOverlay = overlayInst.detachInstance(); // Detach per previous logic pattern // Place overlay logic (centering over "Image" node) const imageNode = targetInstance.findOne(n => n.name === "Image"); if (imageNode) { // Calculate position relative to Page is tricky if we don't group first. // But previous logic grouped them. // Let's adopt previous logic: Group [Instance, Overlay] // We need to calculate absolute positions? // Or just center overlay over image node? // Scaling: Max 20% of instance width const maxW = targetInstance.width * 0.20; if (detachedOverlay.width > maxW) { const scale = maxW / detachedOverlay.width; detachedOverlay.resize(maxW, detachedOverlay.height * scale); } // Find global pos of ImageNode relative to targetInstance // imageNode is inside targetInstance. let dx = 0; let dy = 0; let curr = imageNode; while (curr && curr.id !== targetInstance.id) { dx += curr.x; dy += curr.y; curr = curr.parent; } detachedOverlay.x = targetInstance.x + dx + (imageNode.width - detachedOverlay.width) / 2; detachedOverlay.y = targetInstance.y + dy + (imageNode.height - detachedOverlay.height) / 2; // Add to page page.appendChild(detachedOverlay); // Group const group = figma.group([targetInstance, detachedOverlay], page); group.name = instanceName; // Update processing list (remove individual, add group) const idx = processedInstances.indexOf(targetInstance); if (idx > -1) processedInstances[idx] = group; // Replace tracker log(` - Added Overlay: ${overlayName}`, 'success'); } } } }; // Dispatch processing based on columns log(`Processing Row ${i + 12}: ${label}`, 'info'); // 1. Ports if (label === 'Ports') { await processItem('PB3.0_Ports', 'PB3.0_Ports', 'BTF', null, true); } // 2. ATF (Col F) if (colATF) { let compName = ''; if (['RSC', 'LSC', 'B'].includes(template)) compName = 'ATF_B'; else if (template === 'LS') compName = 'ATF_LS'; else if (template === 'RS') compName = 'ATF_RS'; const dim = colATF; if (compName) await processItem('ATF', compName, 'ATF', dim); } // 3. BTF (Col G) if (colBTF) { let compName = ''; if (['RSC', 'LSC', 'B'].includes(template)) compName = 'BTF_B'; else if (template === 'LS') compName = 'BTF_LS'; else if (template === 'RS') compName = 'BTF_RS'; const dim = colBTF; if (compName) await processItem('BTF', compName, 'BTF', dim); } // 4. BTF_Banner (Col H) if (colBTFBanner) { let compName = ''; if (colBTFBanner === '1464x250') compName = 'BTFB_Thin'; else if (colBTFBanner === '1464x600') { if (template === 'B') compName = 'BTFB_B'; else if (template === 'LS') compName = 'BTFB_LS'; else if (template === 'RS') compName = 'BTFB_RS'; else if (template === 'LSC') compName = 'BTFB_LSC'; else if (template === 'RSC') compName = 'BTFB_RSC'; } const dim = colBTFBanner; if (compName) await processItem('BTF_Banner', compName, 'BTF', dim); } } // Save created instances ID for export (only if we want to track them) // In replace mode, we might want to track them too? figma.clientStorage.setAsync('lastCreatedInstanceIds', processedInstances.map(n => n.id)); figma.ui.postMessage({ type: 'complete' }); } figma.ui.onmessage = async (msg) => { if (msg.type === 'run-automation' || msg.type === 'replace-images') { const mode = msg.type === 'run-automation' ? 'create' : 'replace'; try { await processRows(mode, msg.data, msg.images); } catch (err) { log(`Error: ${err.message}`, 'error'); console.error(err); figma.ui.postMessage({ type: 'error', message: err.message }); } } if (msg.type === 'export-zip') { try { log('Preparing assets for export...', 'info'); // New Export Logic: Scan specific pages for assets // This allows exporting without running automation in the current session const targetPageNames = ['PB3.0_Ports', 'ATF', 'BTF', 'BTF_Banner']; const nodesToExport = []; for (const name of targetPageNames) { const page = figma.root.children.find(p => p.name === name); if (page) { // Get all top-level exportable nodes (Frames, Instances, Groups) const children = page.children.filter(node => node.visible && (node.type === 'FRAME' || node.type === 'INSTANCE' || node.type === 'GROUP' || node.type === 'COMPONENT') && node.name.toLowerCase() !== 'background reference' ); nodesToExport.push(...children); if (children.length > 0) { log(`Found ${children.length} assets on page "${name}"`, 'info'); } } } if (nodesToExport.length === 0) { log('No assets found on target pages (Ports, ATF, BTF, BTF_Banner).', 'warning'); return; } const exportData = []; for (const node of nodesToExport) { // Export as PNG // Using 2x scale as per previous logic const bytes = await node.exportAsync({ format: 'PNG', constraint: { type: 'SCALE', value: 2 } }); exportData.push({ name: `${node.name}.png`, bytes: bytes }); } figma.ui.postMessage({ type: 'export-ready', files: exportData }); log(`Sent ${exportData.length} files to UI for zipping.`, 'success'); } catch (err) { log(`Export Error: ${err.message}`, 'error'); } } };