Search
Loading...
Skip to content

Combine Shapes

Combine multiple shapes using boolean operations to create custom compound designs programmatically.

8 mins
estimated time
Download
StackBlitz
GitHub

CE.SDK provides four boolean operations for combining shapes: Union, Difference, Intersection, and XOR. These operations work with graphic blocks and text blocks, allowing you to build complex designs from simple primitives.

This guide covers checking combinability, applying the four boolean operations, understanding fill inheritance, and troubleshooting common combination issues.

Understanding Boolean Operations#

CE.SDK offers four boolean operations for combining blocks into new shapes. Each operation applies geometric transformations to create unique compound designs.

Union merges all blocks into a single shape, adding their areas together. Difference subtracts overlapping areas from a base block, creating cutouts. Intersection keeps only the overlapping regions. XOR removes overlaps while preserving non-overlapping parts.

The operations use engine.block.combine(ids, operation) where ids is an array of blocks and operation is one of: 'Union', 'Difference', 'Intersection', or 'XOR'.

Combining blocks with the 'Union', 'Intersection', or 'XOR' operation results in a new block whose fill is that of the top-most block. The operation is applied pair-wise from the top-most block to the bottom-most block.

Combining blocks with the 'Difference' operation results in a new block whose fill is that of the bottom-most block. The operation is applied pair-wise from the bottom-most block to the top-most block.

The combined blocks will be destroyed if the 'lifecycle/destroy' scope is enabled.

Checking Combinability#

Before combining blocks, verify they can be combined using engine.block.isCombinable(ids). Only graphic blocks and text blocks with the 'lifecycle/duplicate' scope enabled can be combined.

// Check if blocks can be combined before attempting operations
const canCombineUnion = engine.block.isCombinable([
unionCircle1,
unionCircle2,
unionCircle3,
]);

The check returns true if all blocks meet the requirements. Attempting to combine incompatible blocks will fail.

Combining with Union#

Union merges multiple shapes into one compound outline. The result inherits the fill from the top-most block in the z-order.

// Merge three circles using Union operation
if (canCombineUnion) {
engine.block.combine(
[unionCircle1, unionCircle2, unionCircle3],
'Union',
);
}

We create three circles with different colors. Union combines them into a single block with the blue fill (from the top-most circle).

Use Union for merging logos, creating compound icons, and building complex shapes from simple primitives.

Combining with Difference#

Difference subtracts overlapping shapes from a base block, creating cutout effects. The result inherits the fill from the bottom-most block.

// Create punch-out text effect using Difference operation
if (canCombineDiff) {
engine.block.combine([imageBlock, textBlock], 'Difference');
}

We position an image as the bottom block and text above it. Difference removes the text shape from the image, creating a punch-out effect where the text was.

Use Difference for text punch-outs, logo cutouts, and mask effects. Ensure the base block is at the bottom using engine.block.sendToBack().

Combining with Intersection#

Intersection keeps only the overlapping areas of all blocks. The result inherits the fill from the bottom-most block.

// Extract overlapping area using Intersection operation
if (canCombineIntersect) {
engine.block.combine([intersectCircle1, intersectCircle2], 'Intersection');
}

We create two overlapping circles. Intersection extracts only the area where they overlap, discarding the rest.

Use Intersection for lens effects, overlapping patterns, and extracting geometric intersections.

Combining with XOR#

XOR (exclusive OR) keeps non-overlapping parts while removing intersections, creating an exclusion or donut effect. The result inherits the fill from the top-most block.

// Create exclusion shape using XOR operation
if (canCombineXor) {
engine.block.combine([xorCircle1, xorCircle2], 'XOR');
}

We create two overlapping circles. XOR removes the overlapping area while preserving the non-overlapping parts.

Use XOR for donut shapes, exclusion patterns, and inverted overlaps.

Understanding Fill Inheritance#

Combined blocks inherit properties from a prioritized block based on the operation.

Union, Intersection, XOR: The new block inherits the fill from the top-most block. Operations are applied pair-wise from highest to lowest sort order.

Difference: The new block inherits the fill from the bottom-most block. Operations are applied pair-wise from lowest to highest sort order.

Original blocks are destroyed after combination if the 'lifecycle/destroy' scope is enabled. Control which fill is inherited by reordering blocks with engine.block.bringToFront() and engine.block.sendToBack().

Scope Requirements#

Combining blocks requires specific scopes:

'lifecycle/duplicate': Required on all blocks. Checked by engine.block.isCombinable(). If missing, combination fails.

'lifecycle/destroy': Required for destroying original blocks. If disabled, original blocks remain after combination.

Check scopes with engine.block.isScopeEnabled(id, scope) and enable with engine.block.setScopeEnabled(id, scope, true).

Troubleshooting#

Combination Fails Silently#

Verify blocks are combinable using engine.block.isCombinable(ids) before attempting operations. Only graphic blocks and text blocks can be combined. Check that the 'lifecycle/duplicate' scope is enabled on all blocks.

