diff --git a/src/nodes/process/structure/index.ts b/src/nodes/process/structure/index.ts index c596244..03f0ed3 100644 --- a/src/nodes/process/structure/index.ts +++ b/src/nodes/process/structure/index.ts @@ -54,11 +54,31 @@ export const generateStructure = async (figma: FigmaFrameInfo) => { const jsonContent = extractJSON(structureResult); const parsedStructure = JSON.parse(jsonContent) as Protocol | Protocol[]; + // When AI returns a flat array of sections, wrap them in a root node + let protocol: Protocol; + if (Array.isArray(parsedStructure)) { + if (parsedStructure.length === 1) { + protocol = parsedStructure[0]!; + } else { + const rootName = figma.name || 'LandingPage'; + protocol = { + id: rootName, + data: { + name: rootName, + purpose: `Root layout composing ${parsedStructure.length} sections`, + elements: [], + layout: parsedStructure[0]?.data?.layout, + }, + children: parsedStructure, + }; + } + } else { + protocol = parsedStructure; + } + // Post-process structure: normalize names, populate elements, annotate paths logger.printInfoLog('Processing structure tree...'); - postProcessStructure(parsedStructure, frames); - - const protocol = (Array.isArray(parsedStructure) ? parsedStructure[0] : parsedStructure) as Protocol; + postProcessStructure(protocol, frames); // Extract component props and states for reusable components if (frames && protocol) { diff --git a/src/tools/figma-tool/images.ts b/src/tools/figma-tool/images.ts index 69ff459..0857636 100644 --- a/src/tools/figma-tool/images.ts +++ b/src/tools/figma-tool/images.ts @@ -103,38 +103,35 @@ export const findImageNodes = (nodes: FigmaFrameInfo[], absoluteBoundingBox?: Fi if (node.visible === false) { continue; } - // Rule 1: If node type is VECTOR, directly add to imageNodeIds - else if (node.type === 'VECTOR') { + + if (node.type === 'VECTOR') { imageNodes.push(assignImageObject(node, exportSvgIfNeeded(node, absoluteBoundingBox))); - } - // Rule 2: If node type is IMAGE or has imageRef, directly add to imageNodeIds - else if (isImageNode(node) || isImageNodeViaName(node)) { - if (isImageNode(node) || hasAnyImageNodeInDescendants(node)) { - imageNodes.push(assignImageObject(node, FigmaImageFormat.PNG)); - } else { - imageNodes.push(assignImageObject(node, exportSvgIfNeeded(node, absoluteBoundingBox))); - } } else if (isMaskNode(node)) { imageNodes.push(assignImageObject(node, FigmaImageFormat.PNG)); - } - // Rule 3: For nodes with children, check if any leaf descendant is a TEXT node with characters - else if (node.children && node.children.length > 0) { - const hasAnyTextNode = hasAnyTextNodeWithCharacters(node); - - if (hasAnyTextNode) { - const firstLevelChildrenHasImageNode = node.children.some((child: FigmaFrameInfo) => isImageNode(child)); - const firstLevelChildrenHasTextNode = node.children.some((child: FigmaFrameInfo) => isTextNode(child)); - if (firstLevelChildrenHasImageNode && !firstLevelChildrenHasTextNode) { - imageNodes.push(assignImageObject(node, FigmaImageFormat.PNG)); + } else if (isImageNode(node)) { + imageNodes.push(assignImageObject(node, FigmaImageFormat.PNG)); + } else if (node.children && node.children.length > 0) { + const hasText = hasAnyTextNodeWithCharacters(node); + const hasImage = hasAnyImageNodeInDescendants(node); + + if (hasText || hasImage) { + const childImageNodes = findImageNodes(node.children, absoluteBoundingBox); + imageNodes.push(...childImageNodes); + } else { + // Pure vector/shape group with no text or image descendants. + // If all children are basic shapes, treat as a single icon unit; + // otherwise recurse to decompose composite groups. + const isSimpleVectorIcon = node.children.every((child: FigmaFrameInfo) => isBasicShapeType(child.type)); + + if (isSimpleVectorIcon) { + imageNodes.push(assignImageObject(node, exportSvgIfNeeded(node, absoluteBoundingBox))); } else { - const childImageIds = findImageNodes(node.children, absoluteBoundingBox); - imageNodes.push(...childImageIds); + const childImageNodes = findImageNodes(node.children, absoluteBoundingBox); + imageNodes.push(...childImageNodes); } - } else if (hasAnyImageNodeInDescendants(node)) { - imageNodes.push(assignImageObject(node, FigmaImageFormat.PNG)); - } else { - imageNodes.push(assignImageObject(node, exportSvgIfNeeded(node, absoluteBoundingBox))); } + } else if (isImageNodeViaName(node)) { + imageNodes.push(assignImageObject(node, exportSvgIfNeeded(node, absoluteBoundingBox))); } } @@ -216,7 +213,14 @@ export const isImageNode = (node: FigmaFrameInfo): boolean => { /** Check if node is image node via name **/ export const isImageNodeViaName = (node: FigmaFrameInfo): boolean => { - return (node && node.name.toLowerCase().includes('img')) || node.name.toLowerCase().includes('image'); + if (!node) return false; + const name = node.name.toLowerCase(); + return name.includes('img') || name.includes('image'); +}; + +/** Check if a Figma node type is a basic shape (non-container leaf element) **/ +export const isBasicShapeType = (type: string): boolean => { + return ['VECTOR', 'BOOLEAN_OPERATION', 'LINE', 'ELLIPSE', 'RECTANGLE', 'STAR', 'REGULAR_POLYGON'].includes(type); }; /** Check if node is mask node **/