Search
Loading...
Skip to content

Combine Shapes

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

Boolean operations demonstration showing Union, Difference, Intersection, and XOR

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,
]);
const canCombineDiff = engine.block.isCombinable([imageBlock, textBlock]);
const canCombineIntersect = engine.block.isCombinable([
intersectCircle1,
intersectCircle2,
]);
const canCombineXor = engine.block.isCombinable([xorCircle1, xorCircle2]);

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.

// Combine 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 effect using Difference operation
// Ensure image is at the bottom (will be the base block)
if (canCombineDiff) {
engine.block.sendToBack(imageBlock);
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 pattern 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:

import CreativeEditorSDK from '@cesdk/cesdk-js';
const config = {
license: 'YOUR_CESDK_LICENSE_KEY',
callbacks: {
onUpload: 'local'
}
};
const cesdk = await CreativeEditorSDK.create('#cesdk_container', config);
await cesdk.addDefaultAssetSources();
await cesdk.addDemoAssetSources({ sceneMode: 'Design' });
await cesdk.createDesignScene();
const engine = cesdk.engine;
const page = engine.block.findByType('page')[0];
engine.block.setWidth(page, 800);
engine.block.setHeight(page, 600);
const pageWidth = engine.block.getWidth(page);
const pageHeight = engine.block.getHeight(page);
const quadrantWidth = pageWidth / 2;
const quadrantHeight = pageHeight / 2;
// Union: Combine three circles in top-left quadrant (20% larger, centered)
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, 96); // 80 * 1.2 = 96
engine.block.setHeight(circle1, 96);
engine.block.setPositionX(circle1, quadrantWidth / 2 - 48);
engine.block.setPositionY(circle1, quadrantHeight / 2 - 48);
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, 96); // 80 * 1.2 = 96
engine.block.setHeight(circle2, 96);
engine.block.setPositionX(circle2, quadrantWidth / 2 + 24);
engine.block.setPositionY(circle2, quadrantHeight / 2 - 48);
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); // 100 * 1.2 = 120
engine.block.setHeight(circle3, 120);
engine.block.setPositionX(circle3, quadrantWidth / 2 - 12);
engine.block.setPositionY(circle3, quadrantHeight / 2 - 12);
engine.block.appendChild(page, circle3);
// Check combinability and perform Union
if (engine.block.isCombinable([circle1, circle2, circle3])) {
const unionResult = engine.block.combine([circle1, circle2, circle3], 'Union');
}
// Difference: Create punch-out text effect in top-right quadrant
const imageWidth = 360; // 300 * 1.2 = 360
const imageHeight = 240; // 200 * 1.2 = 240
const imageBlock = engine.block.create('graphic');
engine.block.setShape(imageBlock, engine.block.createShape('//ly.img.ubq/shape/rect'));
const imageFill = engine.block.createFill('image');
engine.block.setString(imageFill, 'fill/image/imageFileURI',
'https://img.ly/static/ubq_samples/sample_1.jpg');
engine.block.setFill(imageBlock, imageFill);
engine.block.setWidth(imageBlock, imageWidth);
engine.block.setHeight(imageBlock, imageHeight);
engine.block.setPositionX(imageBlock, quadrantWidth + (quadrantWidth - imageWidth) / 2);
engine.block.setPositionY(imageBlock, (quadrantHeight - imageHeight) / 2);
engine.block.appendChild(page, imageBlock);
const textBlock = engine.block.create('text');
engine.block.replaceText(textBlock, 'CUTOUT');
engine.block.setFloat(textBlock, 'text/fontSize', 170);
const textWidth = 300;
const textHeight = 120;
engine.block.setWidth(textBlock, textWidth);
engine.block.setHeight(textBlock, textHeight);
// Center text on the image
engine.block.setPositionX(textBlock, quadrantWidth + (quadrantWidth - textWidth) / 2);
engine.block.setPositionY(textBlock, (quadrantHeight - textHeight) / 2);
engine.block.appendChild(page, textBlock);
// Ensure image is at bottom for Difference operation
engine.block.sendToBack(imageBlock);
if (engine.block.isCombinable([imageBlock, textBlock])) {
const differenceResult = engine.block.combine([imageBlock, textBlock], 'Difference');
}
// Zoom to see all results
await engine.scene.zoomToBlock(page, { padding: 40 });

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