npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

  • 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 configuratorId and containerId
  • 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 --save

Import:

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:

  1. CTA Button Label: Custom text (e.g., "Get Quote", "Add to Cart")
  2. Callback URL: HTTP POST endpoint that receives configuration data

CTA Workflow:

User clicks CTA → Metabox POSTs config JSON → Your backend processes → Returns { redirectUrl } → User redirected

Backend 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 configuratorId is 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 UI

Camera 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.

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 orbit mode, distance limits are interpreted relative to the orbit pivot.
  • ResetCamera restores 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 environment

3. 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:

  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

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:

  1. Check the browser console for error messages
  2. Verify your configurator ID in a separate browser tab
  3. Test with a minimal example to isolate the issue
  4. 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 configuratorDataUpdated event 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 localhost for local development.
  • Pin CDN versions in production. Replace @latest with a specific version to avoid unexpected breaking changes.

For more examples and advanced usage, visit our documentation site.