@3dsource/metabox-front-api

中文文档
3.0.33发布于 2026/10/07UNLICENSEDTypeScriptES Module近 30 天下载 2307

Client API for the Metabox Basic configurator

原始使用文档
文档目录29 节
  1. Table of Contents
  2. Features
  3. Prerequisites
  4. Installation
  5. NPM Installation (Recommended)
  6. CDN Installation (Quick Prototyping)
  7. Quick Start
  8. 1. HTML Setup
  9. 2. Basic Integration
  10. API Reference
  11. System Terminology
  12. Core Integration — integrateMetabox()
  13. Command Classes
  14. Event Handling
  15. Utility Functions
  16. Standalone Mode
  17. When to Use Standalone Mode
  18. Behavior Changes
  19. Implementation
  20. Building a Custom UI
  21. Examples
  22. E‑Com CTA example
  23. Basic Product Configurator
  24. React Integration Example
  25. Angular Integration Example
  26. Troubleshooting
  27. Common Issues
  28. Getting Help
  29. Tips & Tricks

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

Peer Dependencies:

  • rxjs >=7.8.0 <8.0.0

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, state, showLoadingProgress, showResumeStreamPopup, showUserInactivityTimer, userInactivityTimeout, autoStartStream
  • HTTPS is strictly enforced (HTTP URLs will be rejected)

Installation

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

TermDescriptionContext
product3D digital twin of physical product in Metabox systemBasic configurator
productIdUnique product identifier (system-generated UUID)All contexts
environment3D scene/room environment for product visualizationVisual context
environmentIdUnique environment identifier (system-generated UUID)Environment management
externalIdCustom SKU/identifier for integration with external systemsE-commerce integration
showcaseCamera animation sequence attached to productProduct attribute
slotIdMaterial slot identifier on product/environmentMaterial assignment
materialIdUnique material identifierMaterial 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:

ParameterTypeRequiredDefaultDescription
configuratorIdstring✅-Configurator UUID (not full URL)
containerIdstring✅-DOM container element ID
apiReadyCallbackfunction✅-Callback invoked when API is ready
configIntegrateMetaboxConfig❌{}Additional configuration options

Config Options (IntegrateMetaboxConfig):

interface IntegrateMetaboxConfig {
  standalone?: boolean; // Disable built-in Metabox UI
  introImage?: string; // URL for intro image
  introVideo?: string; // URL for intro video
  loadingImage?: string; // URL for loading spinner
  state?: string; // Predefined state for initial loading in rison format (see https://github.com/Nanonid/rison)
  domain?: string; // Override hostname, without the https:// prefix
  showLoadingProgress?: boolean; // Show loading progress bar inside the configurator
  showResumeStreamPopup?: boolean; // Show resume stream popup when the stream is paused
  showUserInactivityTimer?: boolean; // Show user inactivity countdown popup before disconnecting
  userInactivityTimeout?: number; // Inactivity timeout in seconds before the user is warned and disconnected
  autoStartStream?: boolean; // Whether the pixel stream starts automatically on load (set false to start it manually via ResumeStream)
}

Validation & Errors:

  • Throws if configuratorId is empty
  • Throws if a container element not found
  • Throws if URL is not HTTPS
  • Replaces existing iframe with ID embeddedContent
  • Grants the iframe autoplay; fullscreen; encrypted-media, plus tools in browsers with WebMCP, so an in-browser AI agent on your page can use the tools the configurator registers. If you build the iframe yourself, add tools to its allow attribute for the same effect.

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

CommandParametersDescription
SetProductproductId: stringSwitch to specified product
SetProductMaterialslotId: string, materialId: stringApply material to product slot
SetEnvironmentenvironmentId: stringChange environment/scene
SetEnvironmentMaterialslotId: string, materialId: stringApply material to environment slot
SetMaterialscommands: MaterialCommand[]Apply several materials at once in a single message
ResetConfigurationNoneReset configurator state to defaults (product, materials, environment)

Example:

