# @3dsource/metabox-front-api 中文文档

规范地址：https://npmcn.com/package/%403dsource/metabox-front-api/

版本：3\.0\.33
npm 元数据更新时间：2026-10-07T06:23:18.463Z
中文译文更新时间：2026-11-06T21:41:27.413Z
软件包许可：UNLICENSED

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%403dsource/metabox-front-api
原文对照：https://npmcn.com/package/%403dsource/metabox-front-api/?lang=original
项目主页：https://bitbucket.org/dreamsoft/front-libraries#readme
源码仓库：https://bitbucket.org/dreamsoft/front-libraries

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：318 kB，7 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=20
- 运行依赖：1 个
- peer 依赖：rxjs \>=7\.8\.0 \<8\.0\.0
- 首次发布：2025-04-02T17:40:19.757Z
- 最新版本发布：2026-10-07T06:23:18.267Z
- 近 30 天下载量：2307（2026-10-05 至 2026-11-03，npm 官方下载统计）
- 已发布版本数：140（近 12 个月 43 个）

## 中文简介

元框基本配置的客户端 API

## 安装

```sh
npm install @3dsource/metabox-front-api
```

也可以使用：`pnpm add @3dsource/metabox-front-api`、`yarn add @3dsource/metabox-front-api`、`bun add @3dsource/metabox-front-api`

## README

# 元框基本配置 API

TypeScript/JavaScript API用于将Metabox Basic配置器整合到网络应用程序. 提供3D产品可视化,材料,环境和配置状态的程序控制.

## 目录

-   [特征](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-features)
-   [先决条件](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-prerequisites)
-   [安装](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-installation)
    -   [国家防范机制安装(建议)](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-npm-installation-recommended)
    -   [CDN 安装( 快速原型)](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-cdn-installation-quick-prototyping)
-   [快速启动](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-quick-start)
    -   [1\. HTML 设置](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-1-html-setup)
    -   [2\. 基本融合](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-2-basic-integration)
        -   [JavaScript (ES6 模块)](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-javascript-es6-modules)
        -   [类型脚本/ 角度/ 反应](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-typescriptangularreact)
-   [API 参考](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-api-reference)
    -   [系统术语](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-system-terminology)
    -   [核心一体化—— `integrateMetabox()`](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-core-integration--integratemetabox)
    -   [命令类](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-command-classes)
        -   [产品和环境管理](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-product--environment-management)
        -   [UI 控制](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-ui-control)
        -   [相机控制器](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-camera-control)
        -   [显示大小写/ 动画控制](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-showcaseanimation-control)
        -   [测量工具](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-measurement-tools)
        -   [流控](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-stream-control)
        -   [导出媒体( M)](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-export--media)
    -   [事件处理](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-event-handling)
        -   [可用事件](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-available-events)
        -   [事件实例](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-event-examples)
    -   [工具函数](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-utility-functions)
-   [独立模式](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-standalone-mode)
    -   [何时使用独立模式](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-when-to-use-standalone-mode)
    -   [行为改变](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-behavior-changes)
    -   [执行情况](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-implementation)
-   [构建自定义用户界面](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-building-a-custom-ui)
-   [实例](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-examples)
-   [解决问题](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-troubleshooting)
    -   [共同问题](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-common-issues)
        -   [1\. 不装入配置器](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-1-configurator-not-loading)
        -   [2\. 不起作用的命令](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-2-commands-not-working)
-   [提示( T)](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-tips--tricks)
-   [仓库开发](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-repository-development)

## 特征

-   框架不可知融合(Angular、React、Vue、vanilla JS)
-   响应性设计支持
-   对材料、环境和产品的完全控制
-   截图和 PDF 导出能力
-   显示大小写/ 激活回放管理
-   完整类型脚本类型定义
-   CDN 支持快速原型
-   实时状态同步事件驱动架构

## 先决条件

**运行时间要求 :**

-   有ES6模块支持的现代浏览器(Chrome 61+,Firefox 60+,Safari 11+,Edge 79+)
-   HTTPS 连接( 非真实引擎流化需要)
-   有效的元框配置符 ID

**发展环境:**

-   节点.js 20

**同伴依赖关系 :**

-   `rxjs >=7.8.0 <8.0.0`

