From 7aece916045f7cdc9809b4b2a76b8c891dc659e4 Mon Sep 17 00:00:00 2001 From: zhangyan3 Date: Mon, 16 Mar 2026 16:51:45 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D`findImageNodes`?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E9=80=BB=E8=BE=91=E5=8F=8A=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/nodes/process/structure/index.ts | 26 +++++++++++-- src/tools/figma-tool/images.ts | 58 +++++++++++++++------------- 2 files changed, 54 insertions(+), 30 deletions(-) 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 **/ From 182364cd48b69af9c659680ec6c7a7d6b7681e55 Mon Sep 17 00:00:00 2001 From: zhangyan3 Date: Mon, 16 Mar 2026 16:51:45 +0800 Subject: [PATCH 2/2] fix: findImageNodes function & component export --- src/nodes/process/structure/index.ts | 26 +++++++++++-- src/tools/figma-tool/images.ts | 58 +++++++++++++++------------- 2 files changed, 54 insertions(+), 30 deletions(-) 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 **/