Skip to content
Merged
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
13 changes: 9 additions & 4 deletions src/extractors/built-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,13 @@ export const layoutOnly = [layoutExtractor];

/**
* Node types that can be exported as SVG images.
* When a FRAME, GROUP, or INSTANCE contains only these types, we can collapse it to IMAGE-SVG.
* Note: FRAME/GROUP/INSTANCE are NOT included here—they're only eligible if collapsed to IMAGE-SVG.
* When a FRAME, GROUP, INSTANCE, or BOOLEAN_OPERATION contains only these types, we can collapse
* it to IMAGE-SVG. BOOLEAN_OPERATION is included because it's both a collapsible container AND
* SVG-eligible as a child (boolean ops always produce vector output).
*/
export const SVG_ELIGIBLE_TYPES = new Set([
"IMAGE-SVG", // VECTOR nodes are converted to IMAGE-SVG, or containers that were collapsed
"BOOLEAN_OPERATION",
"STAR",
"LINE",
"ELLIPSE",
Expand All @@ -222,7 +224,7 @@ export const SVG_ELIGIBLE_TYPES = new Set([
/**
* afterChildren callback that collapses SVG-heavy containers to IMAGE-SVG.
*
* If a FRAME, GROUP, or INSTANCE contains only SVG-eligible children, the parent
* If a FRAME, GROUP, INSTANCE, or BOOLEAN_OPERATION contains only SVG-eligible children, the parent
* is marked as IMAGE-SVG and children are omitted, reducing payload size.
*
* @param node - Original Figma node
Expand All @@ -238,7 +240,10 @@ export function collapseSvgContainers(
const allChildrenAreSvgEligible = children.every((child) => SVG_ELIGIBLE_TYPES.has(child.type));

if (
(node.type === "FRAME" || node.type === "GROUP" || node.type === "INSTANCE") &&
(node.type === "FRAME" ||
node.type === "GROUP" ||
node.type === "INSTANCE" ||
node.type === "BOOLEAN_OPERATION") &&
allChildrenAreSvgEligible &&
!hasImageFillInChildren(node)
) {
Expand Down
53 changes: 53 additions & 0 deletions src/tests/tree-walker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,59 @@ describe("extractFromDesign", () => {
});
});

describe("collapseSvgContainers", () => {
it("collapses BOOLEAN_OPERATION nodes to IMAGE-SVG", async () => {
const booleanOpNode = makeNode({
id: "5:1",
name: "Combined Shape",
type: "BOOLEAN_OPERATION",
booleanOperation: "UNION",
children: [
makeNode({ id: "5:2", name: "Circle", type: "ELLIPSE" }),
makeNode({ id: "5:3", name: "Square", type: "RECTANGLE" }),
],
});

const { nodes } = await extractFromDesign([booleanOpNode], allExtractors, {
afterChildren: collapseSvgContainers,
});

expect(nodes).toHaveLength(1);
expect(nodes[0].type).toBe("IMAGE-SVG");
expect(nodes[0].children).toBeUndefined();
});

it("collapses a frame containing a BOOLEAN_OPERATION to IMAGE-SVG", async () => {
const frameWithBoolOp = makeNode({
id: "6:1",
name: "Icon Frame",
type: "FRAME",
children: [
makeNode({
id: "6:2",
name: "Union",
type: "BOOLEAN_OPERATION",
booleanOperation: "UNION",
children: [
makeNode({ id: "6:3", name: "A", type: "RECTANGLE" }),
makeNode({ id: "6:4", name: "B", type: "ELLIPSE" }),
],
}),
],
});

const { nodes } = await extractFromDesign([frameWithBoolOp], allExtractors, {
afterChildren: collapseSvgContainers,
});

// The BOOLEAN_OPERATION collapses to IMAGE-SVG first (bottom-up),
// then the FRAME sees all children are SVG-eligible and collapses too.
expect(nodes).toHaveLength(1);
expect(nodes[0].type).toBe("IMAGE-SVG");
expect(nodes[0].children).toBeUndefined();
});
});

describe("simplifyRawFigmaObject", () => {
it("produces a complete SimplifiedDesign from a mock API response", async () => {
const mockResponse = {
Expand Down
Loading