**重要注释 :**

-   `integrateMetabox()` 运行时验证输入
-   需要非空闲 `configuratorId` 财务报告和审定财务报表 `containerId`
-   Iframe URL 格式 : `https://{domain}/metabox-configurator/basic/{configuratorId}`
-   可选查询参数 : `introImage`, (中文(简体) ), `introVideo`, (中文(简体) ), `loadingImage`, (中文(简体) ), `state`, (中文(简体) ), `showLoadingProgress`, (中文(简体) ), `showResumeStreamPopup`, (中文(简体) ), `showUserInactivityTimer`, (中文(简体) ), `userInactivityTimeout`, (中文(简体) ), `autoStartStream`
-   HTTPS 被严格执行( HTTP URL 将被拒绝)

## 安装

### 国家防范机制安装(建议)

**安装 :**

```bash
npm install @3dsource/metabox-front-api@latest --save
```

**导入 :**

```typescript
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, GetPdf, saveImage } from '@3dsource/metabox-front-api';
```

### CDN 安装( 快速原型)

**jsDelivr: (英语)**

```javascript
import { integrateMetabox, SetProduct, SetProductMaterial, GetScreenshot, saveImage } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';
```

> **说明:** 用于生产, 将特定版本标出, 而不是使用 `@latest`

## 快速启动

### 1\. HTML 设置

确保您的 HTML 页面有以下结构, 元框配置器将在此集成 。 您必须为# embed3D 源元素提供 CSS, 以使配置器响应并适应您的布局需要 。 创建配置将嵌入的容器元素 :

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Metabox Configurator</title>
    <style>
      #embed3DSource {
        width: 100%;
        height: 600px;
        border: 1px solid #ccc;
        border-radius: 8px;
      }
    </style>
  </head>
  <body>
    <div id="embed3DSource">
      <!-- Metabox Configurator will be embedded here -->
    </div>
  </body>
</html>
```

### 2\. 基本融合

#### JavaScript (ES6 模块)

```html
<script type="module">
  import { integrateMetabox, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, saveImage } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';

  const configuratorId = 'configurator-id'; // your actual configurator id
  // Ensure a container with id embed3DSource exists (see HTML Setup)
  integrateMetabox(configuratorId, 'embed3DSource', (api) => {
    api.addEventListener('configuratorDataUpdated', (env) => {
      console.log('State updated:', env.productId, env.environmentId);
    });
    api.addEventListener('screenshotReady', (image) => {
      if (image) saveImage(image, `configurator-${Date.now()}.png`);
    });
    // Initial commands example
    api.sendCommandToMetabox(new SetProduct('product-1'));
    api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1280, y: 720 }));
  });
</script>
```

#### 类型脚本/ 角度/ 反应

```typescript
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 参考

### 系统术语

| 任期 | 说明 | A. 背景情况 |
| --- | --- | --- |
| **产品** | Metabox系统中物理产品的3D数字双子 | 基本配置符 |
| **产品标识** | 独特的产品标识符(系统生成的UUID) | 所有背景 |
| **环境** | 产品可视化的三维场景/房间环境 | 视觉环境 |
| **环境编号** | 独特的环境标识符(系统生成的UUID) | 环境管理 |
| **外部图像** | 自定义与外部系统整合的 SKU/标识符 | 电子商务一体化 |
| **显示** | 产品附带的相机动画序列 | 产品属性 |
| **时间档** | 关于产品/环境的材料槽标识符 | 物资分配 |
| **材料编号** | 独一无二的材料标识符 | 物资分配 |

**电子商务配置(CTA集成):**

E-Com配置器扩展 Basic配置器,并带有呼叫到动作按钮. 配置 :

**配置 :**

1.  **CTA 按钮标签 :** 自定义文本(如"Get Quote","Add to Cart")
2.  **回调 URL :** 接收配置数据的 HTTP POST 端点

**CTA 工作流程 :**

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

**后端响应格式 :**

```json
{
  "redirectUrl": "https://your.site/checkout-or-thank-you"
}
```

**使用大小写 :**

-   生成 PDF 引文
-   创建购物卡条目
-   向客户关系管理发送线索信息
-   重定向到自定义的取出流程

### 核心一体化—— `integrateMetabox()`

