Skip to main content

Programmatic Modular Scene Control

The modular scene API exposes every placed product instance and lets an integration add products to specific snapping points. Use it to validate assemblies, build scene summaries, or complete a product automatically before Finish or Add to Cart.

Observe All Scene Elements

Observer: window.mimeeqApp.observers.modular.modularSceneElements

The observer emits a map keyed by product instance ID. It emits the current scene immediately after subscription and emits again when products, configurations, connections, dimensions, or available item actions change.

Business Value

  • Distinguish individual scene instances from the product types they represent.
  • Validate connections and required components before checkout.
  • Build custom scene lists and controls without depending on the currently selected product.

Data Shape

type ModularSceneElements = Record<
string,
{
instanceId: string;
productId: string;
productName: string;
configurationCode: string;
connections: SnapConnection[];
dimensions: { width: number; height: number; depth: number } | null;
insertedAt: number | undefined;
settings: {
canConfigure: boolean;
canDelete: boolean;
canClone: boolean;
canMove: boolean;
canSlide: boolean;
canRotate: boolean;
canFlip: boolean;
canCopyStyles: boolean;
canPasteStyles: boolean;
canReplace: boolean;
};
// Placement fields such as position, rotation, quaternion, and freeMove
}
>;

Dimensions are expressed in millimeters. Each connection uses snapIdFrom for the snap on the current element, productUId for the connected instance, and snapIdTo for the snap on that connected instance.

Usage

const sceneSubscription = window.mimeeqApp.observers.modular.modularSceneElements.subscribe(
({ newValue }) => {
if (!newValue) return;

Object.values(newValue).forEach((element) => {
console.log({
instanceId: element.instanceId,
productId: element.productId,
connectedItems: element.connections.length,
canConfigure: element.settings.canConfigure,
});
});
},
);

// Call this when your integration or component is removed.
function cleanupSceneObserver() {
sceneSubscription.unsubscribe();
}

Add a Product to a Snapping Point

Method: window.mimeeqApp.actions.addProductToSnappingPoint(instanceId, snapIdTo, productId?, force?)

Adds a product to a snap owned by an existing scene instance. Without productId, the action uses the product currently selected in the modular product picker. Passing productId selects that product for this placement only and does not change the picker selection.

The promise resolves to true when the product is added. It resolves to false when the target instance or snap does not exist, the snap is unavailable, the product is incompatible with the target, collision validation fails, or the snap is already occupied and placement is not forced.

Business Value

  • Complete required assemblies before an order is submitted.
  • Add known accessories without asking users to navigate the product picker.
  • Handle stale scene data safely because placement is validated when the action runs.

Parameters

NameTypeDefaultDescription
instanceIdstringInstance ID of the existing product that owns the target snap.
snapIdTostringMesh ID of the target snapping point.
productIdstring | undefinedRedux-selected productProduct to add for this operation.
forcebooleanfalseAllows an occupied target snap to be used. Other validation remains active.

Usage

// Add the product currently selected in the modular product picker.
const addedSelectedProduct = await window.mimeeqApp.actions.addProductToSnappingPoint(
'sofa-instance-1',
'right-armrest-snap',
);

// Add a known product without changing the picker selection.
const addedArmrest = await window.mimeeqApp.actions.addProductToSnappingPoint(
'sofa-instance-1',
'right-armrest-snap',
'armrest-product-id',
);

if (!addedArmrest) {
showConfigurationMessage('The armrest cannot be placed at this end.');
}
warning

Set the force argument to true only when your product model intentionally supports multiple connections on the same snap. Force bypasses the occupied-snap guard, but it does not bypass hidden or disabled snaps, conditional snap rules, product compatibility, or collision checks.

const forced = await window.mimeeqApp.actions.addProductToSnappingPoint(
'sofa-instance-1',
'right-armrest-snap',
'armrest-product-id',
true,
);

Example: Complete a Linear Sofa Before Add to Cart

In this example, each sofa module has known left and right end snaps. Before Add to Cart, the integration finds open ends, adds an armrest to each one, and marks the sofa modules that own those ends with an option. If all ends already have armrests, it only applies the marker option.

Replace the product IDs, snap mesh IDs, block name, and option code with values from your product data. The marker block in this example is shared by every sofa module that can own an armrest.

