Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/nodes/process/structure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
58 changes: 31 additions & 27 deletions src/tools/figma-tool/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
}

Expand Down Expand Up @@ -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 **/
Expand Down