@3dsource/metabox-front-api
v3.0.29
Published
Client API for the Metabox Basic configurator
Downloads
2,174
Readme
Metabox Basic Configurator API
TypeScript/JavaScript API for integrating Metabox Basic Configurator into web applications. Provides programmatic control over 3D product visualization, materials, environments, and configuration state.
Table of Contents
- Features
- Prerequisites
- Installation
- Quick Start
- API Reference
- Standalone Mode
- Building a Custom UI
- Examples
- Troubleshooting
- Tips & Tricks
- Repository Development
Features
- Framework-agnostic integration (Angular, React, Vue, vanilla JS)
- Responsive design support
- Complete control over materials, environments, and products
- Screenshot and PDF export capabilities
- Showcase/animation playback management
- Full TypeScript type definitions
- CDN support for rapid prototyping
- Event-driven architecture for real-time state synchronization
Prerequisites
Runtime Requirements:
- Modern browser with ES6 module support (Chrome 61+, Firefox 60+, Safari 11+, Edge 79+)
- HTTPS connection (required by Unreal Engine streaming)
- Valid Metabox configurator ID
Development Environment:
- Node.js >= 20
Peer Dependencies:
rxjs >=7.8.0 <8.0.0
Important Notes:
integrateMetabox()validates inputs at runtime- Requires non-empty
configuratorIdandcontainerId - Iframe URL format:
https://{domain}/metabox-configurator/basic/{configuratorId} - Optional query parameters:
introImage,introVideo,loadingImage,state,showLoadingProgress,showResumeStreamPopup,showUserInactivityTimer,userInactivityTimeout,autoStartStream - HTTPS is strictly enforced (HTTP URLs will be rejected)
Installation
NPM Installation (Recommended)
Installation:
npm install @3dsource/metabox-front-api@latest --saveImport:
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, GetPdf, saveImage } from '@3dsource/metabox-front-api';CDN Installation (Quick Prototyping)
jsDelivr:
import { integrateMetabox, SetProduct, SetProductMaterial, GetScreenshot, saveImage } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';Note: For production, pin a specific version instead of using
@latest
Quick Start
1. HTML Setup
Ensure your HTML page has the following structure, where the Metabox Configurator will be integrated. You must provide CSS for the #embed3DSource element to make the configurator responsive and fit your layout needs. Create a container element where the configurator will be embedded:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Metabox Configurator</title>
<style>
#embed3DSource {
width: 100%;
height: 600px;
border: 1px solid #ccc;
border-radius: 8px;
}
</style>
</head>
<body>
<div id="embed3DSource">
<!-- Metabox Configurator will be embedded here -->
</div>
</body>
</html>2. Basic Integration
JavaScript (ES6 Modules)
<script type="module">
import { integrateMetabox, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, saveImage } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';
const configuratorId = 'configurator-id'; // your actual configurator id
// Ensure a container with id embed3DSource exists (see HTML Setup)
integrateMetabox(configuratorId, 'embed3DSource', (api) => {
api.addEventListener('configuratorDataUpdated', (env) => {
console.log('State updated:', env.productId, env.environmentId);
});
api.addEventListener('screenshotReady', (image) => {
if (image) saveImage(image, `configurator-${Date.now()}.png`);
});
// Initial commands example
api.sendCommandToMetabox(new SetProduct('product-1'));
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1280, y: 720 }));
});
</script>TypeScript/Angular/React
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, ShowEmbeddedMenu, ShowOverlayInterface, saveImage, type MimeType } from '@3dsource/metabox-front-api';
class MetaboxIntegrator {
private api: Communicator | null = null;
private readonly configuratorId: string;
constructor(configuratorId: string) {
this.configuratorId = configuratorId;
this.initialize();
}
private initialize(): void {
integrateMetabox(this.configuratorId, 'embed3DSource', (api: Communicator) => {
this.api = api;
this.setupEventListeners();
this.setupInitialState();
});
}
private setupEventListeners(): void {
if (!this.api) return;
// Listen for configuration data changes
this.api.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
console.log('Configurator data updated:', data);
this.handleConfiguratorData(data);
});
// Handle screenshot events
this.api.addEventListener('screenshotReady', (imageData: string | null) => {
saveImage(imageData ?? '', 'configurator-render.png');
});
// Handle viewport ready events
this.api.addEventListener('viewportReady', (isReady: boolean) => {
console.log('Viewport ready:', isReady);
});
// Handle status messages
this.api.addEventListener('statusMessageChanged', (message: string | null) => {
console.log('Status message:', message);
});
}
private setupInitialState(): void {
if (!this.api) return;
// Configure initial product and environment
this.api.sendCommandToMetabox(new SetProduct('your-product-id'));
this.api.sendCommandToMetabox(new SetEnvironment('your-environment-id'));
this.api.sendCommandToMetabox(new ShowOverlayInterface(true));
this.api.sendCommandToMetabox(new ShowEmbeddedMenu(true));
}
private handleConfiguratorData(data: ConfiguratorEnvelope): void {
// Access typed data properties
console.log('Product:', data.productId);
console.log('Materials:', data.productMaterialsIds);
console.log('Environment:', data.environmentId);
// Process configurator data according to your needs
this.updateUI(data);
}
private updateUI(data: ConfiguratorEnvelope): void {
// Update your application UI based on configurator state
// This method would contain your specific UI update logic
}
// Public API methods
public takeScreenshot(format: MimeType = 'image/png', size?: { x: number; y: number }): void {
if (this.api) {
this.api.sendCommandToMetabox(new GetScreenshot(format, size));
}
}
public changeMaterial(slotId: string, materialId: string): void {
if (this.api) {
this.api.sendCommandToMetabox(new SetProductMaterial(slotId, materialId));
}
}
public changeEnvironment(environmentId: string): void {
if (this.api) {
this.api.sendCommandToMetabox(new SetEnvironment(environmentId));
}
}
public destroy(): void {
this.api = null;
}
}
// Usage example
const configurator = new MetaboxIntegrator('configurator-id');
// Take a high-resolution screenshot
configurator.takeScreenshot('image/png', { x: 1920, y: 1080 });
// Change material
configurator.changeMaterial('seat-fabric', 'leather-brown');API Reference
System Terminology
| Term | Description | Context | | ----------------- | ----------------------------------------------------------- | ---------------------- | | product | 3D digital twin of physical product in Metabox system | Basic configurator | | productId | Unique product identifier (system-generated UUID) | All contexts | | environment | 3D scene/room environment for product visualization | Visual context | | environmentId | Unique environment identifier (system-generated UUID) | Environment management | | externalId | Custom SKU/identifier for integration with external systems | E-commerce integration | | showcase | Camera animation sequence attached to product | Product attribute | | slotId | Material slot identifier on product/environment | Material assignment | | materialId | Unique material identifier | Material assignment |
E-Commerce Configurator (CTA Integration):
E-Com configurators extend Basic configurators with a call-to-action button. Configuration:
Configuration:
- CTA Button Label: Custom text (e.g., "Get Quote", "Add to Cart")
- Callback URL: HTTP POST endpoint that receives configuration data
CTA Workflow:
User clicks CTA → Metabox POSTs config JSON → Your backend processes → Returns { redirectUrl } → User redirectedBackend Response Format:
{
"redirectUrl": "https://your.site/checkout-or-thank-you"
}Use Cases:
- Generate PDF quotations
- Create shopping cart entries
- Send lead information to CRM
- Redirect to custom checkout flow
Core Integration — integrateMetabox()
Embeds the Metabox configurator iframe and establishes communication channel.
Signature:
function integrateMetabox(configuratorId: string, containerId: string, apiReadyCallback: (api: Communicator) => void, config?: IntegrateMetaboxConfig): void;Parameters:
| Parameter | Type | Required | Default | Description |
| ------------------ | ------------------------ | -------- | ------- | ---------------------------------- |
| configuratorId | string | ✅ | - | Configurator UUID (not full URL) |
| containerId | string | ✅ | - | DOM container element ID |
| apiReadyCallback | function | ✅ | - | Callback invoked when API is ready |
| config | IntegrateMetaboxConfig | ❌ | {} | Additional configuration options |
Config Options (IntegrateMetaboxConfig):
interface IntegrateMetaboxConfig {
standalone?: boolean; // Disable built-in Metabox UI
introImage?: string; // URL for intro image
introVideo?: string; // URL for intro video
loadingImage?: string; // URL for loading spinner
state?: string; // Predefined state for initial loading in rison format (see https://github.com/Nanonid/rison)
domain?: string; // Override hostname, without the https:// prefix
showLoadingProgress?: boolean; // Show loading progress bar inside the configurator
showResumeStreamPopup?: boolean; // Show resume stream popup when the stream is paused
showUserInactivityTimer?: boolean; // Show user inactivity countdown popup before disconnecting
userInactivityTimeout?: number; // Inactivity timeout in seconds before the user is warned and disconnected
autoStartStream?: boolean; // Whether the pixel stream starts automatically on load (set false to start it manually via ResumeStream)
}Validation & Errors:
- Throws if
configuratorIdis empty - Throws if a container element not found
- Throws if URL is not HTTPS
- Replaces existing iframe with ID
embeddedContent
Example:
integrateMetabox(
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'my-container',
(api) => {
console.log('Configurator ready');
api.sendCommandToMetabox(new SetProduct('product-123'));
},
{
standalone: true,
loadingImage: 'https://cdn.example.com/loader.gif',
},
);Command Classes
Commands control configurator behavior. Send via api.sendCommandToMetabox(new CommandName(...)).
Product & Environment Management
| Command | Parameters | Description |
| ------------------------ | ------------------------------------ | ---------------------------------------------------------------------- |
| SetProduct | productId: string | Switch to specified product |
| SetProductMaterial | slotId: string, materialId: string | Apply material to product slot |
| SetEnvironment | environmentId: string | Change environment/scene |
| SetEnvironmentMaterial | slotId: string, materialId: string | Apply material to environment slot |
| ResetConfiguration | None | Reset configurator state to defaults (product, materials, environment) |
Example:
api.sendCommandToMetabox(new SetProduct('product-uuid'));
api.sendCommandToMetabox(new SetProductMaterial('body', 'red-metallic'));
api.sendCommandToMetabox(new ResetConfiguration()); // Restore template defaultsUI Control
- Note: ShowEmbeddedMenu and ShowOverlayInterface commands are ineffective in standalone mode.
| Command | Parameters | Description |
| ---------------------- | ------------------ | --------------------------- |
| ShowEmbeddedMenu | visible: boolean | Show/hide right sidebar |
| ShowOverlayInterface | visible: boolean | Show/hide viewport controls |
Example:
api.sendCommandToMetabox(new ShowEmbeddedMenu(false)); // Hide for custom UICamera Control
| Command | Parameters | Description |
| ------------- | ------------------------------ | ----------------------------------------------------------- |
| SetCamera | camera: CameraCommandPayload | Set camera position, rotation, fov and restrictions |
| GetCamera | None | Request current camera; returns via getCameraResult event |
| ResetCamera | None | Reset camera to default position |
| ApplyZoom | zoom: number | Apply zoom delta (positive=in, negative=out) |
Example:
api.sendCommandToMetabox(new ApplyZoom(50)); // Zoom in
api.sendCommandToMetabox(new ApplyZoom(-25)); // Zoom out
api.sendCommandToMetabox(new ResetCamera()); // Reset view
api.sendCommandToMetabox(
new SetCamera({
fov: 45,
mode: 'orbit',
position: { x: 100, y: 100, z: 100 },
rotation: { horizontal: 0, vertical: 0 },
restrictions: {
maxDistanceToPivot: 0,
maxFov: 0,
maxHorizontalRotation: 0,
maxVerticalRotation: 0,
minDistanceToPivot: 0,
minFov: 0,
minHorizontalRotation: 0,
minVerticalRotation: 0,
},
}),
);
// Request current camera state
api.sendCommandToMetabox(new GetCamera());
// Listen for the camera data
api.addEventListener('getCameraResult', (camera) => {
console.log('Current camera:', camera);
// camera is of type CameraCommandPayload
});CameraCommandPayload
The payload used by the SetCamera command to configure the camera.
interface CameraCommandPayload {
fov?: number;
mode?: 'fps' | 'orbit';
position?: { x: number; y: number; z: number };
rotation?: {
horizontal: number;
vertical: number;
};
restrictions?: {
maxDistanceToPivot?: number;
maxFov?: number;
maxHorizontalRotation?: number;
maxVerticalRotation?: number;
minDistanceToPivot?: number;
minFov?: number;
minHorizontalRotation?: number;
minVerticalRotation?: number;
};
}fov(number): Field of view in degrees.mode('fps' | 'orbit'):fps: First‑person style camera, moves freely in space.orbit: Orbiting camera around a pivot (typical product viewer behavior).
position({ x, y, z }): Camera position in world coordinates.rotation({ horizontal, vertical }): Camera rotation angles in degrees.horizontal: Yaw (left/right).vertical: Pitch (up/down).
restrictions(optional): Limits applied by the viewer to constrain user/camera movement.- Distance limits (orbit mode):
minDistanceToPivot,maxDistanceToPivot. - Field‑of‑view limits (first-person mode):
minFov,maxFov. - Rotation limits:
minHorizontalRotation,maxHorizontalRotation,minVerticalRotation,maxVerticalRotation.
- Distance limits (orbit mode):
Notes:
- Provide only the limits you want to enforce; unspecified values are left as currently configured by the viewer.
- Rotation units are degrees; positive/negative values follow the viewer’s right‑handed coordinate system.
- In
orbitmode, distance limits are interpreted relative to the orbit pivot. ResetCamerarestores the default position/rotation/FOV defined by the current product or environment template.
Showcase/Animation Control
| Command | Parameters | Description |
| --------------- | ---------- | ------------------------------------ |
| InitShowcase | None | Initialize showcase (load animation) |
| PlayShowcase | None | Start/resume animation playback |
| PauseShowcase | None | Pause animation |
| StopShowcase | None | Stop and reset animation |
Measurement Tools
| Command | Parameters | Description |
| ----------------- | ---------- | -------------------------- |
| ShowMeasurement | None | Display product dimensions |
| HideMeasurement | None | Hide dimension overlay |
Stream Control
| Command | Parameters | Description |
| -------------------------- | ---------- | ----------------------------------------------------------- |
| ResumeStream | None | Resume pixel streaming session after pause or disconnection |
| StopStream | None | Stop the pixel streaming session and release its resources |
| ResetUserInactivityTimer | None | Reset the AFK inactivity timer to keep the session alive |
Example:
// Resume the stream after an idle timeout or network interruption
api.sendCommandToMetabox(new ResumeStream());
// Stop the stream, e.g. when closing a modal or navigating away
api.sendCommandToMetabox(new StopStream());
// Reset the inactivity timer when user confirms they are still present
api.sendCommandToMetabox(new ResetUserInactivityTimer());Export & Media
| Command | Parameters | Description | Event Triggered |
| ---------------------------- | --------------------------------------------------- | -------------------- | ---------------------- |
| GetScreenshot | mimeType: MimeType, size?: {x: number, y: number} | Render screenshot | screenshotReady |
| GetPdf | None | Generate PDF export | Server-side (no event) |
| GetCallToActionInformation | None | Trigger CTA workflow | Backend redirect |
Screenshot Example:
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
api.addEventListener('screenshotReady', (imageData) => {
saveImage(imageData, 'product.png');
});Event Handling
Event-driven architecture for reactive state management. Register listeners via api.addEventListener(eventName, handler).
Available Events
| Event | Payload Type | Description | Use Case |
| ----------------------------- | --------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------- |
| configuratorDataUpdated | ConfiguratorEnvelope | Configuration state changed | Sync UI with product/material/environment |
| ecomConfiguratorDataUpdated | EcomConfigurator | CTA configuration loaded | Display CTA button with label |
| viewportReady | boolean | 3D viewport ready state | Hide loading, enable interactions |
| showcaseStatusChanged | ShowCaseStatus | Animation playback status | Update play/pause button state |
| statusMessageChanged | string \| null | Loading/progress message | Display user feedback |
| screenshotReady | string \| null | Base64 screenshot data | Download or display image |
| getCameraResult | CameraCommandPayload | Current camera data returned | Capture/store current camera |
| videoResolutionChanged | VideoResolution | Stream resolution changed | Adjust viewport layout |
| videoStatsUpdated | VideoStats | Video stream quality metrics | QoS dashboards, adaptive behavior |
| loadingStatusChanged | { percents: number; loaderVisible: boolean } | Loading progress or overlay changed | Show custom loading progress bar |
| userInactivityDetected | { remainingSecondsToDisconnect: number; countdownVisible: boolean } | AFK countdown started or updated | Show "Stay connected?" prompt |
Event Examples
Configuration State Sync:
api.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
console.log('Product:', data.productId);
console.log('Materials:', data.productMaterialsIds);
console.log('Environment:', data.environmentId);
// Update custom UI
updateProductSelector(data.productId);
updateMaterialSwatches(data.productMaterialsIds);
});Screenshot Handling:
api.addEventListener('screenshotReady', (imageData: string | null) => {
if (!imageData) {
console.error('Screenshot failed');
return;
}
saveImage(imageData, `product-${Date.now()}.png`);
});Loading State:
api.addEventListener('viewportReady', (isReady: boolean) => {
if (isReady) {
hideLoadingSpinner();
enableComponentSelectors();
}
});
api.addEventListener('statusMessageChanged', (message: string | null) => {
document.getElementById('status-text').textContent = message || '';
});Showcase Control:
api.addEventListener('showcaseStatusChanged', (status: ShowCaseStatus) => {
const playButton = document.getElementById('play-btn');
switch (status) {
case 'play':
playButton.textContent = 'Pause';
break;
case 'pause':
case 'stop':
playButton.textContent = 'Play';
break;
}
});Loading Progress:
api.addEventListener('loadingStatusChanged', ({ percents, loaderVisible }) => {
const bar = document.getElementById('progress-bar');
if (bar) {
bar.style.display = loaderVisible ? 'block' : 'none';
bar.style.width = `${percents}%`;
}
});AFK Inactivity Countdown:
import { ResetUserInactivityTimer } from '@3dsource/metabox-front-api';
api.addEventListener('userInactivityDetected', ({ remainingSecondsToDisconnect, countdownVisible }) => {
const dialog = document.getElementById('afk-dialog');
const countdown = document.getElementById('afk-countdown');
if (dialog) dialog.style.display = countdownVisible ? 'flex' : 'none';
if (countdown) countdown.textContent = String(remainingSecondsToDisconnect);
});
document.getElementById('stay-connected-btn')?.addEventListener('click', () => {
api.sendCommandToMetabox(new ResetUserInactivityTimer());
});Utility Functions
saveImage(imageUrl: string, filename: string): void
Downloads base64-encoded image to user's device.
Parameters:
| Parameter | Type | Description |
| ---------- | -------- | --------------------------------------------------- |
| imageUrl | string | Base64 data URL (e.g., data:image/png;base64,...) |
| filename | string | Desired filename with extension |
Example:
api.addEventListener('screenshotReady', (imageData: string | null) => {
if (imageData) {
saveImage(imageData, `config-${Date.now()}.png`);
}
});Implementation Note:
Creates a temporary anchor element with download attribute to trigger browser download.
Accepted input: only a base64 data URL of type image/png, image/jpeg or
image/webp — the formats GetScreenshot produces. Anything else (a remote
https:// image, an svg+xml or text/html data URL) is ignored with a console
warning instead of being downloaded, since this function is fed event payloads
straight off the postMessage channel.
fromCommunicatorEvent(target: Communicator, eventName: string): Observable
RxJS wrapper for Communicator events. Works like RxJS fromEvent — creates a typed Observable that emits each time the specified event fires. Alternative to api.addEventListener().
Parameters:
| Parameter | Type | Description |
| ----------- | -------------- | -------------------------------------------- |
| target | Communicator | The Communicator instance to listen on |
| eventName | string | Event name (e.g., configuratorDataUpdated) |
Returns: Observable<T> — typed Observable matching the event payload.
Example:
import { fromCommunicatorEvent } from '@3dsource/metabox-front-api';
integrateMetabox('configurator-id', 'embed3DSource', (api) => {
fromCommunicatorEvent(api, 'configuratorDataUpdated').subscribe((data) => {
console.log('Product:', data.productId);
});
fromCommunicatorEvent(api, 'screenshotReady').subscribe((imageData) => {
if (imageData) saveImage(imageData, 'screenshot.png');
});
});Standalone Mode
Headless rendering mode for custom UI implementations. Disables all built-in Metabox UI elements.
When to Use Standalone Mode
✅ Use When:
- Building a fully custom configurator interface
- Implementing brand-specific product selection UX
- Integrating into existing design systems
- Requiring complete control over configuration flow
❌ Don't Use When:
- Quick prototyping with default UI is sufficient
- Minimal customization needed
- Limited development resources
Behavior Changes
| Feature | Default Mode | Standalone Mode | | ------------------ | ------------ | --------------- | | Right sidebar menu | Visible | Hidden | | Viewport overlays | Visible | Hidden | | Template logic | Active | Disabled | | API control | Partial | Full | | Event system | Available | Available |
Implementation
TypeScript:
import { integrateMetabox, Communicator, SetProduct, SetEnvironment } from '@3dsource/metabox-front-api';
integrateMetabox(
'configurator-id',
'embed3DSource',
(api: Communicator) => {
// Work with API here
},
{ standalone: true }, // <- Enable standalone mode to disable default UI
);JavaScript (CDN):
<script type="module">
import { integrateMetabox } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';
integrateMetabox(
'configurator-id',
'embed3DSource',
(api) => {
// Work with API here
},
{ standalone: true }, // <- Enable standalone mode to disable default UI
);
</script>Building a Custom UI
To create a custom configurator interface, use standalone mode and subscribe to events before sending commands, then follow these steps:
1. Listen to State Changes
api.addEventListener('configuratorDataUpdated', (data) => {
// Update your UI based on configurator state
});2. Control Product Configuration
api.sendCommandToMetabox(new SetProduct('product-id')); // Change product
api.sendCommandToMetabox(new SetProductMaterial('slot-id', 'material-id')); // Apply material
api.sendCommandToMetabox(new SetEnvironment('environment-id')); // Change environment3. Export Configuration
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
api.sendCommandToMetabox(new GetPdf());
api.sendCommandToMetabox(new GetCallToActionInformation());Examples
E‑Com CTA example
Minimal example of triggering the Call‑To‑Action flow from your page once the API is ready.
<div id="metabox-container"></div>
<button id="cta-btn" disabled>Send configuration (CTA)</button>
<script type="module">
import { integrateMetabox, GetCallToActionInformation } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';
let api = null;
integrateMetabox('configurator-id', 'metabox-container', (apiInstance) => {
api = apiInstance;
document.getElementById('cta-btn').disabled = false;
});
document.getElementById('cta-btn').addEventListener('click', () => {
// Triggers sending the current configuration to your CTA Callback URL
api.sendCommandToMetabox(new GetCallToActionInformation());
});
</script>Basic Product Configurator
A complete HTML example showing how to integrate the Metabox configurator with vanilla JavaScript.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Product Configurator</title>
<style>
#metabox-container {
width: 100%;
height: 600px;
border: 1px solid #ddd;
border-radius: 8px;
}
.controls {
margin: 20px 0;
}
button {
margin: 5px;
padding: 10px 15px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f5f5f5;
cursor: pointer;
}
button:hover {
background: #e5e5e5;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.error {
color: #d32f2f;
padding: 10px;
background: #ffebee;
border: 1px solid #ffcdd2;
border-radius: 4px;
margin: 10px 0;
}
</style>
</head>
<body>
<div id="loading" class="loading">Loading configurator...</div>
<div id="error" class="error" style="display: none;"></div>
<div id="metabox-container"></div>
<div class="controls">
<button id="product1-btn" onclick="changeProduct('product-1')" disabled>Product 1</button>
<button id="product2-btn" onclick="changeProduct('product-2')" disabled>Product 2</button>
<button id="red-material-btn" onclick="applyMaterial('slot-1', 'material-red')" disabled>Red Material</button>
<button id="blue-material-btn" onclick="applyMaterial('slot-1', 'material-blue')" disabled>Blue Material</button>
<button id="screenshot-btn" onclick="takeScreenshot()" disabled>Take Screenshot</button>
</div>
<script type="module">
import { integrateMetabox, SetProduct, SetProductMaterial, GetScreenshot, saveImage } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';
let api = null;
let isApiReady = false;
// Helper function to show/hide loading state
function setLoadingState(loading) {
const loadingEl = document.getElementById('loading');
const buttons = document.querySelectorAll('button');
loadingEl.style.display = loading ? 'block' : 'none';
buttons.forEach((btn) => (btn.disabled = loading || !isApiReady));
}
// Helper function to show error messages
function showError(message) {
const errorEl = document.getElementById('error');
errorEl.textContent = message;
errorEl.style.display = 'block';
setLoadingState(false);
}
// Initialize configurator with error handling
try {
// Replace 'configurator-id' with your actual configurator ID from 3DSource
const configuratorId = 'configurator-id';
integrateMetabox(configuratorId, 'metabox-container', (apiInstance) => {
try {
api = apiInstance;
isApiReady = true;
setLoadingState(false);
console.log('Configurator ready!');
// Listen for configurator state changes
api.addEventListener('configuratorDataUpdated', (data) => {
console.log('Configurator state updated:', data);
// You can update your UI based on the current state
// For example, update available materials, products, etc.
});
// Listen for screenshot completion
api.addEventListener('screenshotReady', (imageData) => {
console.log('Screenshot captured successfully');
// Save the image with a timestamp
saveImage(imageData, `configurator-screenshot-${Date.now()}.png`);
});
} catch (error) {
console.error('Error setting up API:', error);
showError('Failed to initialize configurator API: ' + error.message);
}
});
} catch (error) {
console.error('Error initializing configurator:', error);
showError('Failed to load configurator: ' + error.message);
}
// Global functions for button interactions
window.changeProduct = (productId) => {
if (!isApiReady || !api) {
console.warn('API not ready yet');
return;
}
try {
console.log(`Changing to product: ${productId}`);
api.sendCommandToMetabox(new SetProduct(productId));
} catch (error) {
console.error('Error changing product:', error);
showError('Failed to change product: ' + error.message);
}
};
window.applyMaterial = (slotId, materialId) => {
if (!isApiReady || !api) {
console.warn('API not ready yet');
return;
}
try {
console.log(`Applying material ${materialId} to slot ${slotId}`);
api.sendCommandToMetabox(new SetProductMaterial(slotId, materialId));
} catch (error) {
console.error('Error applying material:', error);
showError('Failed to apply material: ' + error.message);
}
};
window.takeScreenshot = () => {
if (!isApiReady || !api) {
console.warn('API not ready yet');
return;
}
try {
console.log('Taking screenshot...');
// Request high-quality screenshot in PNG format
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
} catch (error) {
console.error('Error taking screenshot:', error);
showError('Failed to take screenshot: ' + error.message);
}
};
</script>
</body>
</html>React Integration Example
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, GetScreenshot, saveImage } from '@3dsource/metabox-front-api';
interface MetaboxConfiguratorProps {
configuratorId: string;
onStateChange?: (data: ConfiguratorEnvelope) => void;
onError?: (error: string) => void;
className?: string;
}
interface ConfiguratorState {
isLoading: boolean;
error: string | null;
isApiReady: boolean;
}
const MetaboxConfigurator: React.FC<MetaboxConfiguratorProps> = ({ configuratorId, onStateChange, onError, className }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [api, setApi] = useState<Communicator | null>(null);
const [state, setState] = useState<ConfiguratorState>({
isLoading: true,
error: null,
isApiReady: false,
});
// Generate unique container ID to avoid conflicts
const containerId = useRef(`metabox-container-${Math.random().toString(36).substr(2, 9)}`);
// Error handler
const handleError = useCallback(
(error: string) => {
setState((prev) => ({ ...prev, error, isLoading: false }));
onError?.(error);
console.error('Metabox Configurator Error:', error);
},
[onError],
);
// Initialize configurator
useEffect(() => {
if (!containerRef.current) return;
let mounted = true;
const initializeConfigurator = async () => {
try {
setState((prev) => ({ ...prev, isLoading: true, error: null }));
// Set the container ID
containerRef.current!.id = containerId.current;
integrateMetabox(configuratorId, containerId.current, (apiInstance) => {
if (!mounted) return; // Component was unmounted
try {
setApi(apiInstance);
setState((prev) => ({ ...prev, isLoading: false, isApiReady: true }));
// Set up event listeners with proper typing
apiInstance.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
if (mounted) {
onStateChange?.(data);
}
});
apiInstance.addEventListener('screenshotReady', (imageData: string) => {
if (mounted) {
console.log('Screenshot captured successfully');
saveImage(imageData, `configurator-screenshot-${Date.now()}.png`);
}
});
console.log('React Metabox Configurator initialized successfully');
} catch (error) {
if (mounted) {
handleError(`Failed to set up API: ${error instanceof Error ? error.message : String(error)}`);
}
}
});
} catch (error) {
if (mounted) {
handleError(`Failed to initialize configurator: ${error instanceof Error ? error.message : String(error)}`);
}
}
};
initializeConfigurator();
// Cleanup function
return () => {
mounted = false;
if (api) {
// Clean up any event listeners if the API provides cleanup methods
console.log('Cleaning up Metabox Configurator');
}
};
}, [configuratorId, onStateChange, handleError, api]);
// Command methods with error handling
const changeProduct = useCallback(
(productId: string) => {
if (!state.isApiReady || !api) {
console.warn('API not ready yet');
return;
}
try {
console.log(`Changing to product: ${productId}`);
api.sendCommandToMetabox(new SetProduct(productId));
} catch (error) {
handleError(`Failed to change product: ${error instanceof Error ? error.message : String(error)}`);
}
},
[api, state.isApiReady, handleError],
);
const applyMaterial = useCallback(
(slotId: string, materialId: string) => {
if (!state.isApiReady || !api) {
console.warn('API not ready yet');
return;
}
try {
console.log(`Applying material ${materialId} to slot ${slotId}`);
api.sendCommandToMetabox(new SetProductMaterial(slotId, materialId));
} catch (error) {
handleError(`Failed to apply material: ${error instanceof Error ? error.message : String(error)}`);
}
},
[api, state.isApiReady, handleError],
);
const takeScreenshot = useCallback(() => {
if (!state.isApiReady || !api) {
console.warn('API not ready yet');
return;
}
try {
console.log('Taking screenshot...');
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
} catch (error) {
handleError(`Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`);
}
}, [api, state.isApiReady, handleError]);
// Render error state
if (state.error) {
return (
<div className={className}>
<div
style={{
color: '#d32f2f',
padding: '20px',
background: '#ffebee',
border: '1px solid #ffcdd2',
borderRadius: '4px',
textAlign: 'center',
}}
>
<h3>Configurator Error</h3>
<p>{state.error}</p>
<button
onClick={() => window.location.reload()}
style={{
padding: '10px 20px',
marginTop: '10px',
border: '1px solid #d32f2f',
background: '#fff',
color: '#d32f2f',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Reload Page
</button>
</div>
</div>
);
}
return (
<div className={className}>
{/* Loading indicator */}
{state.isLoading && (
<div
style={{
textAlign: 'center',
padding: '40px',
color: '#666',
background: '#f5f5f5',
borderRadius: '8px',
}}
>
<div>Loading configurator...</div>
<div style={{ marginTop: '10px', fontSize: '14px' }}>Please wait while we initialize the 3D viewer</div>
</div>
)}
{/* Configurator container */}
<div
ref={containerRef}
style={{
width: '100%',
height: '600px',
border: '1px solid #ddd',
borderRadius: '8px',
display: state.isLoading ? 'none' : 'block',
}}
/>
{/* Controls */}
{state.isApiReady && (
<div style={{ marginTop: '20px' }}>
<div style={{ marginBottom: '10px', fontWeight: 'bold', color: '#333' }}>Configurator Controls:</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
<button
onClick={() => changeProduct('product-1')}
disabled={!state.isApiReady}
style={{
padding: '10px 15px',
border: '1px solid #ccc',
borderRadius: '4px',
background: state.isApiReady ? '#f5f5f5' : '#e0e0e0',
cursor: state.isApiReady ? 'pointer' : 'not-allowed',
transition: 'background-color 0.2s',
}}
onMouseOver={(e) => {
if (state.isApiReady) e.currentTarget.style.background = '#e5e5e5';
}}
onMouseOut={(e) => {
if (state.isApiReady) e.currentTarget.style.background = '#f5f5f5';
}}
>
Product 1
</button>
<button
onClick={() => changeProduct('product-2')}
disabled={!state.isApiReady}
style={{
padding: '10px 15px',
border: '1px solid #ccc',
borderRadius: '4px',
background: state.isApiReady ? '#f5f5f5' : '#e0e0e0',
cursor: state.isApiReady ? 'pointer' : 'not-allowed',
}}
>
Product 2
</button>
<button
onClick={() => applyMaterial('slot-1', 'material-red')}
disabled={!state.isApiReady}
style={{
padding: '10px 15px',
border: '1px solid #ccc',
borderRadius: '4px',
background: state.isApiReady ? '#f5f5f5' : '#e0e0e0',
cursor: state.isApiReady ? 'pointer' : 'not-allowed',
}}
>
Red Material
</button>
<button
onClick={() => applyMaterial('slot-1', 'material-blue')}
disabled={!state.isApiReady}
style={{
padding: '10px 15px',
border: '1px solid #ccc',
borderRadius: '4px',
background: state.isApiReady ? '#f5f5f5' : '#e0e0e0',
cursor: state.isApiReady ? 'pointer' : 'not-allowed',
}}
>
Blue Material
</button>
<button
onClick={takeScreenshot}
disabled={!state.isApiReady}
style={{
padding: '10px 15px',
border: '1px solid #007bff',
borderRadius: '4px',
background: state.isApiReady ? '#007bff' : '#6c757d',
color: 'white',
cursor: state.isApiReady ? 'pointer' : 'not-allowed',
}}
>
📸 Take Screenshot
</button>
</div>
</div>
)}
</div>
);
};
export default MetaboxConfigurator;Angular Integration Example
// metabox-configurator.component.ts
import { Component, input, OnInit, output, signal } from '@angular/core';
import { Communicator, ConfiguratorEnvelope, GetCallToActionInformation, GetPdf, GetScreenshot, InitShowcase, integrateMetabox, PauseShowcase, PlayShowcase, saveImage, SetProductMaterial, SetProduct, ShowEmbeddedMenu, ShowOverlayInterface, StopShowcase } from '@3dsource/metabox-front-api';
interface ConfiguratorState {
isLoading: boolean;
error: string | null;
isApiReady: boolean;
}
@Component({
selector: 'app-metabox-configurator',
template: `
@let _state = state();
<div class="configurator-container">
<!-- Loading State -->
@if (_state.isLoading) {
<div class="loading-container">
<div class="loading-content">
<div class="loading-spinner"></div>
<div class="loading-text">Loading configurator...</div>
<div class="loading-subtext">Please wait while we initialize the 3D viewer</div>
</div>
</div>
}
<!-- Error State -->
@if (_state.error) {
<div class="error-container">
<h3>Configurator Error</h3>
<p>{{ _state.error }}</p>
<button (click)="retryInitialization()" class="retry-button">Retry</button>
</div>
}
<!-- Configurator Container -->
<div [id]="containerId()" class="configurator-viewport" [hidden]="_state.isLoading || _state.error"></div>
<!-- Controls -->
@if (_state.isApiReady) {
<div class="controls">
<div class="controls-title">Configurator Controls:</div>
<div class="controls-buttons">
<button (click)="changeProduct('541f46ab-a86c-48e3-bcfa-f92341483db3')" [disabled]="!_state.isApiReady" class="control-button">Product Change</button>
<button (click)="initShowcase()" [disabled]="!_state.isApiReady" class="control-button">Init showcase</button>
<button (click)="stopShowcase()" [disabled]="!_state.isApiReady" class="control-button">Stop showcase</button>
<button (click)="playShowcase()" [disabled]="!_state.isApiReady" class="control-button">Play showcase</button>
<button (click)="pauseShowcase()" [disabled]="!_state.isApiReady" class="control-button">Pause showcase</button>
<button (click)="applyMaterial('slot-1', 'material-red')" [disabled]="!_state.isApiReady" class="control-button">Red Material</button>
<button (click)="applyMaterial('slot-1', 'material-blue')" [disabled]="!_state.isApiReady" class="control-button">Blue Material</button>
<button (click)="getPdf()" [disabled]="!_state.isApiReady" class="control-button">Get PDF</button>
<button (click)="takeScreenshot()" [disabled]="!_state.isApiReady" class="control-button screenshot-button">📸 Take Screenshot</button>
<button (click)="sendCallToActionInformation()" [disabled]="!_state.isApiReady" class="control-button">Send Call To Action Information</button>
</div>
</div>
}
</div>
`,
styles: [
`
.configurator-container {
width: 100%;
position: relative;
}
.configurator-viewport {
width: 100%;
height: 600px;
border: 1px solid #ddd;
border-radius: 8px;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
height: 600px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 8px;
}
.loading-content {
text-align: center;
color: #666;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
}
.loading-subtext {
font-size: 14px;
opacity: 0.8;
}
.error-container {
padding: 40px;
text-align: center;
background: #ffebee;
border: 1px solid #ffcdd2;
border-radius: 8px;
color: #d32f2f;
}
.error-container h3 {
margin: 0 0 16px 0;
font-size: 18px;
}
.error-container p {
margin: 0 0 20px 0;
font-size: 14px;
}
.retry-button {
padding: 10px 20px;
border: 1px solid #d32f2f;
background: #fff;
color: #d32f2f;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.retry-button:hover {
background: #d32f2f;
color: #fff;
}
.controls {
margin-top: 20px;
}
.controls-title {
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
.controls-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.control-button {
padding: 10px 15px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f5f5f5;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
}
.control-button:hover:not(:disabled) {
background: #e5e5e5;
}
.control-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.screenshot-button {
border-color: #007bff;
background: #007bff;
color: white;
}
.screenshot-button:hover:not(:disabled) {
background: #0056b3;
}
`,
],
})
export class MetaboxConfiguratorComponent implements OnInit {
configuratorId = input.required<string>();
stateChange = output<ConfiguratorEnvelope>();
errorFired = output<string>();
state = signal<ConfiguratorState>({
isLoading: true,
error: null,
isApiReady: false,
});
containerId = signal(`metabox-container-${Math.random().toString(36).substring(2, 9)}`);
private api: Communicator | null = null;
ngOnInit(): void {
this.initializeConfigurator();
}
// Public methods for external control
changeProduct(productId: string): void {
if (!this.state().isApiReady || !this.api) {
console.warn('API not ready yet');
return;
}
try {
console.log(`Changing to product: ${productId}`);
this.api.sendCommandToMetabox(new SetProduct(productId));
} catch (error) {
this.handleError(`Failed to change product: ${this.getErrorMessage(error)}`);
}
}
initShowcase(): void {
if (!this.state().isApiReady || !this.api) {
console.warn('API not ready yet');
return;
}
try {
this.api.sendCommandToMetabox(new InitShowcase());
} catch (error) {
this.handleError(`Failed to init showcase for product: ${this.getErrorMessage(error)}`);
}
}
stopShowcase(): void {
if (!this.state().isApiReady || !this.api) {
console.warn('API not ready yet');
return;
}
try {
console.log(`Stop showcase`);
this.api.sendCommandToMetabox(new StopShowcase());
} catch (error) {
this.handleError(`Failed to init showcase for product: ${this.getErrorMessage(error)}`);
}
}
playShowcase(): void {
if (!this.state().isApiReady || !this.api) {
console.warn('API not ready yet');
return;
}
try {
console.log(`Play showcase`);
this.api.sendCommandToMetabox(new PlayShowcase());
} catch {
this.handleError(`Failed to play showcase`);
}
}
pauseShowcase(): void {
if (!this.state().isApiReady || !this.api) {
console.warn('API not ready yet');
return;
}
try {
console.log(`Pause showcase`);
this.api.sendCommandToMetabox(new PauseShowcase());
} catch {
this.handleError(`Failed to pause showcase`);
}
}
applyMaterial(slotId: string, materialId: string): void {
if (!this.state().isApiReady || !this.api) {
console.warn('API not ready yet');
return;
}
try {
console.log(`Applying material ${materialId} to slot ${slotId}`);
this.api.sendCommandToMetabox(new SetProductMaterial(slotId, materialId));
} catch (error) {
this.handleError(`Failed to apply material: ${this.getErrorMessage(error)}`);
}
}
takeScreenshot(): void {
if (!this.state().isApiReady || !this.api) {
console.warn('API not ready yet');
return;
}
try {
console.log('Taking screenshot...');
this.api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
} catch (error) {
this.handleError(`Failed to take screenshot: ${this.getErrorMessage(error)}`);
}
}
sendCallToActionInformation(): void {
if (!this.state().isApiReady || !this.api) {
console.warn('API not ready yet');
return;
}
try {
console.log('Generating cta information...');
this.api.sendCommandToMetabox(new GetCallToActionInformation());
} catch (error) {
this.handleError(`Failed to generating cta information: ${this.getErrorMessage(error)}`);
}
}
getPdf(): void {
if (!this.state().isApiReady || !this.api) {
console.warn('API not ready yet');
return;
}
try {
console.log('Generating PDF...');
this.api.sendCommandToMetabox(new GetPdf());
} catch (error) {
this.handleError(`Failed to generating pdf: ${this.getErrorMessage(error)}`);
}
}
retryInitialization(): void {
this.updateState({ error: null });
this.initializeConfigurator();
}
sendInitCommands() {
if (!this.api) {
return;
}
this.api.sendCommandToMetabox(new ShowEmbeddedMenu(true));
this.api.sendCommandToMetabox(new ShowOverlayInterface(true));
}
private initializeConfigurator() {
try {
this.updateState({ isLoading: true, error: null });
integrateMetabox(this.configuratorId(), this.containerId(), (apiInstance) => {
try {
this.api = apiInstance;
this.updateState({ isLoading: false, isApiReady: true });
this.sendInitCommands();
this.setupEventListeners();
console.log('Angular Metabox Configurator initialized successfully');
} catch (error) {
this.handleError(`Failed to set up API: ${this.getErrorMessage(error)}`);
}
});
} catch (error) {
this.handleError(`Failed to initialize configurator: ${this.getErrorMessage(error)}`);
}
}
private setupEventListeners(): void {
if (!this.api) {
return;
}
// Listen for configurator state changes
this.api.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
console.log('Configurator state updated:', data);
this.stateChange.emit(data);
});
// Listen for screenshot completion
this.api.addEventListener('screenshotReady', (imageData: string | null) => {
console.log(`Screenshot captured successfully ${imageData ?? ''}`);
saveImage(imageData ?? '', `configurator-screenshot-${Date.now()}.png`);
});
}
private updateState(updates: Partial<ConfiguratorState>): void {
this.state.set({ ...this.state(), ...updates });
}
private handleError(errorMessage: string): void {
console.error('Metabox Configurator Error:', errorMessage);
this.updateState({ error: errorMessage, isLoading: false });
this.errorFired.emit(errorMessage);
}
private getErrorMessage(error: any): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
}Key Features of this Angular Example:
- Comprehensive Error Handling: Try-catch blocks and user-friendly error states
- Loading States: Visual feedback with spinner during initialization
- TypeScript Integration: Full type safety with proper interfaces
- Unique Container IDs: Automatic ID generation to avoid conflicts
- Event Outputs: Emits state changes and errors to parent components
- Responsive Design: Clean, accessible button layout with proper styling
Usage Example:
// app.component.ts
import { Component } from '@angular/core';
import { ConfiguratorEnvelope } from '@3dsource/metabox-front-api';
@Component({
selector: 'app-root',
template: `
<div class="app-container">
<h1>My Product Configurator</h1>
<app-metabox-configurator [configuratorId]="configuratorId" (stateChange)="onConfiguratorStateChange($event)" (errorFired)="onConfiguratorError($event)"></app-metabox-configurator>
</div>
`,
styles: [
`
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
`,
],
})
export class AppComponent {
configuratorId = 'configurator-id';
onConfiguratorStateChange(data: ConfiguratorEnvelope): void {
console.log('Configurator state changed:', data);
// Update your application state based on configurator changes
}
onConfiguratorError(error: string): void {
console.error('Configurator error:', error);
// Handle errors (show notifications, log to analytics, etc.)
}
}To Use This Component:
- Install the package:
npm install @3dsource/metabox-front-api@latest - Import the component in your Angular module
- Replace
'configurator-id'with your actual configurator ID - Replace placeholder IDs with your real product, material, and slot IDs
- Customize the styling by modifying the component styles
Troubleshooting
Common Issues
1. Configurator Not Loading
Symptoms: Blank screen, iframe not appearing, loading indefinitely
Root Causes & Solutions:
| Issue | Diagnostic | Solution |
| --------------------------- | ------------------------------------ | --------------------------------------------------------- |
| Invalid configurator ID | Browser console shows 404 errors | Verify configurator ID in Metabox admin |
| Container not found | Error: Container element not found | Ensure element exists before calling integrateMetabox() |
| HTTP instead of HTTPS | Mixed content warnings | Use HTTPS or test on localhost |
| Container has no dimensions | Invisible iframe (0x0) | Set explicit width/height on container element |
| CORS/iframe blocking | X-Frame-Options errors | Check domain allowlist in Metabox settings |
Diagnostic Code:
const containerId = 'embed3DSource';
const container = document.getElementById(containerId);
if (!container) {
throw new Error(`Container #${containerId} not found`);
}
// Verify container has dimensions
const rect = container.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.warn('⚠️ Container has no dimensions');
}
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
console.warn('⚠️ HTTPS required for Unreal Engine streaming');
}
integrateMetabox('configurator-id', containerId, (api) => {
console.log('✅ Configurator loaded');
});2. Commands Not Working
Symptoms: Commands sent but no visual changes in configurator
Common Mistakes:
| Mistake | Problem | Solution |
| -------------------------- | ----------------- | ----------------------------------------------- |
| Sending before API ready | Commands ignored | Only send in apiReadyCallback |
| Wrong product/material IDs | Entity not found | Verify IDs from configuratorDataUpdated event |
| Wrong slot ID | Material rejected | Verify slot IDs from configurator data |
Correct Pattern:
integrateMetabox('configurator-id', 'container', (api) => {
// ✅ Correct: Commands sent after API ready
api.sendCommandToMetabox(new SetProduct('product-id'));
api.sendCommandToMetabox(new SetProductMaterial('slot-id', 'material-id'));
});
// ❌ Wrong: Command sent too early
// api.sendCommandToMetabox(new SetProduct('product-id'));Getting Help
If you're still experiencing issues:
- Check the browser console for error messages
- Verify your configurator ID in a separate browser tab
- Test with a minimal example to isolate the issue
- Contact support with: browser version, configurator URL, console errors, and steps to reproduce
Tips & Tricks
- Debounce rapid commands. Avoid sending too many commands in quick succession — debounce material or product changes (e.g., 300 ms).
- Responsive container. Set the configurator container to responsive dimensions (
width: 100%; height: 100%;) and adjust with media queries for mobile. - Events work in all modes.
configuratorDataUpdated,screenshotReady, and other events fire in both default and standalone mode — use them to keep your UI synchronized. - Wait for API ready. Only send commands inside the
apiReadyCallback. Commands sent before initialization are silently ignored. - Validate IDs from events. Use the
configuratorDataUpdatedevent payload to discover valid product, material, and slot IDs rather than hardcoding them. - HTTPS is required. The iframe URL enforces HTTPS. HTTP URLs will be rejected. Use
localhostfor local development. - Pin CDN versions in production. Replace
@latestwith a specific version to avoid unexpected breaking changes.
Repository Development
Run commands from the repository root:
pnpm metabox-front-api:build
pnpm metabox-front-api:docs
pnpm metabox-front-api:release:patchgenerate:docs writes HTML and Markdown API documentation into the demo build
tree. The release command performs verification, changes the package version
and publishes the verified tarball to npm.
For more examples and advanced usage, visit our [documentation site](https://preview.3dsource.com/front-libraries/master/basi