嵌入了Metabox配置器 Iframe并建立了通信通道.

**签署:**

```typescript
function integrateMetabox(configuratorId: string, containerId: string, apiReadyCallback: (api: Communicator) => void, config?: IntegrateMetaboxConfig): void;
```

**参数:**

| 参数 | 类型 | 需求 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `configuratorId` | `string` | ✅ | \- 说吧 | 配置器 UUID( 非完整 URL) |
| `containerId` | `string` | ✅ | \- 说吧 | DOM 容器元素标识 |
| `apiReadyCallback` | `function` | ✅ | \- 说吧 | API 准备时重新调用 |
| `config` | `IntegrateMetaboxConfig` | ❌ | `{}` | 额外的配置选项 |

**配置选项( E)`IntegrateMetaboxConfig`:**

```typescript
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)
}
```

**验证出错( E) :**

-   丢弃,如果 `configuratorId` 为空
-   如果找不到容器元素, 则丢弃
-   如果 URL 并非 HTTPS 丢弃
-   用 ID 替换已存在的 Iframe `embeddedContent`
-   准许iframe `autoplay; fullscreen; encrypted-media`附加时 `tools` 在使用WebMCP的浏览器中,因此您页面上的浏览器AI代理可以使用配置器注册的工具. 如果你自己建造这个框架,请加上 `tools` 联合国 `allow` 属性表示相同的效果.

**示例**

```typescript
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',
  },
);
```

### 命令类

命令控制配置行为. 通过 `api.sendCommandToMetabox(new CommandName(...))`。 。 。 .

#### 产品和环境管理

| 命令 | 参数 | 说明 |
| --- | --- | --- |
| `SetProduct` | `productId: string` | 切换到指定的产品 |
| `SetProductMaterial` | `slotId: string, materialId: string` | 将材料应用到产品槽中 |
| `SetEnvironment` | `environmentId: string` | 改变环境/场景 |
| `SetEnvironmentMaterial` | `slotId: string, materialId: string` | 将材料应用于环境槽 |
| `SetMaterials` | `commands: MaterialCommand[]` | 将多个材料同时应用到单个信件中 |
| `ResetConfiguration` | 无 | 重置配置器状态为默认值( 产品、 材料、 环境) |

**示例**

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

`SetMaterials` 应用在一个消息中设置的完整材料,而不是每个插槽一个命令——将其用于预设,被保存的配置,或者从外部系统同步出的材料集. 它接受 `SetProductMaterial` 财务报告和审定财务报表 `SetEnvironmentMaterial` 实例并维护其顺序:

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

#### UI 控制

-   注意: ShowEmberdedMenu和showOverlayInterface命令在独立模式下无效.

| 命令 | 参数 | 说明 |
| --- | --- | --- |
| `ShowEmbeddedMenu` | `visible: boolean` | 显示/ 隐藏右侧栏 |
| `ShowOverlayInterface` | `visible: boolean` | 显示/隐藏取景口控制器 |

**示例**

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

#### 相机控制器

| 命令 | 参数 | 说明 |
| --- | --- | --- |
| `SetCamera` | `camera: CameraCommandPayload` | 设置相机位置、旋转、起伏和限制 |
| `GetCamera` | 无 | 请求当前相机; 通过 `getCameraResult` 事件 |
| `ResetCamera` | 无 | 将相机重置为默认位置 |
| `ApplyZoom` | `zoom: number` | 应用缩放三角洲(正=in,负=出) |

**示例**

```typescript
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`

A. 使用的有效载荷 `SetCamera` 命令来配置相机.

```typescript
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` (数字):以度为单位的视野.
-   `mode` ("fps" qq"轨道"):
    -   `fps`: 第一人称相机,在空间中自由移动.
    -   `orbit`:轨道相机围绕一个枢轴(典型的产品取景器行为).
-   `position` ({x,y,z}):世界坐标中的相机位置.
-   `rotation` ({水平,垂直}):相机旋转角度以度为单位.
    -   `horizontal`:Yaw (左/右).
    -   `vertical`:皮奇 (上/下).
