一、3D 开关首先是能力选择器
“三维预览”开关不应只控制一个组件的显示与隐藏。开启后,页面需要确认载体有可用模型、资源能加载、场景对象能创建、材质能绑定;任一步失败,都要回到用户能理解的状态。关闭后则应立即使用稳定的二维预览,不能因为三维资源异常而阻断生成与导出主流程。
页面因此维护两层选择:外层use3DPreview决定使用三维还是二维渲染;三维组件内部再用sceneMode区分自动场景和自定义场景。自动场景直接交给Component3D加载原始 GLB,适合快速展示;自定义场景显式创建相机、灯光和材质,适合贴图与手势控制。
| 模式 | 场景来源 | 交互能力 | 失败后的去向 |
|---|---|---|---|
| 二维预览 | Canvas/图片渲染 | 稳定展示、可导出 | 继续保留二维结果 |
| 自动 3D | 原始 GLB 资源 | 平台默认旋转缩放 | 可切回二维或自定义 |
| 自定义 3D | Scene.load()结果 | 自定义相机、贴图、手势 | 显示错误并提供重试 |
二、页面分支必须由领域条件共同决定
自定义照片载体没有固定 GLB,不能因为开关为真就强行进入三维组件。渲染分支需要同时检查载体类型和开关状态,让自定义载体走专用图片渲染,固定载体才进入三维。
if (isCustomCarrier(this.workshop.productId)) { CustomCarrierRenderer({ carrierImageUri: this.customCarrierUri, patternId: this.workshop.patternId, aiImageUrl: this.aiImageUrl }) } else if (this.workshop.use3DPreview) { Carrier3DViewer({ productId: this.workshop.productId, patternId: this.workshop.patternId, aiImageUrl: this.aiImageUrl }) } else { CanvasRenderer({ patternId: this.workshop.patternId, productId: this.workshop.productId, aiImageUrl: this.aiImageUrl }) }这种判断顺序避免“自定义照片 + 开启 3D”落入不存在资源的分支。组件 ID 仍由纹样和载体稳定 ID 组成,导出服务不用理解当前是二维还是三维,只消费页面已经确认的预览结果。
三、自动场景和自定义场景必须使用不同生命周期
自动场景只需把资源交给组件,页面不持有Scene。切到自定义模式后才清理旧引用并异步加载,随后创建SceneOptions。模式切换时主动释放引用,防止旧相机和材质状态污染下一次加载。
private toggleSceneMode(): void { if (this.sceneMode === 'auto') { this.sceneMode = 'custom' this.loadCustomScene() return } this.sceneMode = 'auto' this.sceneRef = null this.sceneOpt = null this.cameraRef = null this.textureApplied = false } private loadCustomScene(): void { this.sceneRef = null this.sceneOpt = null this.cameraRef = null this.textureApplied = false this.loadScene() }状态清理必须发生在新请求开始前。如果先加载再清理,异步回调可能把新Scene写入后又被旧逻辑置空,表现为偶现黑屏。
四、加载状态要覆盖所有退出路径
三维加载最常见的问题不是抛错,而是某条提前返回没有恢复loading。组件需要在未知载体、资源工厂为空、加载成功和 Promise 拒绝四条路径中都写入明确状态。
private loadScene(): void { this.loading = true this.errorMsg = '' const product = getProductById(this.productId) if (product === undefined || product.glbPath.length === 0) { this.loading = false this.errorMsg = '当前载体不支持三维预览' return } Scene.load($rawfile(product.glbPath)) .then(async (scene: Scene) => { const factory = scene.getResourceFactory() if (factory === null) { this.loading = false this.errorMsg = '三维资源工厂不可用' return } await this.setupCustomScene(scene, factory) this.sceneRef = scene this.sceneOpt = { scene, modelType: ModelType.TEXTURE } this.loading = false }) .catch((error: Error) => { this.loading = false this.errorMsg = error.message }) }错误态应包含重试和切回二维两个动作。重试负责处理瞬时资源问题;切回二维确保用户仍能完成业务。自动静默切换虽然更顺滑,却会掩盖设备差异,因此界面应保留“当前使用二维预览”的可见提示。
五、自定义相机手势需要边界约束
单指拖动映射为绕模型旋转,双指缩放映射为相机距离。若不限制俯仰角和距离,相机会进入模型内部、翻转到地面下方,或者缩到浮点精度不稳定的范围。
private updateOrbit(dx: number, dy: number): void { this.orbitAngleY += dx * 0.01 this.orbitAngleX += dy * 0.01 this.orbitAngleX = Math.max( -Math.PI / 3, Math.min(Math.PI / 3, this.orbitAngleX) ) this.updateCameraPosition() } private updateZoom(scale: number): void { if (scale <= 0) { return } this.cameraDist = this.cameraDist / scale this.cameraDist = Math.max(1.5, Math.min(10, this.cameraDist)) this.updateCameraPosition() } private updateCameraPosition(): void { if (this.cameraRef === null) return this.cameraRef.position = { x: this.cameraDist * Math.sin(this.orbitAngleY) * Math.cos(this.orbitAngleX), y: this.cameraDist * Math.sin(this.orbitAngleX), z: this.cameraDist * Math.cos(this.orbitAngleY) * Math.cos(this.orbitAngleX) } }手势层只修改相机,不重新加载模型。资源生命周期与交互状态分离后,拖动和缩放不会触发昂贵的场景重建。
六、运行证据要区分“容器出现”和“模型可用”
灰色矩形出现只能证明Component3D占位布局已创建,不能证明模型加载成功。有效证据至少包括:模型轮廓真实可见;自动/自定义模式标签正确;拖动后观察角度变化;双指缩放后距离受限;切回二维后业务预览仍存在。
@Builder ViewerState() { if (this.loading) { LoadingProgress() } else if (this.errorMsg.length > 0) { Column() { Text(this.errorMsg) Button('重新加载').onClick(() => this.loadScene()) Button('使用二维预览').onClick(() => this.onFallback()) } } else { this.ViewerContent() } }截图中的模型主体、模式按钮和“自动场景”状态共同构成运行证据。若只截取开关,不足以判断三维内容是否真正渲染。
七、失败语义和降级矩阵
| 故障点 | 三维组件状态 | 页面动作 | 业务是否可继续 |
|---|---|---|---|
| 载体无 GLB | 明确提示不支持 | 切到二维 | 可以 |
Scene.load失败 | 保存错误文本 | 重试或二维 | 可以 |
| 资源工厂为空 | 停止加载 | 切到二维 | 可以 |
| 材质绑定失败 | 模型仍可见、贴图状态为否 | 使用原材质 | 可以 |
| 相机创建失败 | 自定义模式失败 | 切回自动场景 | 可以 |
| 手势越界 | 相机参数被钳制 | 继续拖动 | 可以 |
| 设备不支持 3D | 开关回退并提示 | 使用二维 | 可以 |
| 页面退出 | 清理场景引用 | 再进入重新加载 | 可以 |
降级的原则是“缩减表现能力,不改变用户已经选择的纹样和载体”。切回二维时不应清空patternId、productId或生成图 URL,否则一个渲染问题会扩大成业务数据丢失。
八、验证步骤
1. 固定载体开启三维预览,确认真实模型出现,而不是只有空容器。 2. 在自动场景和自定义场景之间往返两次,确认没有旧模型叠加或黑屏。 3. 单指拖动到上下边界,确认俯仰角停止在限制范围内;双指缩放确认不会进入模型内部。 4. 注入不存在的载体 ID,确认加载结束并显示“不支持”,页面仍可切换二维。 5. 模拟 GLB 加载失败,确认只影响预览,不影响已选纹样、载体与导出格式。 6. 关闭三维开关后返回页面,确认二维预览稳定出现;再次开启时重新建立场景。 7. 在不具备三维能力的设备上,确认降级提示真实可见且没有无限 Loading。
九、总结
三维预览的可靠性来自明确的能力边界:页面先判断载体是否适配,再由开关选择二维或三维;三维内部把自动场景与自定义场景分开管理;加载、相机、材质和错误各有独立状态。即使 GLB、资源工厂或设备能力失败,业务仍能退回二维继续完成生成和导出。
平台三维图形能力可参考ArkGraphics 3D 概述。