Appearance
框架集成
本页讲 @modelcubes/viewer-core 在 Vue 3 与纯 JavaScript 项目里的集成姿势。两个可直接运行的完整示例工程在仓库 fmb-app/examples/ 下:
| 目录 | 形态 |
|---|---|
fmb-app/examples/vue3 | Vue 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 钩子 | 该做什么 |
|---|---|
onMounted | Viewer.create(canvas)(canvas 必须已在 DOM 里) |
onBeforeUnmount | viewer.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.js 的 loadModel)。
完整工程见 fmb-app/examples/vanilla。