api.sendCommandToMetabox(new SetProduct('product-uuid'));
api.sendCommandToMetabox(new SetProductMaterial('body', 'red-metallic'));
api.sendCommandToMetabox(new ResetConfiguration()); // Restore template defaults

SetMaterials applies a whole material set in one message instead of one command per slot — use it for presets, saved configurations, or a material set synced from an external system. It accepts SetProductMaterial and SetEnvironmentMaterial instances and preserves their order:

api.sendCommandToMetabox(new SetMaterials([new SetProductMaterial('body', 'red-metallic'), new SetProductMaterial('trim', 'brushed-steel'), new SetEnvironmentMaterial('floor', 'oak-planks')]));

UI Control

  • Note: ShowEmbeddedMenu and ShowOverlayInterface commands are ineffective in standalone mode.
CommandParametersDescription
ShowEmbeddedMenuvisible: booleanShow/hide right sidebar
ShowOverlayInterfacevisible: booleanShow/hide viewport controls

Example:

api.sendCommandToMetabox(new ShowEmbeddedMenu(false)); // Hide for custom UI

Camera Control

CommandParametersDescription
SetCameracamera: CameraCommandPayloadSet camera position, rotation, fov and restrictions
GetCameraNoneRequest current camera; returns via getCameraResult event
ResetCameraNoneReset camera to default position
ApplyZoomzoom: numberApply zoom delta (positive=in, negative=out)

Example:

api.sendCommandToMetabox(new ApplyZoom(50)); // Zoom in
api.sendCommandToMetabox(new ApplyZoom(-25)); // Zoom out
api.sendCommandToMetabox(new ResetCamera()); // Reset view
api.sendCommandToMetabox(
  new SetCamera({
    fov: 45,
    mode: 'orbit',
    position: { x: 100, y: 100, z: 100 },
    rotation: { horizontal: 0, vertical: 0 },
    restrictions: {
      maxDistanceToPivot: 0,
      maxFov: 0,
      maxHorizontalRotation: 0,
      maxVerticalRotation: 0,
      minDistanceToPivot: 0,
      minFov: 0,
      minHorizontalRotation: 0,
      minVerticalRotation: 0,
    },
  }),
);

// Request current camera state
api.sendCommandToMetabox(new GetCamera());

// Listen for the camera data
api.addEventListener('getCameraResult', (camera) => {
  console.log('Current camera:', camera);
  // camera is of type CameraCommandPayload
});
CameraCommandPayload

The payload used by the SetCamera command to configure the camera.

interface CameraCommandPayload {
  fov?: number;
  mode?: 'fps' | 'orbit';
  position?: { x: number; y: number; z: number };
  rotation?: {
    horizontal: number;
    vertical: number;
  };
  restrictions?: {
    maxDistanceToPivot?: number;
    maxFov?: number;
    maxHorizontalRotation?: number;
    maxVerticalRotation?: number;
    minDistanceToPivot?: number;
    minFov?: number;
    minHorizontalRotation?: number;
    minVerticalRotation?: number;
  };
}
  • fov (number): Field of view in degrees.
  • mode ('fps' | 'orbit'):
    • fps: First‑person style camera, moves freely in space.
    • orbit: Orbiting camera around a pivot (typical product viewer behavior).
  • position ({ x, y, z }): Camera position in world coordinates.
  • rotation ({ horizontal, vertical }): Camera rotation angles in degrees.
    • horizontal: Yaw (left/right).
    • vertical: Pitch (up/down).
  • restrictions (optional): Limits applied by the viewer to constrain user/camera movement.
    • Distance limits (orbit mode): minDistanceToPivot, maxDistanceToPivot.
    • Field‑of‑view limits (first-person mode): minFov, maxFov.
    • Rotation limits: minHorizontalRotation, maxHorizontalRotation, minVerticalRotation, maxVerticalRotation.

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

CommandParametersDescription
InitShowcaseNoneInitialize showcase (load animation)
PlayShowcaseNoneStart/resume animation playback
PauseShowcaseNonePause animation
StopShowcaseNoneStop and reset animation