Original Blocks Not Destroyed#

Ensure the 'lifecycle/destroy' scope is enabled on input blocks. If disabled, blocks remain after combination. Check with engine.block.isScopeEnabled(id, 'lifecycle/destroy').

Wrong Fill on Result#

For Union/Intersection/XOR, the top-most block’s fill is inherited. For Difference, the bottom-most block’s fill is inherited. Reorder blocks before combining using engine.block.bringToFront() or engine.block.sendToBack().

Unexpected Shape Result#

Boolean operations are applied pair-wise in specific order. Union/Intersection/XOR start with the highest sort order (top-most). Difference starts with the lowest sort order (bottom-most). Control order with z-order methods.

Full Code#

Here’s the complete implementation showing all four boolean operations in a headless Node.js environment:

import CreativeEngine from '@cesdk/node';
import { config } from 'dotenv';
import { writeFileSync, mkdirSync, existsSync } from 'fs';
// Load environment variables
config();
// Initialize CE.SDK engine in headless mode
const engine = await CreativeEngine.init({
// license: process.env.CESDK_LICENSE, // Optional (trial mode available)
});
try {
const outputDir = './output';
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
// Create a design scene
engine.scene.create('VerticalStack', {
page: { size: { width: 400, height: 300 } },
});
const page = engine.block.findByType('page')[0];
// Create three circles for union demonstration
const circle1 = engine.block.create('graphic');
engine.block.setShape(circle1, engine.block.createShape('//ly.img.ubq/shape/ellipse'));
const fill1 = engine.block.createFill('color');
engine.block.setColor(fill1, 'fill/color/value', { r: 1.0, g: 0.4, b: 0.4, a: 1.0 });
engine.block.setFill(circle1, fill1);
engine.block.setWidth(circle1, 120);
engine.block.setHeight(circle1, 120);
engine.block.setPositionX(circle1, 100);
engine.block.setPositionY(circle1, 80);
engine.block.appendChild(page, circle1);
const circle2 = engine.block.create('graphic');
engine.block.setShape(circle2, engine.block.createShape('//ly.img.ubq/shape/ellipse'));
const fill2 = engine.block.createFill('color');
engine.block.setColor(fill2, 'fill/color/value', { r: 0.4, g: 1.0, b: 0.4, a: 1.0 });
engine.block.setFill(circle2, fill2);
engine.block.setWidth(circle2, 120);
engine.block.setHeight(circle2, 120);
engine.block.setPositionX(circle2, 180);
engine.block.setPositionY(circle2, 100);
engine.block.appendChild(page, circle2);
const circle3 = engine.block.create('graphic');
engine.block.setShape(circle3, engine.block.createShape('//ly.img.ubq/shape/ellipse'));
const fill3 = engine.block.createFill('color');
engine.block.setColor(fill3, 'fill/color/value', { r: 0.4, g: 0.4, b: 1.0, a: 1.0 });
engine.block.setFill(circle3, fill3);
engine.block.setWidth(circle3, 120);
engine.block.setHeight(circle3, 120);
engine.block.setPositionX(circle3, 140);
engine.block.setPositionY(circle3, 140);
engine.block.appendChild(page, circle3);
// Check combinability and perform Union
if (engine.block.isCombinable([circle1, circle2, circle3])) {
engine.block.combine([circle1, circle2, circle3], 'Union');
}
// Export the result
const blob = await engine.block.export(page, { mimeType: 'image/png' });
const buffer = Buffer.from(await blob.arrayBuffer());
writeFileSync(`${outputDir}/combine-union-result.png`, buffer);
console.log('✓ Exported Union result to output/combine-union-result.png');
} finally {
// Always dispose the engine to free resources
engine.dispose();
}

API Reference#

MethodCategoryPurpose
engine.block.isCombinable(ids)ValidationCheck if blocks can be combined
engine.block.combine(ids, op)CombinationPerform boolean operation on blocks
engine.block.create('graphic')CreationCreate graphic block for shapes
engine.block.create('text')CreationCreate text block
engine.block.createShape(type)ShapesCreate shape (ellipse, rect, etc.)
engine.block.setShape(id, shape)ShapesApply shape to graphic block
engine.block.createFill(type)FillsCreate fill (color, image, etc.)
engine.block.setFill(id, fill)FillsApply fill to block
engine.block.setPositionX/Y(id, val)TransformPosition blocks before combining
engine.block.setWidth/Height(id, val)TransformSize blocks before combining
engine.block.appendChild(parent, child)HierarchyAdd blocks to scene
engine.block.isScopeEnabled(id, scope)ScopeCheck if scope is enabled
engine.block.setScopeEnabled(id, scope, enabled)ScopeEnable/disable scope
engine.block.bringToFront(id)OrderControl stacking order
engine.block.sendToBack(id)OrderControl stacking order