@3dsource/metabox-front-api
v3.0.17
Published
API for Metabox BASIC configurator
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
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
- npm > 9
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 - 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 domain (HTTPS only)
}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 |
Example:
api.sendCommandToMetabox(new SetProduct('product-uuid'));
api.sendCommandToMetabox(new SetProductMaterial('body', 'red-metallic'));UI 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 | Set zoom level (within configured limits) |
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 |
Example:
// Resume the stream after an idle timeout or network interruption
api.sendCommandToMetabox(new ResumeStream());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 |
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;
}
});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.
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.
For more examples and advanced usage, visit our documentation site.