Measurement Tools

CommandParametersDescription
ShowMeasurementNoneDisplay product dimensions
HideMeasurementNoneHide dimension overlay

Stream Control

CommandParametersDescription
ResumeStreamNoneResume pixel streaming session after pause or disconnection
StopStreamNoneStop the pixel streaming session and release its resources
ResetUserInactivityTimerNoneReset the AFK inactivity timer to keep the session alive

Example:

// Resume the stream after an idle timeout or network interruption
api.sendCommandToMetabox(new ResumeStream());

// Stop the stream, e.g. when closing a modal or navigating away
api.sendCommandToMetabox(new StopStream());

// Reset the inactivity timer when user confirms they are still present
api.sendCommandToMetabox(new ResetUserInactivityTimer());

Export & Media

CommandParametersDescriptionEvent Triggered
GetScreenshotmimeType: MimeType, size?: {x: number, y: number}Render screenshotscreenshotReady
GetPdfNoneGenerate PDF exportServer-side (no event)
GetCallToActionInformationNoneTrigger CTA workflowBackend 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

EventPayload TypeDescriptionUse Case
configuratorDataUpdatedConfiguratorEnvelopeConfiguration state changedSync UI with product/material/environment
ecomConfiguratorDataUpdatedEcomConfiguratorCTA configuration loadedDisplay CTA button with label
viewportReadyboolean3D viewport ready stateHide loading, enable interactions
showcaseStatusChangedShowCaseStatusAnimation playback statusUpdate play/pause button state
statusMessageChangedstring | nullLoading/progress messageDisplay user feedback
screenshotReadystring | nullBase64 screenshot dataDownload or display image
getCameraResultCameraCommandPayloadCurrent camera data returnedCapture/store current camera
videoResolutionChangedVideoResolutionStream resolution changedAdjust viewport layout
videoStatsUpdatedVideoStatsVideo stream quality metricsQoS dashboards, adaptive behavior
loadingStatusChanged{ percents: number; loaderVisible: boolean }Loading progress or overlay changedShow custom loading progress bar
userInactivityDetected{ remainingSecondsToDisconnect: number; countdownVisible: boolean }AFK countdown started or updatedShow "Stay connected?" prompt
configuratorNotFoundtrueThe configurator id does not existShow an error instead of waiting

Event Examples

Configuration State Sync:

api.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
  console.log('Product:', data.productId);
  console.log('Materials:', data.productMaterialsIds);
  console.log('Environment:', data.environmentId);

  // Update custom UI
  updateProductSelector(data.productId);
  updateMaterialSwatches(data.productMaterialsIds);
});

Screenshot Handling:

api.addEventListener('screenshotReady', (imageData: string | null) => {
  if (!imageData) {
    console.error('Screenshot failed');
    return;
  }

  saveImage(imageData, `product-${Date.now()}.png`);
});

Loading State:

api.addEventListener('viewportReady', (isReady: boolean) => {
  if (isReady) {
    hideLoadingSpinner();
    enableComponentSelectors();
  }
});

api.addEventListener('statusMessageChanged', (message: string | null) => {
  document.getElementById('status-text').textContent = message || '';
});

Showcase Control:

api.addEventListener('showcaseStatusChanged', (status: ShowCaseStatus) => {
  const playButton = document.getElementById('play-btn');

  switch (status) {
    case 'play':
      playButton.textContent = 'Pause';
      break;
    case 'pause':
    case 'stop':
      playButton.textContent = 'Play';
      break;
  }
});

Loading Progress:

api.addEventListener('loadingStatusChanged', ({ percents, loaderVisible }) => {
  const bar = document.getElementById('progress-bar');
  if (bar) {
    bar.style.display = loaderVisible ? 'block' : 'none';
    bar.style.width = `${percents}%`;
  }
});

AFK Inactivity Countdown:

import { ResetUserInactivityTimer } from '@3dsource/metabox-front-api';