-   `restrictions` (可选性):由取景者用来限制用户/相机移动的限度.
    -   距离限制(轨道模式): `minDistanceToPivot`, (中文(简体) ), `maxDistanceToPivot`。 。 。 .
    -   视野限制(第一人称模式): `minFov`, (中文(简体) ), `maxFov`。 。 。 .
    -   旋转限制 : `minHorizontalRotation`, (中文(简体) ), `maxHorizontalRotation`, (中文(简体) ), `minVerticalRotation`, (中文(简体) ), `maxVerticalRotation`。 。 。 .

注释:

-   仅提供您想要执行的限制; 保留目前由查看器配置的未指定值 .
-   旋转单位是度;正值/负值跟随取景者的右手坐标系统.
-   单位 `orbit` 模式、距离限制相对于轨道中枢进行解释.
-   `ResetCamera` 恢复当前产品或环境模板所定义的默认位置/旋转/FOV.

#### 显示大小写/ 动画控制

| 命令 | 参数 | 说明 |
| --- | --- | --- |
| `InitShowcase` | 无 | 初始化显示( 加载动画) |
| `PlayShowcase` | 无 | 开始/ 恢复播放动画 |
| `PauseShowcase` | 无 | 暂停动画 |
| `StopShowcase` | 无 | 停止并重置动画 |

#### 测量工具

| 命令 | 参数 | 说明 |
| --- | --- | --- |
| `ShowMeasurement` | 无 | 显示产品尺寸 |
| `HideMeasurement` | 无 | 隐藏维度覆盖 |

#### 流控

| 命令 | 参数 | 说明 |
| --- | --- | --- |
| `ResumeStream` | 无 | 暂停或断开后恢复像素流线 |
| `StopStream` | 无 | 停止像素流出会话并释放其资源 |
| `ResetUserInactivityTimer` | 无 | 重置 AFK 不活动计时器以保持会话的活性 |

**示例**

```typescript
// 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());
```

#### 导出媒体( M)

| 命令 | 参数 | 说明 | 事件触发 |
| --- | --- | --- | --- |
| `GetScreenshot` | `mimeType: MimeType, size?: {x: number, y: number}` | 渲染截图 | `screenshotReady` |
| `GetPdf` | 无 | 生成 PDF 导出 | 服务器侧( 无事件) |
| `GetCallToActionInformation` | 无 | 触发 CTA 工作流程 | 后端重定向 |

**屏幕截图示例 :**

```typescript
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));

api.addEventListener('screenshotReady', (imageData) => {
  saveImage(imageData, 'product.png');
});
```

### 事件处理

事件驱动的被动状态管理架构. 通过 `api.addEventListener(eventName, handler)`。 。 。 .

#### 可用事件

| 活动 | 有效载荷类型 | 说明 | 使用大小写 |
| --- | --- | --- | --- |
| `configuratorDataUpdated` | `ConfiguratorEnvelope` | 配置状态已更改 | 与产品/材料/环境同步UI |
| `ecomConfiguratorDataUpdated` | `EcomConfigurator` | 已装入 CTA 配置 | 显示带有标签的 CTA 按钮 |
| `viewportReady` | `boolean` | 三维视图端口准备状态 | 隐藏加载, 启用交互 |
| `showcaseStatusChanged` | `ShowCaseStatus` | 动画回放状态 | 更新播放/暂停按钮状态 |
| `statusMessageChanged` | `string | null` | 装入/ 进度消息 | 显示用户反馈 |
| `screenshotReady` | `string | null` | Base64 截图数据 | 下载或显示图像 |
| `getCameraResult` | `CameraCommandPayload` | 返回当前相机数据 | 抓取/存储当前相机 |
| `videoResolutionChanged` | `VideoResolution` | 流解已更改 | 调整取景口布局 |
| `videoStatsUpdated` | `VideoStats` | 视频流质量衡量标准 | QoS仪表板,适应行为 |
| `loadingStatusChanged` | `{ percents: number; loaderVisible: boolean }` | 正在装入进度或覆盖已更改 | 显示自定义加载进度栏 |
| `userInactivityDetected` | `{ remainingSecondsToDisconnect: number; countdownVisible: boolean }` | AFK 倒计时开始或更新 | 显示“ 保持连接” 提示 |
| `configuratorNotFound` | `true` | 配置符 ID 不存在 | 显示错误而不是等待 |

