Files
Genarrative/apps/ai-game-creator-shell/src/view/project-development/ResourceModelPreview.tsx
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

150 lines
4.9 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 { useEffect, useRef, useState } from 'react';
import { renderResourceModelThumbnail } from './resourceModelThumbnail';
type ResourceModelPreviewProps = {
/** 预览管线给出的模型字节 URL(blob URL)。 */
sourceUrl: string;
/** 原生侧判定的媒体类型(`model/gltf-binary` / `model/gltf+json` / FBX 的 octet-stream)。 */
mediaType: string;
/** 缩略图缓存身份(见 `resourceModelThumbnailIdentity`):blob URL 会变,身份不会。 */
identity: string;
/** 渲染不出来时画在同一个位置上的类型卡文案(例如「模型 · GLB」)。 */
fallbackLabel: string;
};
/**
* 缩略图超采样倍数。
*
* 资源画布用 `transform: scale(--resource-section-zoom)` 放大栏目视图(真机 1.5 倍),而
* `ResizeObserver` 与 `offsetWidth` 都只看**布局盒**:按布局尺寸 1:1 渲染出来的图,被画布
* 放大后就是糊的。按 2 倍超采样既有余量覆盖画布缩放,也不至于让单张缩略图失控。
*/
const RESOURCE_MODEL_THUMBNAIL_SUPERSAMPLE = 2;
const RESOURCE_MODEL_THUMBNAIL_MAX_EDGE = 1024;
/**
* 资源卡上的引擎模型缩略图。
*
* 这一层只关心「把渲染结果画出来」,渲染本身在 `resourceModelThumbnail` 的单例队列里
* (整页只有一个 WebGL 上下文)。渲染失败**不算预览失败**:卡面就地降级成类型卡,
* 预览管线里已经读到的字节与状态保持不变,排障信息通过
* `data-model-preview-status` 暴露在 DOM 上。
*/
export function ResourceModelPreview({
sourceUrl,
mediaType,
identity,
fallbackLabel,
}: ResourceModelPreviewProps) {
const hostRef = useRef<HTMLSpanElement>(null);
const [thumbnail, setThumbnail] = useState<string | null>(null);
const [status, setStatus] = useState<'loading' | 'ready' | 'failed'>(
'loading',
);
/**
* 渲染尺寸按**宿主真实几何**(含画布缩放后的视觉尺寸)取,并且跟着宿主的尺寸变化重渲。
*
* 卡片在总览小图与栏目大图里是同一个组件:只在挂载时量一次,会拿到当时那个更小的盒子,
* 之后卡片放大也不会重画 —— 表现为缩略图被放大到发虚、比例与卡面不一致。
*/
const [renderSize, setRenderSize] = useState<{
width: number;
height: number;
} | null>(null);
useEffect(() => {
const host = hostRef.current;
if (!host) {
return undefined;
}
const measure = () => {
/*
* 取**布局尺寸**(`offsetWidth` 不受祖先 transform 影响)而不是 `getBoundingClientRect`:
* 画布缩放是 transform,像素值会随缩放变化;布局尺寸才是稳定的口径,再乘超采样倍数。
*/
const width = Math.min(
RESOURCE_MODEL_THUMBNAIL_MAX_EDGE,
Math.max(
192,
Math.round(
(host.offsetWidth || 320) * RESOURCE_MODEL_THUMBNAIL_SUPERSAMPLE,
),
),
);
const height = Math.min(
RESOURCE_MODEL_THUMBNAIL_MAX_EDGE,
Math.max(
144,
Math.round(
(host.offsetHeight || 240) * RESOURCE_MODEL_THUMBNAIL_SUPERSAMPLE,
),
),
);
setRenderSize((current) =>
current && current.width === width && current.height === height
? current
: { width, height },
);
};
measure();
// 测试用的 jsdom 没有 ResizeObserver:没有它就退化成"只在挂载时量一次"。
if (typeof ResizeObserver === 'undefined') {
return undefined;
}
const observer = new ResizeObserver(measure);
observer.observe(host);
return () => observer.disconnect();
}, []);
useEffect(() => {
if (!renderSize) {
return undefined;
}
let disposed = false;
setThumbnail(null);
setStatus('loading');
const request = {
sourceUrl,
mediaType,
identity,
width: renderSize.width,
height: renderSize.height,
};
void renderResourceModelThumbnail(request)
.then((dataUrl) => {
if (disposed) {
return;
}
setThumbnail(dataUrl);
setStatus('ready');
})
.catch(() => {
if (!disposed) {
setStatus('failed');
}
});
return () => {
disposed = true;
};
}, [identity, mediaType, renderSize, sourceUrl]);
return (
<span
ref={hostRef}
className="game-resource-card-model-visual"
data-model-preview-status={status}
// 渲染尺寸暴露到 DOM:缩略图糊 / 比例不对时,先看它和卡片实际几何是否一致。
data-model-render-size={
renderSize ? `${renderSize.width}x${renderSize.height}` : undefined
}
>
{thumbnail ? (
<img src={thumbnail} alt="" decoding="async" />
) : (
<span className="game-resource-card-engine-badge">{fallbackLabel}</span>
)}
</span>
);
}