@3dsource/metabox-front-api
v3.0.5
Published
API for Metabox BASIC configurator
Downloads
539
Readme
Metabox Basic Configurator API
A powerful TypeScript API for seamlessly integrating and controlling the Metabox Basic Configurator in your web applications. This API provides full programmatic control over 3D product visualization, products, their material slots, and materials, environments.
Table of Contents
- Features
- Prerequisites
- Installation
- Quick Start
- API Reference
- Standalone Mode
- Examples
- Best Practices
- Troubleshooting
Features
- 🚀 Easy integration with any web framework (Angular, React, Vue, etc.)
- 📱 Responsive design support
- 🎨 Full control over materials, environments, and products
- 📸 Screenshot and PDF generation
- 🎬 Showcase management
- 📦 TypeScript support with full type definitions
- 🌐 CDN support for quick prototyping
Prerequisites
- Modern web browser with ES6 module support
- HTTPS connection (required for unreal engine)
- Valid Metabox basic configurator URL
Environment requirements (per package.json engines):
- Node.js >= 20
- npm > 9
Important: integrateMetabox validates inputs at runtime. It requires a non-empty configuratorId and a non-empty containerId. The iframe URL is constructed internally as https://{domain}/metabox-configurator/basic/{configuratorId} with optional query parameters (introImage, introVideo, loadingImage). HTTPS is enforced and non-HTTPS URLs are rejected.
Installation
Method 1: NPM Package (Recommended)
For use with frameworks like Angular, React, Vue, etc.:
npm install @3dsource/metabox-front-api@latest --save
# or pin a specific versionThen import the required components:
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, GetPdf, saveImage } from '@3dsource/metabox-front-api';Method 2: CDN Import
Alternative CDN options:
- jsDelivr (latest):
https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm
For quick prototyping or vanilla JavaScript projects:
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, GetPdf, saveImage } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';Quick Start
1. HTML Setup
Create a container element where the Metabox 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 Example
<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
api.sendCommandToMetabox(new SetProduct('product-1'));
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1280, y: 720 }));
});
</script>TypeScript Example
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, GetPdf, ShowEmbeddedMenu, ShowOverlayInterface, saveImage } from '@3dsource/metabox-front-api';
class MetaboxIntegrator {
private api: Communicator | null = null;
constructor() {
this.initialize();
}
private async initialize(): Promise<void> {
try {
// Replace 'configurator-id' with your actual configurator ID
const configuratorId = 'configurator-id';
// Initialize the configurator with type safety
integrateMetabox(configuratorId, 'embed3DSource', (api: Communicator) => {
this.onApiReady(api);
});
} catch (error) {
console.error('Failed to initialize Metabox:', error);
}
}
private onApiReady(api: Communicator): void {
this.api = api;
console.log('Metabox API is ready!');
// Set up event listeners with proper typing
this.setupEventListeners();
// Configure initial state
this.configureInitialState();
}
private setupEventListeners(): void {
if (!this.api) return;
// Listen for configurator data updates
this.api.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
console.log('Configurator data updated:', data);
this.handleConfiguratorUpdate(data);
});
// Listen for screenshot/render completion
this.api.addEventListener('screenshotReady', (imageData: string) => {
console.log('Screenshot ready');
saveImage(imageData, 'configurator-screenshot.png');
});
}
private handleConfiguratorUpdate(data: ConfiguratorEnvelope): void {
// Handle configurator state changes with full type safety
// Access ConfiguratorEnvelope properties.
// Example: Update UI based on current product
if (data) {
console.log('Current state:', data);
}
}
private configureInitialState(): void {
if (!this.api) return;
// Set initial product
this.api.sendCommandToMetabox(new SetProduct('your-product-id'));
// Set initial environment
this.api.sendCommandToMetabox(new SetEnvironment('your-environment-id'));
// Apply material to a specific slot
this.api.sendCommandToMetabox(new SetProductMaterial('slot-id', 'material-id'));
// Configure UI visibility
this.api.sendCommandToMetabox(new ShowEmbeddedMenu(false));
this.api.sendCommandToMetabox(new ShowOverlayInterface(false));
// Generate initial screenshot
this.api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1024, y: 1024 }));
}
// Public method to change product
public changeProduct(productId: string): void {
if (this.api) {
this.api.sendCommandToMetabox(new SetProduct(productId));
}
}
// Public method to apply material
public applyMaterial(slotId: string, materialId: string): void {
if (this.api) {
this.api.sendCommandToMetabox(new SetProductMaterial(slotId, materialId));
}
}
// Public method to take screenshot
public takeScreenshot(width: number = 1024, height: number = 1024): void {
if (this.api) {
this.api.sendCommandToMetabox(new GetScreenshot('image/png', { x: width, y: height }));
}
}
}
// Initialize the integrator
const metaboxIntegrator = new MetaboxIntegrator();
// Example usage:
// metaboxIntegrator.changeProduct('new-product-id');
// metaboxIntegrator.applyMaterial('slot-1', 'material-2');
// metaboxIntegrator.takeScreenshot(1920, 1080);API Reference
System Terminology
- product - 3D digital twin of the actual physical product within a Metabox system; concept is used in Basic configurator.
- productId - Unique Product ID within the Metabox system; mandatory. Assigned by the Metabox system automatically after the product's 3D digital twin is uploaded into Metabox.
- environment - 3D digital twin of the actual environment (room scene or other) within a Metabox system.
- environmentId - Unique environment ID within the Metabox system; mandatory. Assigned by the Metabox system automatically after the environment's 3D digital twin is uploaded into Metabox.
- externalId - Product or Material ID assigned to product or material in Metabox; optional; can be a combination of letters or numbers or both. Should be identical to the product SKU number in any other non-Metabox e-commerce system that is used for managing products, so Metabox can be integrated with such a system.
- showcase - Exists as an attribute of product only; optional. If present, it means the product's digital twin contains a camera sequence that causes the video mode added automatically to the configurator by Metabox system to play this camera sequence.
- component - Product in the Modular configurator context.
- componentId - Unique Component ID within the Metabox system; mandatory. Assigned by the Metabox system automatically after the Modular configurator is created in Metabox.
- componentType - Exists in Modular Configurator only; the name of the type of the product component, which can include one or several separate products of the same category (examples of product component types: truck wheel, truck bumper). It is set by an unreal artist while creating the digital twin model and is automatically added to the Metabox system after this twin is uploaded to Metabox.
- E‑Com Configurator — can be created from any Basic or Modular Configurator by adding a CTA (call‑to‑action) button to the standard configurator right‑nav menu in the Metabox Configurator Manager (where users create and edit configurators).
To enable the CTA button, add two mandatory parameters in the Configurator Manager:
- Text label for the CTA button (examples: "Get Quote", "Contact Dealer").
- Callback URL for an HTTP POST request. Response requirements:
When the user clicks the CTA button in the configurator:{ "redirectUrl": "https://your.site/thank-you-or-checkout" }- Metabox generates a JSON payload with the current configuration and sends it to the specified Callback URL.
- Your backend decides how to use the configuration JSON (e.g., generate a PDF or build a product list).
- After your endpoint responds with a JSON containing "redirectUrl", the user is redirected in a new browser tab to that URL (a non‑Metabox page on your side) to complete the flow (e.g., leave contact information, proceed to checkout, etc.).
Steps to build a custom menu
- configuratorDataUpdated - add event listener to receive configurator data and updates
- showEmbeddedMenu - hide embedded metabox right sidebar; show again with the command
- showOverlayInterface - hide viewport metabox actions if you want to add yours
- setEnvironment - set active environment when you want to change environment
- setEnvironmentMaterialById - set environment material by slot ID
- setProduct - set active product when you want to change the product
- setProductMaterialById - set product material by slot ID
- getScreenshot - get screenshot
- getCallToActionInformation - send call to action information to api endpoint provided in CTA configuration
- getPdf - get PDF.
Core Functions
Integrates the Metabox configurator into the specified container element.
TypeScript Signature:
function integrateMetabox(
configuratorId: string,
containerId?: string, // defaults to 'embed3DSource'
apiReadyCallback: (api: Communicator) => void, // required
config?: IntegrateMetaboxConfig,
): void;Parameters:
configuratorId(string, required): Basic Configurator ID (not a full URL). Internal iframe src:https://{domain}/metabox-configurator/basic/{configuratorId}.containerId(string, optional): Host element id. Defaults toembed3DSource.apiReadyCallback(required): Invoked once Metabox signals readiness.config(optional):standalone?: Disable built‑in UI/template logic inside iframe.introImage?,introVideo?,loadingImage?: Optional URLs appended as query params.state?: Optional initial configurator state.domain?: Override domain (HTTPS enforced).
Throws: Error if configuratorId or containerId empty, container element missing, or generated URL not HTTPS.
Notes:
- HTTPS is enforced: attempts to use non-HTTPS iframe URLs are rejected.
- If a previous iframe with id
embeddedContentexists, it will be removed before inserting a new one.
Example:
import { IntegrateMetaboxConfig } from '@3dsource/metabox-front-api';
const config: IntegrateMetaboxConfig = {
introImage: 'https://example.com/intro.png',
loadingImage: 'https://example.com/loading.png',
standalone: false,
};
integrateMetabox(
'configurator-id',
'embed3DSource',
(api) => {
console.log('API ready!', api);
},
config,
);Command Classes
All commands are sent using api.sendCommandToMetabox(new SomeCommand(...)).
Command Payload Types (Internal)
Product & Environment
SetProduct(productId: string)– Select active product.SetProductMaterial(slotId: string, materialId: string)– Apply material to product slot.SetEnvironment(id: string)– Activate environment.SetEnvironmentMaterial(slotId: string, materialId: string)– Apply material to environment slot.
UI & Overlay
ShowEmbeddedMenu(visible: boolean)– Toggle right sidebar menu.ShowOverlayInterface(visible: boolean)– Toggle viewport overlay UI.
Camera & View
ResetCamera()– Reset camera to initial state.ApplyZoom(zoom: number)– Apply a zoom factor within allowed range.
Showcase Control
InitShowcase()– Initialize a product showcase sequence.PlayShowcase()/PauseShowcase()/StopShowcase()– Control showcase playback.
Media & Export
GetScreenshot(format: MimeType, size?)– Request a screenshot (listen toscreenshotReady).GetPdf()– Request PDF generation (no dedicated event, handled internally server‑side; poll status messages if needed).GetCallToActionInformation()– Trigger CTA flow (backend must return{ redirectUrl }).
Advanced / Experimental
UnrealCommand(command: object)– Send low‑level command to Unreal. Use cautiously.MetaboxConfig(appId: string, partialConfig)– Internal initialization command (auto‑sent on communicator creation).
Example:
import { UnrealCommand } from '@3dsource/metabox-front-api';
api.sendCommandToMetabox(new UnrealCommand({ type: 'SetLightIntensity', value: 2.5 }));Event Handling
The API uses an event-driven architecture. Register listeners via api.addEventListener(eventName, handler).
Available Events
| Event | Payload Type | Description |
| ----------------------------- | -------------------- | ---------------------------------------------------------- | ------ | -------------------------- |
| configuratorDataUpdated | ConfiguratorEnvelope | Product/environment/material selections changed. |
| ecomConfiguratorDataUpdated | EcomConfigurator | CTA config (label, callbackUrl) updated. |
| dataChannelConnected | boolean | Unreal data channel connectivity state. |
| viewportReady | boolean | Unreal viewport visibility and readiness. |
| showcaseStatusChanged | ShowCaseStatus | Showcase playback status: init/play/pause/stop. |
| statusMessageChanged | string | null | Human‑readable loading/progress message. |
| screenshotReady | string | null | Base64 image data from latest screenshot (null if failed). |
| videoResolutionChanged | { width: number | null; height: number | null } | Video stream size changes. |
Example: Basic State Listener
api.addEventListener('configuratorDataUpdated', (data) => {
console.log('Product:', data.productId);
console.log('Product materials:', data.productMaterialsIds);
console.log('Environment:', data.environmentId);
console.log('Environment materials:', data.environmentMaterialsIds);
});Screenshot
api.addEventListener('screenshotReady', (image) => {
if (!image) return;
});pdfGenerated
Fired when PDF generation is completed.
api.addEventListener('pdfGenerated', (pdfData: string) => {
console.log('PDF generated');
// Handle PDF data
});Utility Functions
saveImage
Utility function to save rendered images to a file.
function saveImage(imageUrl: string, filename: string): void;Parameters:
imageUrl: The URL of the image to save (can be base64 encoded image data)filename: The name of the file to save
Example:
api.addEventListener('screenshotReady', (imageData: string | null) => {
saveImage(imageData ?? '', 'my-render.png');
});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';
integrate -
metabox.ts(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`);
});
// Listen for errors
api.addEventListener('error', (error) => {
console.error('Configurator error:', error);
showError('An error occurred in the configurator: ' + error.message);
});
} 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`);
}
});
// Listen for errors from the configurator
apiInstance.addEventListener('error', (error: any) => {
if (mounted) {
handleError(`Configurator error: ${error.message || error}`);
}
});
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, 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
- Proper Lifecycle Management: OnDestroy implementation with cleanup
- 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
- Component Cleanup: Prevents memory leaks and handles component destruction
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
Best Practices
1. Error Handling
Always implement proper error handling when working with the API:
integrateMetabox(configuratorId, containerId, (api) => {
try {
// Set up event listeners with error handling
api.addEventListener('configuratorDataUpdated', (data) => {
try {
handleConfiguratorUpdate(data);
} catch (error) {
console.error('Error handling configurator update:', error);
}
});
// Send commands with error handling
api.sendCommandToMetabox(new SetProduct(productId));
} catch (error) {
console.error('Error initializing configurator:', error);
}
});2. State Management
Keep track of the configurator state for a better user experience:
class ConfiguratorStateManager {
private currentState = {
product: null,
materials: {},
environment: null,
};
updateState(data) {
this.currentState = { ...this.currentState, ...data };
this.saveStateToLocalStorage();
}
saveStateToLocalStorage() {
localStorage.setItem('configuratorState', JSON.stringify(this.currentState));
}
loadStateFromLocalStorage() {
const saved = localStorage.getItem('configuratorState');
return saved ? JSON.parse(saved) : null;
}
}3. Performance Optimization
- Debounce rapid commands: Avoid sending too many commands in quick succession
// Debounce example
const debouncedMaterialChange = debounce((slotId, materialId) => {
api.sendCommandToMetabox(new SetProductMaterial(slotId, materialId));
}, 300);4. Responsive Design
Ensure the configurator works well on different screen sizes:
#embed3DSource {
width: 100%;
height: 60vh;
min-height: 400px;
max-height: 800px;
}
@media (max-width: 768px) {
#embed3DSource {
height: 50vh;
min-height: 300px;
}
}5. Loading States
Provide feedback to users while the configurator loads:
const showLoadingState = () => {
document.getElementById('loading').style.display = 'block';
};
const hideLoadingState = () => {
document.getElementById('loading').style.display = 'none';
};
integrateMetabox(configuratorId, containerId, (api) => {
hideLoadingState();
// Continue with initialization
});Troubleshooting
Common Issues
1. Configurator Not Loading
Problem: The configurator iframe doesn't appear or shows a blank screen.
Solutions:
- Verify the configurator ID is correct and accessible
- Ensure the container element exists in the DOM before calling
integrateMetabox - Check that you're using HTTPS (required for Unreal Engine)
- Verify your configurator ID is valid
// Check if container exists
const container = document.getElementById('embed3DSource');
if (!container) {
console.error('Container element not found');
return;
}
// Verify HTTPS
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
console.warn('HTTPS is required for Metabox configurator');
}2. Commands Not Working
Problem: Commands sent to the configurator don't have any effect.
Solutions:
- Ensure the API is fully initialized before sending commands
- Check that product/material/environment IDs are correct
- Verify the configurator supports the specific command
let apiReady = false;
integrateMetabox(configuratorId, containerId, (api) => {
apiReady = true;
// Now it's safe to send commands
});
// Wait for API to be ready
const sendCommand = (command) => {
if (apiReady && api) {
api.sendCommandToMetabox(command);
} else {
console.warn('API not ready yet');
}
};3. Events Not Firing
Problem: Event listeners don't receive events from the configurator.
Solutions:
- Set up event listeners immediately after API initialization
- Check event names for typos
- Ensure the configurator is configured to send the expected events
integrateMetabox(configuratorId, containerId, (api) => {
// Set up listeners immediately
api.addEventListener('configuratorDataUpdated', (data) => {
console.log('Event received:', data);
});
// Test if events are working
setTimeout(() => {
console.log('Testing event system...');
api.sendCommandToMetabox(new SetProduct('test-product'));
}, 1000);
});4. Screenshot/PDF Generation Issues
Problem: Screenshots or PDFs are not generated or are of poor quality.
Solutions:
- Wait for the scene to fully load before capturing
- Use appropriate dimensions for your use case
- Ensure the configurator viewport is visible (not hidden or zero-sized)
// Wait for scene to load before screenshot
api.addEventListener('configuratorDataUpdated', (data) => {
// Scene is loaded, safe to take screenshot
setTimeout(() => {
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
}, 500);
});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 and type
- Configurator URL (if possible)
- Console error messages
- Steps to reproduce the issue
Standalone Mode
Standalone mode lets you embed the Metabox renderer without the built‑in Metabox UI and template logic. This is ideal when you want to build a fully custom UI (menus, buttons, flows) and drive the configurator entirely through this API.
When standalone is enabled, the embedded page does not render Metabox’s internal menus, overlays, or template logic. You are responsible for sending commands (e.g., setProduct, setMaterial, setEnvironment) and listening to events to keep your UI in sync.
Key points:
- Purpose: use when you need a 100% custom interface and logic on the host page.
- Behavior: Metabox default UI, embedded menu, and template logic are disabled on the iframe side.
- Control: you drive everything with API commands and events.
How to enable
Pass as param to config with a standalone set to true when calling integrateMetabox.
TypeScript example:
import { integrateMetabox, Communicator, SetProduct, SetEnvironment } from '@3dsource/metabox-front-api';
integrateMetabox(
'configurator-id',
'embed3DSource',
(api: Communicator) => {
// Send your initial commands to establish state
api.sendCommandToMetabox(new SetProduct('your-product-id'));
api.sendCommandToMetabox(new SetEnvironment('your-environment-id'));
},
{ standalone: true },
);JavaScript (CDN) example:
<script type="module">
import { integrateMetabox, SetProduct, SetEnvironment } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';
integrateMetabox(
'configurator-id',
'embed3DSource',
(api) => {
api.sendCommandToMetabox(new SetProduct('your-product-id'));
api.sendCommandToMetabox(new SetEnvironment('your-environment-id'));
},
{ standalone: true },
);
</script>Notes and tips:
- In standalone mode, nothing will change until you send initial commands (e.g., set product/environment). If you see a blank/default view, ensure you dispatch the necessary SetProduct/SetEnvironment commands once api is ready.
- All events (like configuratorDataUpdated, screenshotReady) still work; use them to keep your UI synchronized.
- Runtime validation still applies: configuratorId and containerId must be non-empty. HTTPS is enforced for the internally constructed iframe URL. The hostUrl and apiVersion values are handled internally; you do not need to pass them.
