@3dsource/metabox-front-api
Version:
API for Metabox BASIC configurator
1,506 lines (1,261 loc) • 56.8 kB
Markdown
# 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](#features)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Method 1: NPM Package (Recommended)](#method-1-npm-package-recommended)
- [Method 2: CDN Import](#method-2-cdn-import)
- [Quick Start](#quick-start)
- [1. HTML Setup](#1-html-setup)
- [2. Basic Integration](#2-basic-integration)
- [JavaScript Example](#javascript-example)
- [TypeScript Example](#typescript-example)
- [API Reference](#api-reference)
- [System Terminology](#system-terminology)
- [Steps to build a custom menu](#steps-to-build-a-custom-menu)
- [Core Functions](#core-functions)
- [Command Classes](#command-classes)
- [Command Payload Types (Internal)](#command-payload-types-internal)
- [Product & Environment](#product--environment)
- [UI & Overlay](#ui--overlay)
- [Camera & View](#camera--view)
- [Showcase Control](#showcase-control)
- [Media & Export](#media--export)
- [Advanced / Experimental](#advanced--experimental)
- [Event Handling](#event-handling)
- [Available Events](#available-events)
- [Example: Basic State Listener](#example-basic-state-listener)
- [Utility Functions](#utility-functions)
- [Standalone Mode](#standalone-mode)
- [Examples](#examples)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)
- [Common Issues](#common-issues)
## 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.:
```bash
npm install @3dsource/metabox-front-api@latest --save
# or pin a specific version
```
Then import the required components:
```typescript
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:
```javascript
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:
```html
<!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
```html
<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
```typescript
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:
```json
{
"redirectUrl": "https://your.site/thank-you-or-checkout"
}
```
When the user clicks the CTA button in the configurator:
- 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:**
```typescript
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 to `embed3DSource`.
- `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 `embeddedContent` exists, it will be removed before inserting a new one.
**Example:**
```typescript
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 to `screenshotReady`).
- `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:
```typescript
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
```typescript
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
```typescript
api.addEventListener('screenshotReady', (image) => {
if (!image) return;
});
```
##### `pdfGenerated`
Fired when PDF generation is completed.
```typescript
api.addEventListener('pdfGenerated', (pdfData: string) => {
console.log('PDF generated');
// Handle PDF data
});
```
### Utility Functions
#### `saveImage`
Utility function to save rendered images to a file.
```typescript
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:**
```typescript
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.
```html
<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.
```html
<!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
```tsx
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
```typescript
// 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:**
```typescript
// 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:**
1. Install the package: `npm install @3dsource/metabox-front-api@latest`
2. Import the component in your Angular module
3. Replace `'configurator-id'` with your actual configurator ID
4. Replace placeholder IDs with your real product, material, and slot IDs
5. Customize the styling by modifying the component styles
## Best Practices
### 1. Error Handling
Always implement proper error handling when working with the API:
```typescript
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:
```typescript
class ConfiguratorStateManager {
private currentState = {
product: null,
materials: {},
environment: null,
};
updateState(data) {
this.currentState = { ...this.currentState, ...data };
this.saveStateToLocalStorage();
}