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
## 目的 资源画布支持引擎(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
119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
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?.();
|
||
}
|
||
});
|
||
}
|