Skip to content

框架集成

本页讲 @modelcubes/viewer-core 在 Vue 3 与纯 JavaScript 项目里的集成姿势。两个可直接运行的完整示例工程在仓库 fmb-app/examples/ 下:

目录形态
fmb-app/examples/vue3Vue 3 + Vite + TypeScript
fmb-app/examples/vanilla纯 JavaScript + Vite

无论哪种框架,就绪口径都是:load() resolve 即可交互(骨架就绪),几何继续后台流式到达。不要等 model-loaded —— LOD 按需加载下该事件可能不触发。需要加载进度条时可订阅 model-geom-progress 事件,见事件系统

Vue 3

核心是把 Viewer 封装成一个组件,管好「创建-事件-销毁」三件事:

vue
<script setup lang="ts">
import { markRaw, onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
import { Viewer } from "@modelcubes/viewer-core";

const props = defineProps<{ src: string }>();
const emit = defineEmits<{
  ready: [viewer: Viewer];
  error: [message: string];
}>();

const canvasRef = ref<HTMLCanvasElement | null>(null);
const viewer = shallowRef<Viewer | null>(null);

onMounted(async () => {
  if (!canvasRef.value) return;
  try {
    const v = markRaw(await Viewer.create(canvasRef.value));
    viewer.value = v;
    // load() 在骨架就绪即 resolve(可交互);几何继续后台流式到达。
    await v.model.load(props.src);
    v.camera.fitView();
    emit("ready", v);
  } catch (e) {
    emit("error", e instanceof Error ? e.message : String(e));
  }
});

onBeforeUnmount(() => {
  viewer.value?.dispose();
  viewer.value = null;
});

defineExpose({ viewer });
</script>

<template>
  <canvas ref="canvasRef" class="fmb-canvas"></canvas>
</template>

<style scoped>
.fmb-canvas {
  width: 100%;
  height: 100%;
  display: block;
}
</style>

最重要的一条:Viewer 不能进深响应式

Viewer 持有 Three.js 场景等大量内部状态。把它放进 ref() / reactive() 会让 Vue 对内部对象做深代理 —— 逐字段递归追踪既慢,又可能破坏内部判等逻辑。正确姿势是双保险:

  • shallowRef 持有 —— 只追踪 .value 引用本身,不碰内部;
  • markRaw 包实例 —— 即使实例后续被放进别的 reactive 容器也不会被代理。

生命周期对应关系

Vue 钩子该做什么
onMountedViewer.create(canvas)(canvas 必须已在 DOM 里)
onBeforeUnmountviewer.dispose()(释放 GPU 资源与事件订阅)

事件经 viewer.on(...) 订阅后转成 Vue emit 抛给父组件,父组件用普通响应式状态接住(选中名、加载状态等都是普通值,可以进 ref)。

完整工程(含工具栏、渲染模式切换、选中显示)见 fmb-app/examples/vue3

纯 JavaScript

不用 TypeScript、不用框架,一个 <script type="module"> 入口即可。注意包仍以 ESM 分发且内部用 Web Worker,需要 Vite / webpack 5 等现代构建工具,不支持直接 <script src> 引入(见安装)。

js
import { Viewer, RenderMode } from "@modelcubes/viewer-core";

async function main() {
  const viewer = await Viewer.create(document.querySelector("#viewport"));

  viewer.on("selection-changed", ({ current }) => {
    const first = current[0];
    if (first) console.log("选中:", viewer.model.getNodeInfo(first.nodeId)?.name ?? first.nodeId);
  });

  await viewer.model.load("/fixtures/demo.fmbv");
  viewer.camera.fitView();

  // 渲染模式切换
  viewer.renderMode.set(RenderMode.Wireframe);
}

main().catch(console.error);

从本地文件加载(Uint8Array 路径)

load() 也接受 Uint8Array,配合 <input type="file"> 可以打开用户本地的 .fmbv:

js
fileInput.addEventListener("change", async () => {
  const file = fileInput.files?.[0];
  if (!file) return;
  const bytes = new Uint8Array(await file.arrayBuffer());
  await viewer.model.load(bytes);
  viewer.camera.fitView();
});

注意:再次调用 load() 会取消仍在进行的上一次加载,被取消的那次会以 code === "Cancelled"ViewerError reject —— 这不是失败,捕获后忽略即可 (完整处理见 fmb-app/examples/vanilla/src/main.jsloadModel)。

完整工程见 fmb-app/examples/vanilla

下一步

  • 指南 — 按功能域逐项了解各能力。
  • 事件系统viewer.on() 可订阅的完整事件清单。
  • API 参考Viewer 入口与各 manager 的完整签名。