@3dsource/metabox-front-api

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

元框基本配置的客户端 API

中文使用文档
文档目录29 节
  1. 目录
  2. 特征
  3. 先决条件
  4. 安装
  5. 国家防范机制安装(建议)
  6. CDN 安装( 快速原型)
  7. 快速启动
  8. 1. HTML 设置
  9. 2. 基本融合
  10. API 参考
  11. 系统术语
  12. 核心一体化—— integrateMetabox()
  13. 命令类
  14. 事件处理
  15. 工具函数
  16. 独立模式
  17. 何时使用独立模式
  18. 行为改变
  19. 执行情况
  20. 构建自定义用户界面
  21. 实例
  22. E-Com CTA 实例
  23. 基本产品配置器
  24. 反应集成示例
  25. 角融合示例
  26. 解决问题
  27. 共同问题
  28. 获取帮助
  29. 提示( T)

元框基本配置 API

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

目录

特征

  • 框架不可知融合(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 将被拒绝)

安装

安装 :

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

导入 :

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

CDN 安装( 快速原型)

jsDelivr: (英语)

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

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

快速启动

1. HTML 设置

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

<!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 模块)

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

类型脚本/ 角度/ 反应

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

后端响应格式 :

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

使用大小写 :

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

核心一体化—— integrateMetabox()

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

签署:

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

参数:

参数类型需求默认说明
configuratorIdstring✅- 说吧配置器 UUID( 非完整 URL)
containerIdstring✅- 说吧DOM 容器元素标识
apiReadyCallbackfunction✅- 说吧API 准备时重新调用
configIntegrateMetaboxConfig❌{}额外的配置选项

配置选项( E)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)
}

验证出错( E) :

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

示例

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(...))。 。 。 .

产品和环境管理

命令参数说明
SetProductproductId: string切换到指定的产品
SetProductMaterialslotId: string, materialId: string将材料应用到产品槽中
SetEnvironmentenvironmentId: string改变环境/场景
SetEnvironmentMaterialslotId: string, materialId: string将材料应用于环境槽
SetMaterialscommands: MaterialCommand[]将多个材料同时应用到单个信件中
ResetConfiguration无重置配置器状态为默认值( 产品、 材料、 环境)

示例

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

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

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

UI 控制

  • 注意: ShowEmberdedMenu和showOverlayInterface命令在独立模式下无效.
命令参数说明
ShowEmbeddedMenuvisible: boolean显示/ 隐藏右侧栏
ShowOverlayInterfacevisible: boolean显示/隐藏取景口控制器

示例

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

相机控制器

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

示例

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 命令来配置相机.

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 不活动计时器以保持会话的活性

示例

// 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)

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

屏幕截图示例 :

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

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

事件处理

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

可用事件

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

事件实例

配置状态同步 :

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

屏幕截图处理 :

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

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

装入状态 :

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

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

显示大小写控制 :

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

装入进度 :

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

AFK 无活动倒计时 :

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-编码图像下载到用户的设备.

参数:

参数类型说明
imageUrlstringBase64数据 URL(例如, data:image/png;base64,...(中文(简体) )
filenamestring希望的文件名带有扩展名

示例

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()。 。 。 .

参数:

参数类型说明
targetCommunicator通讯员听
eventNamestring事件名称(例如, configuratorDataUpdated(中文(简体) )

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

示例

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 控制部分满
事件系统可用可用

执行情况

类型脚本 :

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) :

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

构建自定义用户界面

要创建自定义配置界面, 请使用 独立模式 然后在发送命令前订阅事件,然后遵循这些步骤:

1. 倾听国家变化

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

2. 控制产品配置

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. 导出配置

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的最小例子会从您的页面中流出.

<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集成.

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

反应集成示例

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;

角融合示例

// 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以避免冲突
  • 活动产出: 向父组件输入状态变化和错误
  • 响应设计: 干净、可访问的按钮布局,并有适当的样式

用法示例:

// 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- 选项错误在元框设置中检查域允许列表

诊断代码 :

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

正确模式 :

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 共发布 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 是什么?

元框基本配置的客户端 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 和提交记录判断。