api.addEventListener('userInactivityDetected', ({ remainingSecondsToDisconnect, countdownVisible }) => {
  const dialog = document.getElementById('afk-dialog');
  const countdown = document.getElementById('afk-countdown');

  if (dialog) dialog.style.display = countdownVisible ? 'flex' : 'none';
  if (countdown) countdown.textContent = String(remainingSecondsToDisconnect);
});

document.getElementById('stay-connected-btn')?.addEventListener('click', () => {
  api.sendCommandToMetabox(new ResetUserInactivityTimer());
});

Utility Functions

saveImage(imageUrl: string, filename: string): void

Downloads base64-encoded image to user's device.

Parameters:

ParameterTypeDescription
imageUrlstringBase64 data URL (e.g., data:image/png;base64,...)
filenamestringDesired filename with extension

Example:

api.addEventListener('screenshotReady', (imageData: string | null) => {
  if (imageData) {
    saveImage(imageData, `config-${Date.now()}.png`);
  }
});

Implementation Note: Creates a temporary anchor element with download attribute to trigger browser download.

Accepted input: only a base64 data URL of type image/png, image/jpeg or image/webp — the formats GetScreenshot produces. Anything else (a remote https:// image, an svg+xml or text/html data URL) is ignored with a console warning instead of being downloaded, since this function is fed event payloads straight off the postMessage channel.

fromCommunicatorEvent(target: Communicator, eventName: string): Observable

RxJS wrapper for Communicator events. Works like RxJS fromEvent — creates a typed Observable that emits each time the specified event fires. Alternative to api.addEventListener().

Parameters:

ParameterTypeDescription
targetCommunicatorThe Communicator instance to listen on
eventNamestringEvent 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

FeatureDefault ModeStandalone Mode
Right sidebar menuVisibleHidden
Viewport overlaysVisibleHidden
Template logicActiveDisabled
API controlPartialFull
Event systemAvailableAvailable

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:

IssueDiagnosticSolution
Invalid configurator IDBrowser console shows 404 errorsVerify configurator ID in Metabox admin
Container not foundError: Container element not foundEnsure element exists before calling integrateMetabox()
HTTP instead of HTTPSMixed content warningsUse HTTPS or test on localhost
Container has no dimensionsInvisible iframe (0x0)Set explicit width/height on container element
CORS/iframe blockingX-Frame-Options errorsCheck 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:

MistakeProblemSolution
Sending before API readyCommands ignoredOnly send in apiReadyCallback
Wrong product/material IDsEntity not foundVerify IDs from configuratorDataUpdated event
Wrong slot IDMaterial rejectedVerify 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 defaul

版本与发布记录

@3dsource/metabox-front-api 共发布 140 个版本,首次发布于 2025/04/03;过去 12 个月发布了 43 个版本。

版本发布日期距今
3.0.33latest2026/10/071 个月前
3.0.322026/08/282 个月前
3.0.32-dev.0dev2026/08/282 个月前
3.0.312026/08/212 个月前
3.0.302026/08/172 个月前
3.0.292026/08/152 个月前

其他发布通道:beta → 0.0.19-beta.6,dev → 3.0.32-dev.0。

关于 @3dsource/metabox-front-api 的常见问题

@3dsource/metabox-front-api 是什么?

Client API for the Metabox Basic configurator

如何安装 @3dsource/metabox-front-api?

在项目目录运行 npm install @3dsource/metabox-front-api,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:rxjs(>=7.8.0 <8.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@3dsource/metabox-front-api 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@3dsource/metabox-front-api 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@3dsource/metabox-front-api 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@3dsource/metabox-front-api 可以商用吗?

@3dsource/metabox-front-api 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@3dsource/metabox-front-api 的下载量有多少?

根据 npm 官方下载统计,@3dsource/metabox-front-api 在 2026/10/05 – 2026/11/03 这 30 天内被下载 2,307 次,日均约 77 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@3dsource/metabox-front-api 还在维护吗?

最新版本 3.0.33 发布于 2026/10/07(1 个月前);至今共发布 140 个版本,其中过去 12 个月发布了 43 个;首次发布于 2025/04/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。