<button id="complete-sofa-and-add-to-cart">Add to Cart</button>
document.addEventListener('mimeeq-app-loaded', () => {
const app = window.mimeeqApp;
const addToCartButton = document.getElementById('complete-sofa-and-add-to-cart');

const ARMREST_PRODUCT_ID = 'linear-sofa-armrest';
const END_SNAPS_BY_PRODUCT_ID = {
'linear-sofa-seat': ['left-armrest-snap', 'right-armrest-snap'],
'linear-sofa-corner': ['left-armrest-snap', 'right-armrest-snap'],
};
const END_MARKER_BLOCK_NAME = 'Armrest status';
const END_MARKER_OPTION_CODE = 'ATTACHED';

let sceneElements = {};

const sceneSubscription = app.observers.modular.modularSceneElements.subscribe(({ newValue }) => {
sceneElements = newValue || {};
});

function waitForObserverValue(observer, predicate) {
return new Promise((resolve) => {
let subscription;

subscription = observer.subscribe(({ newValue }) => {
if (!predicate(newValue)) return;

resolve(newValue);
// Observers emit synchronously when subscribed, so clean up in a microtask.
queueMicrotask(() => subscription?.unsubscribe());
});
});
}

function inspectSofaEnds(elements) {
const openEnds = [];
const armrestOwners = new Set();

Object.entries(elements).forEach(([instanceId, element]) => {
const endSnapIds = END_SNAPS_BY_PRODUCT_ID[element.productId];
if (!endSnapIds) return;

endSnapIds.forEach((snapId) => {
const connection = element.connections.find((item) => item.snapIdFrom === snapId);

if (!connection) {
openEnds.push({ instanceId, snapId });
return;
}

const connectedElement = elements[connection.productUId];
if (connectedElement?.productId === ARMREST_PRODUCT_ID) {
armrestOwners.add(instanceId);
}
});
});

return { openEnds, armrestOwners };
}

async function markArmrestOwners(instanceIds) {
if (instanceIds.length === 0) return;

// Select one owner so optionSets.blocks exposes the relevant product blocks.
const referenceInstanceId = instanceIds[0];
await app.actions.selectElement(referenceInstanceId);
await waitForObserverValue(
app.observers.modular.currentElement,
(value) =>
value === referenceInstanceId ||
(Array.isArray(value) && value.includes(referenceInstanceId)),
);

const blocks = await waitForObserverValue(
app.observers.optionSets.blocks,
(value) =>
Array.isArray(value) && value.some((block) => block.blockName === END_MARKER_BLOCK_NAME),
);
const block = blocks.find((item) => item.blockName === END_MARKER_BLOCK_NAME);
const option = block.options?.find((item) => item.code === END_MARKER_OPTION_CODE);

if (!option) {
throw new Error('The armrest marker option is not available.');
}

await app.actions.markOptionModular(
option,
block.id,
block.widgetType,
true,
block.blockName,
instanceIds,
);
}

async function completeLinearSofa() {
const { openEnds, armrestOwners } = inspectSofaEnds(sceneElements);

for (const { instanceId, snapId } of openEnds) {
// Do not force automatic completion. A false result protects checkout
// from an occupied, incompatible, hidden, or collision-blocked snap.
const added = await app.actions.addProductToSnappingPoint(
instanceId,
snapId,
ARMREST_PRODUCT_ID,
);

if (!added) {
throw new Error(`Cannot add an armrest to ${instanceId}:${snapId}.`);
}

armrestOwners.add(instanceId);
}

await markArmrestOwners([...armrestOwners]);
}

addToCartButton.addEventListener('click', async () => {
addToCartButton.disabled = true;

try {
await completeLinearSofa();
// The cart payload is generated after the scene and marker options update.
await app.actions.triggerFinishEvent('basket');
} catch (error) {
console.error(error);
showConfigurationMessage(
'Please review the sofa ends before adding this configuration to cart.',
);
} finally {
addToCartButton.disabled = false;
}
});

window.addEventListener('beforeunload', () => sceneSubscription.unsubscribe(), { once: true });
});

For a Finish or summary flow, call triggerFinishEvent('summary') after completeLinearSofa() instead.

note

Run this workflow before calling triggerFinishEvent(). The mimeeq-add-to-cart event contains an already generated cart payload, so changing the scene in that event handler is too late to affect that payload.