Skip to content

Commit 19c50b3

Browse files
committed
fix: include BOOLEAN_OPERATION in SVG container collapse
Boolean operations (UNION, SUBTRACT, INTERSECT, EXCLUDE) always produce vector output but were missing from the SVG rollup logic, causing them to block parent containers from collapsing to IMAGE-SVG.
1 parent b5724ad commit 19c50b3

2 files changed

Lines changed: 62 additions & 4 deletions

File tree

src/extractors/built-in.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,13 @@ export const layoutOnly = [layoutExtractor];
207207

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

240242
if (
241-
(node.type === "FRAME" || node.type === "GROUP" || node.type === "INSTANCE") &&
243+
(node.type === "FRAME" ||
244+
node.type === "GROUP" ||
245+
node.type === "INSTANCE" ||
246+
node.type === "BOOLEAN_OPERATION") &&
242247
allChildrenAreSvgEligible &&
243248
!hasImageFillInChildren(node)
244249
) {

src/tests/tree-walker.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,59 @@ describe("extractFromDesign", () => {
105105
});
106106
});
107107

108+
describe("collapseSvgContainers", () => {
109+
it("collapses BOOLEAN_OPERATION nodes to IMAGE-SVG", async () => {
110+
const booleanOpNode = makeNode({
111+
id: "5:1",
112+
name: "Combined Shape",
113+
type: "BOOLEAN_OPERATION",
114+
booleanOperation: "UNION",
115+
children: [
116+
makeNode({ id: "5:2", name: "Circle", type: "ELLIPSE" }),
117+
makeNode({ id: "5:3", name: "Square", type: "RECTANGLE" }),
118+
],
119+
});
120+
121+
const { nodes } = await extractFromDesign([booleanOpNode], allExtractors, {
122+
afterChildren: collapseSvgContainers,
123+
});
124+
125+
expect(nodes).toHaveLength(1);
126+
expect(nodes[0].type).toBe("IMAGE-SVG");
127+
expect(nodes[0].children).toBeUndefined();
128+
});
129+
130+
it("collapses a frame containing a BOOLEAN_OPERATION to IMAGE-SVG", async () => {
131+
const frameWithBoolOp = makeNode({
132+
id: "6:1",
133+
name: "Icon Frame",
134+
type: "FRAME",
135+
children: [
136+
makeNode({
137+
id: "6:2",
138+
name: "Union",
139+
type: "BOOLEAN_OPERATION",
140+
booleanOperation: "UNION",
141+
children: [
142+
makeNode({ id: "6:3", name: "A", type: "RECTANGLE" }),
143+
makeNode({ id: "6:4", name: "B", type: "ELLIPSE" }),
144+
],
145+
}),
146+
],
147+
});
148+
149+
const { nodes } = await extractFromDesign([frameWithBoolOp], allExtractors, {
150+
afterChildren: collapseSvgContainers,
151+
});
152+
153+
// The BOOLEAN_OPERATION collapses to IMAGE-SVG first (bottom-up),
154+
// then the FRAME sees all children are SVG-eligible and collapses too.
155+
expect(nodes).toHaveLength(1);
156+
expect(nodes[0].type).toBe("IMAGE-SVG");
157+
expect(nodes[0].children).toBeUndefined();
158+
});
159+
});
160+
108161
describe("simplifyRawFigmaObject", () => {
109162
it("produces a complete SimplifiedDesign from a mock API response", async () => {
110163
const mockResponse = {

0 commit comments

Comments
 (0)