#### 事件实例

**配置状态同步 :**

```typescript
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);
});
```

**屏幕截图处理 :**

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

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

**装入状态 :**

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

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

**显示大小写控制 :**

```typescript
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;
  }
});
```

**装入进度 :**

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

**AFK 无活动倒计时 :**

```typescript
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());
});
```

### 工具函数

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

将基数64-编码图像下载到用户的设备.

**参数:**

| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `imageUrl` | `string` | Base64数据 URL(例如, `data:image/png;base64,...`(中文(简体) ) |
| `filename` | `string` | 希望的文件名带有扩展名 |

**示例**

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

**执行情况说明:** 以 `download` 属性以触发浏览器下载.

**接受输入 :** 只有一类的基数64数据 URL `image/png`, (中文(简体) ), `image/jpeg` 或者说 `image/webp` ——格式 `GetScreenshot` 生产。 其他任何东西(遥控器) `https://` 图像, 一个 `svg+xml` 或者说 `text/html` 使用控制台忽略数据 URL) 警告,而不是下载,因为此功能被输入事件有效载荷 直接关闭后Message频道.

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

RxJS 通讯器事件的包装. RxJS 之类的工作 `fromEvent` ——创建可输入的可观察器,每次指定事件起火时会发出. 备选案文 `api.addEventListener()`。 。 。 .

**参数:**

| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `target` | `Communicator` | 通讯员听 |
| `eventName` | `string` | 事件名称(例如, `configuratorDataUpdated`(中文(简体) ) |

**回返情况:** `Observable<T>` - 与事件有效载荷相匹配的可观察型.

**示例**

```typescript
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');
  });
});
```

## 独立模式

自定义 UI 执行的无头渲染模式 。 禁用所有内置的元框 UI 元素 .

### 何时使用独立模式

✅ **使用时 :**

-   构建一个完全自定义配置界面
-   实施特定品牌产品选择 UX
-   纳入现有设计系统
-   要求完整控制配置流程

❌ **不使用时间 :**

-   用默认的 UI 快速原型已足够
-   需要最小定制
-   有限的发展资源

### 行为改变

| 特性 | 默认模式 | 独立模式 |
| --- | --- | --- |
| 右侧栏菜单 | 可见 | 隐藏 |
| 视图端口覆盖 | 可见 | 隐藏 |
| 模板逻辑 | 活动 | 已禁用 |
| API 控制 | 部分 | 满 |
| 事件系统 | 可用 | 可用 |

### 执行情况

**类型脚本 :**

```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
);
```

**Java 脚本( CDN) :**

```html
<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>
```

## 构建自定义用户界面

要创建自定义配置界面, 请使用 [独立模式](https://npmcn.com/package/%403dsource/metabox-front-api/#readme-standalone-mode) 然后在发送命令前订阅事件,然后遵循这些步骤:

**1\. 倾听国家变化**

```typescript
api.addEventListener('configuratorDataUpdated', (data) => {
  // Update your UI based on configurator state
});
```

**2\. 控制产品配置**

```typescript
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\. 导出配置**

```typescript
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
api.sendCommandToMetabox(new GetPdf());
api.sendCommandToMetabox(new GetCallToActionInformation());
```

## 实例

### E-Com CTA 实例

一旦API准备就绪,启动Call-To-Action的最小例子会从您的页面中流出.

```html
<div id="metabox-container"></div>
<button id="cta-btn" disabled>Send configuration (CTA)</button>

<script type="module">
  import { integrateMetabox, GetCallToActionInformation } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';

  let api = null;

  integrateMetabox('configurator-id', 'metabox-container', (apiInstance) => {
    api = apiInstance;
    document.getElementById('cta-btn').disabled = false;
  });

  document.getElementById('cta-btn').addEventListener('click', () => {
    // Triggers sending the current configuration to your CTA Callback URL
    api.sendCommandToMetabox(new GetCallToActionInformation());
  });
</script>
```

### 基本产品配置器

一个完整的HTML示例,显示如何将元盒配置器与香草JavaScript集成.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Product Configurator</title>
    <style>
      #metabox-container {
        width: 100%;
        height: 600px;
        border: 1px solid #ddd;
        border-radius: 8px;
      }

      .controls {
        margin: 20px 0;
      }

      button {
        margin: 5px;
        padding: 10px 15px;
        border: 1px solid #ccc;
        border-radius: 4px;
        background: #f5f5f5;
        cursor: pointer;
      }

      button:hover {
        background: #e5e5e5;
      }

      button:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }

      .loading {
        text-align: center;
        padding: 20px;
        color: #666;
      }

      .error {
        color: #d32f2f;
        padding: 10px;
        background: #ffebee;
        border: 1px solid #ffcdd2;
        border-radius: 4px;
        margin: 10px 0;
      }
    </style>
  </head>
  <body>
    <div id="loading" class="loading">Loading configurator...</div>
    <div id="error" class="error" style="display: none;"></div>
    <div id="metabox-container"></div>
    <div class="controls">
      <button id="product1-btn" onclick="changeProduct('product-1')" disabled>Product 1</button>
      <button id="product2-btn" onclick="changeProduct('product-2')" disabled>Product 2</button>
      <button id="red-material-btn" onclick="applyMaterial('slot-1', 'material-red')" disabled>Red Material</button>
      <button id="blue-material-btn" onclick="applyMaterial('slot-1', 'material-blue')" disabled>Blue Material</button>
      <button id="screenshot-btn" onclick="takeScreenshot()" disabled>Take Screenshot</button>
    </div>

    <script type="module">
      import { integrateMetabox, SetProduct, SetProductMaterial, GetScreenshot, saveImage } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';

      let api = null;
      let isApiReady = false;

      // Helper function to show/hide loading state
      function setLoadingState(loading) {
        const loadingEl = document.getElementById('loading');
        const buttons = document.querySelectorAll('button');

        loadingEl.style.display = loading ? 'block' : 'none';
        buttons.forEach((btn) => (btn.disabled = loading || !isApiReady));
      }

      // Helper function to show error messages
      function showError(message) {
        const errorEl = document.getElementById('error');
        errorEl.textContent = message;
        errorEl.style.display = 'block';
        setLoadingState(false);
      }

      // Initialize configurator with error handling
      try {
        // Replace 'configurator-id' with your actual configurator ID from 3DSource
        const configuratorId = 'configurator-id';

        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>
```

### 反应集成示例

```tsx
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, GetScreenshot, saveImage } from '@3dsource/metabox-front-api';

interface MetaboxConfiguratorProps {
  configuratorId: string;
  onStateChange?: (data: ConfiguratorEnvelope) => void;
  onError?: (error: string) => void;
  className?: string;
}

interface ConfiguratorState {
  isLoading: boolean;
  error: string | null;
  isApiReady: boolean;
}

const MetaboxConfigurator: React.FC<MetaboxConfiguratorProps> = ({ configuratorId, onStateChange, onError, className }) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const [api, setApi] = useState<Communicator | null>(null);
  const [state, setState] = useState<ConfiguratorState>({
    isLoading: true,
    error: null,
    isApiReady: false,
  });

  // Generate unique container ID to avoid conflicts
  const containerId = useRef(`metabox-container-${Math.random().toString(36).substr(2, 9)}`);

  // Error handler
  const handleError = useCallback(
    (error: string) => {
      setState((prev) => ({ ...prev, error, isLoading: false }));
      onError?.(error);
      console.error('Metabox Configurator Error:', error);
    },
    [onError],
  );

  // Initialize configurator
  useEffect(() => {
    if (!containerRef.current) return;

    let mounted = true;

    const initializeConfigurator = async () => {
      try {
        setState((prev) => ({ ...prev, isLoading: true, error: null }));

        // Set the container ID
        containerRef.current!.id = containerId.current;

        integrateMetabox(configuratorId, containerId.current, (apiInstance) => {
          if (!mounted) return; // Component was unmounted

          try {
            setApi(apiInstance);
            setState((prev) => ({ ...prev, isLoading: false, isApiReady: true }));

            // Set up event listeners with proper typing
            apiInstance.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
              if (mounted) {
                onStateChange?.(data);
              }
            });

            apiInstance.addEventListener('screenshotReady', (imageData: string) => {
              if (mounted) {
                console.log('Screenshot captured successfully');
                saveImage(imageData, `configurator-screenshot-${Date.now()}.png`);
              }
            });

            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;
```

### 角融合示例

```typescript
// 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);
  }
}
```

**此角示例的关键特征 :**

-   **综合错误处理**: 尝试捕获块和方便用户的错误状态
-   **装入国**: 初始化期间带有自旋器的视觉反馈
-   **类型脚本集成**: 带有适当接口的完整类型安全
-   **独特的集装箱标识**: 自动生成ID以避免冲突
-   **活动产出**: 向父组件输入状态变化和错误
-   **响应设计**: 干净、可访问的按钮布局,并有适当的样式

**用法示例:**

```typescript
// app.component.ts
import { Component } from '@angular/core';
import { ConfiguratorEnvelope } from '@3dsource/metabox-front-api';

@Component({
  selector: 'app-root',
  template: `
    <div class="app-container">
      <h1>My Product Configurator</h1>
      <app-metabox-configurator [configuratorId]="configuratorId" (stateChange)="onConfiguratorStateChange($event)" (errorFired)="onConfiguratorError($event)"></app-metabox-configurator>
    </div>
  `,
  styles: [
    `
      .app-container {
        max-width: 1200px;
        margin: 0 auto;
        padding: 20px;
      }
    `,
  ],
})
export class AppComponent {
  configuratorId = 'configurator-id';

  onConfiguratorStateChange(data: ConfiguratorEnvelope): void {
    console.log('Configurator state changed:', data);
    // Update your application state based on configurator changes
  }

  onConfiguratorError(error: string): void {
    console.error('Configurator error:', error);
    // Handle errors (show notifications, log to analytics, etc.)
  }
}
```

**要使用此组件 :**

1.  安装软件包 : `npm install @3dsource/metabox-front-api@latest`
2.  导入您的角模块中的组件
3.  替换 `'configurator-id'` 使用您实际配置的 ID
4.  将占位符ID替换为您的真实产品、材料和插槽ID
5.  通过修改组件样式自定义样式

## 解决问题

### 共同问题

#### 1\. 不装入配置器

**症状:** 空白屏幕, iframe 未出现, 无限期装入

**根原因解决方案( S) :**

| 问题 | 诊断 | 解决方案 |
| --- | --- | --- |
| 无效的配置符 ID | 浏览器控制台显示404个错误 | 在元框管理员中验证配置器 ID |
| 未找到容器 | `Error: Container element not found` | 调用前确保元素存在 `integrateMetabox()` |
| HTTP 代替 HTTPS | 混合内容警告 | 在本地主机上使用 HTTPS 或测试 |
| 集装箱没有尺寸 | 隐形iframe (0x0) | 设置容器元素的明显宽度/ 高度 |
| CORS/框架封锁 | X- Frame- 选项错误 | 在元框设置中检查域允许列表 |

**诊断代码 :**

```typescript
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\. 不起作用的命令

**症状:** 命令已发送, 但配置中没有视觉变化

**常见错误:**

| 错误 | 问题 | 解决方案 |
| --- | --- | --- |
| API 准备好前发送 | 忽略命令 | 只送进来 `apiReadyCallback` |
| 错误的产品/材料标识 | 未找到实体 | 验证身份 `configuratorDataUpdated` 事件 |
| 错误的位号 | 拒绝的材料 | 从配置器数据校验槽ID |

**正确模式 :**

```typescript
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'));
```

### 获取帮助

如果你还在经历问题:

1.  检查浏览器控制台的错误消息
2.  在单独的浏览器标签中校验您的配置 ID
3.  用最小的例子进行测试以孤立问题
4.  联系支持 : 浏览器版本、 配置器 URL、 控制台错误以及复制步骤

## 提示( T)

-   **放弃快速命令.** 避免快速相继发送过多命令——去除物质或产品变化(如300毫秒).
-   **反应容器.** 将配置容器设置为响应维度( S)`width: 100%; height: 100%;`),并随媒体查询而调整移动.
-   **活动在所有模式中都有效.** `configuratorDataUpdated`, (中文(简体) ), `screenshotReady`,以及其他事件 点燃了两德法尔

## 常见问题

### @3dsource/metabox-front-api 是什么？

元框基本配置的客户端 API

### 如何安装 @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 和提交记录判断。

