Files
Genarrative/apps/ai-game-creator-shell/src/view/project-development/resourceModelScene.ts
T
kdletters d5743cd4b8
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
资源画布支持引擎资源预览与模型交互预览 (#413)
## 目的

资源画布支持引擎(Cocos Creator)资源的**只读预览**:能被发现、登记、进入画布,并按类型出预览。

## 主要改动

- 发现层识别 Cocos 资源并区分 `model` / `binary` 两个发现类别;`.meta` 等导入侧车文件仍只可发现
- 登记层按扩展名写入**既有** canonical kind(模型/场景/预制体 → `scene`,动画 → `character-animation`,材质/特效 → `code`,图集与容器 → `document`),不新增 manifest 契约字段
- 引擎工程的 `library/` / `temp/` / `profiles/` / `local/` 不再进入发现结果(仅当目录确实是 Cocos 工程),同名目录在非引擎工程里照常列出
- 资源画布新增三个卡面分支:模型缩略图(整页共用一个 WebGL 上下文)、结构摘要(Cocos 序列化资源)、类型卡(客户端解不了的容器)
- 新增模型放大预览浮层:左键旋转 / 右键或中键平移 / 滚轮缩放 / 复位视角
- 模型预览上限 32 MiB;缩略图按布局尺寸 2 倍超采样,缓存键改用稳定身份
- 引擎图像容器(tga/tif/tiff/hdr)在原生侧转码成 PNG 后复用既有图片预览链路
- 修复资源卡状态边框吃掉内容盒导致的卡面与角标位移
- 同步 Agent 提示词与 AGC 技能文档,并补里程碑与踩坑文档

## 验证

- `cargo check`、`cargo fmt --check`
- Rust 定向:`cargo test … cocos` 9 条、`resource_inspect::tests` 7 条、`agent_asset_import_tests` 9 条、生成目录过滤用例
- 前端:`resource` + `project` 套件 52 文件 / 546 用例;`appSurface` 450 通过 / 20 跳过;app `tsc --noEmit`
- `npm run check:encoding`、`git diff --check`、`npm run check:doc-index`
- 真机:在真实客户端内打开本地 Cocos 夹具工程,逐栏核对模型缩略图 / 结构摘要 / TGA 转码 / 类型卡,并量过卡片在指针移开、悬停、选中三态下几何完全一致

## 未验证

- 模型缩略图与放大预览的真机视觉只覆盖自带夹具工程;多文件 glTF(外部 .bin/贴图)与超大模型仍是类型卡

---------

Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/413
2026-09-17 20:28:39 +08:00

119 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type * as ThreeTypes from 'three';
/**
* 引擎三维模型的**共用场景能力**:加载、取景、释放。
*
* 卡片缩略图(`resourceModelThumbnail`)和交互式预览浮层(`ResourceModelViewer`)都走这里,
* 避免出现「缩略图能打开、放大后打不开」这种两套加载逻辑的分叉 —— 格式支持面、取景算法和
* 释放口径必须逐字一致。
*/
export type ResourceModelSource = {
/** 预览管线给出的 blob URL(模型整份字节)。 */
sourceUrl: string;
/** `model/gltf-binary`、`model/gltf+json` 或 `application/octet-stream`FBX)。 */
mediaType: string;
};
export function isResourceModelFbx(source: ResourceModelSource) {
return (
source.mediaType === 'application/octet-stream' ||
source.sourceUrl.toLowerCase().includes('.fbx')
);
}
/**
* 按媒体类型加载模型。
*
* 只支持**自包含**的模型:`.glb`、单文件 `.gltf`buffer 内嵌)、`.fbx`。多文件 glTF
* `baseURI` 指向外部 `.bin` / 贴图)在 blob URL 下无法解析相对路径,加载失败由调用方
* 降级成类型卡,不做静默半渲染。
*/
export async function loadResourceModelObject(
source: ResourceModelSource,
): Promise<ThreeTypes.Object3D> {
if (isResourceModelFbx(source)) {
const { FBXLoader } = await import(
'three/examples/jsm/loaders/FBXLoader.js'
);
return new FBXLoader().loadAsync(source.sourceUrl);
}
const { GLTFLoader } = await import(
'three/examples/jsm/loaders/GLTFLoader.js'
);
const gltf = await new GLTFLoader().loadAsync(source.sourceUrl);
if (!gltf.scene) {
throw new Error('模型内容为空');
}
return gltf.scene;
}
/**
* 把相机摆到能完整看见整个模型的位置,并返回模型中心与尺寸。
*
* 取景口径与 3D 软件一致:按包围盒最大边计算距离,留 35% 余量,从右上前方俯视。
* 缩略图与交互预览共用同一条公式,尺寸窗口不同也不会出现「一边看得见、一边看不见」。
*/
export function frameResourceModelInCamera(
THREE: typeof ThreeTypes,
object: ThreeTypes.Object3D,
camera: ThreeTypes.PerspectiveCamera,
) {
const box = new THREE.Box3().setFromObject(object);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
const maxDimension = Math.max(size.x, size.y, size.z);
if (!Number.isFinite(maxDimension) || maxDimension <= 0) {
throw new Error('模型几何尺寸无效');
}
const distance =
(maxDimension / 2 / Math.tan((camera.fov * Math.PI) / 360)) * 1.35;
camera.position.set(
center.x + distance * 0.55,
center.y + distance * 0.42,
center.z + distance * 0.75,
);
camera.near = Math.max(distance / 500, 0.001);
camera.far = distance * 20;
camera.lookAt(center);
camera.updateProjectionMatrix();
return { center, size, maxDimension, distance };
}
/** 环境光照:半球光 + 一盏主光,保证没有材质贴图的模型也有体积感。 */
export function addResourceModelLights(
THREE: typeof ThreeTypes,
scene: ThreeTypes.Scene,
) {
scene.add(new THREE.HemisphereLight(0xffffff, 0x445566, 2.2));
const keyLight = new THREE.DirectionalLight(0xffffff, 2.0);
keyLight.position.set(2, 3, 4);
scene.add(keyLight);
}
/** 释放模型对象占用的几何、材质与贴图,避免反复开关预览时显存只涨不降。 */
export function disposeResourceModelObject(object: ThreeTypes.Object3D) {
object.traverse((child) => {
const mesh = child as ThreeTypes.Mesh;
mesh.geometry?.dispose?.();
const material = mesh.material;
const materials = Array.isArray(material)
? material
: material
? [material]
: [];
for (const entry of materials) {
for (const value of Object.values(
entry as unknown as Record<string, unknown>,
)) {
const texture = value as
| { isTexture?: boolean; dispose?: () => void }
| undefined;
if (texture?.isTexture && typeof texture.dispose === 'function') {
texture.dispose();
}
}
(entry as { dispose?: () => void }).dispose?.();
}
});
}