e765c9f8a6
- resourceCanvasToolbarModel:只有在「已登记的栅格角色图」上放行 character-animation (非角色 / 未登记为正式素材 / 非栅格三类都不放行;Rust 侧角色动画要解码源图取宽高,SVG 必失败);其余动作仍按 opt-in 不渲染,并写明不做它们各自的阻塞证据。 - index.tsx:把源解析与 revision 缓存抽成 resolveResourceDeriveSource,快速编辑与生成动画共用,避免同一个源在两条链路上得到两份 manifest 视图;新增生成动画的源/浮层状态、请求身份(与快速编辑同一套「提示词变了才重铸」口径)与打开/提交/关闭时机。 - 面板复用共享 ImageCanvasCharacterAnimationPanelView,参数入口显式关闭(比例/时长/清晰度与模型由客户端 Rust 固定为 same/720p/32 帧/4 秒/seedance2.0-fast,改它不改变请求);档位常量与 Rust 常量同值,泥点价按同一算法算,避免按钮上出现与实际计费不符的数字。 - 「改造」(redraw) 不再另起入口:其语义由既有「快速编辑」承担,接独立按钮只是同链路重复入口。 - 断言改写(四不写):角色资源那条由「只放行快速编辑与下载」改写为「放行生成动画」,并新增三条反向守卫(非角色图片 / 角色图未登记 / 非栅格角色图均不放行)。 - 端到端新增「从角色资源卡生成动画」:断言 editKind/generationMode/prompt/assetName/sourceAssetId 与泥点价档位,并断言参数入口不存在。
265 lines
8.1 KiB
TypeScript
265 lines
8.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||
import { resolveResourceCanvasToolbarActions } from '../src/features/resource-canvas/resourceCanvasToolbarModel';
|
||
import type {
|
||
ProjectResource,
|
||
ProjectVersionResourceSummary,
|
||
} from '../src/view/project-development/resourceProjectionModel';
|
||
|
||
function createResource(
|
||
overrides: Partial<ProjectResource> = {},
|
||
): ProjectResource {
|
||
return {
|
||
id: 'asset:hero',
|
||
category: 'ui-interaction',
|
||
subtype: 'icon',
|
||
label: '主角立绘',
|
||
path: 'assets/hero.png',
|
||
mediaType: 'image/png',
|
||
sourceLabel: '生成',
|
||
taskTitle: null,
|
||
manifestAssetId: 'hero',
|
||
producerTaskId: null,
|
||
externalResourceId: null,
|
||
referenceResourceIds: [],
|
||
dependencies: [],
|
||
dependencyDepth: 0,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
/** 虚拟版本条目:只有版本摘要,没有对应文件。 */
|
||
function createVersionSummary(): ProjectVersionResourceSummary {
|
||
return {
|
||
versionId: 'version-1',
|
||
parentVersionId: null,
|
||
projectRevision: 3,
|
||
resourceBindings: [],
|
||
createdReason: 'initial',
|
||
createdAt: 1,
|
||
label: '版本 1',
|
||
childVersionIds: [],
|
||
};
|
||
}
|
||
|
||
function createManifest() {
|
||
return createGameCreationAppManifest(
|
||
'project-toolbar-actions',
|
||
'工具条动作测试',
|
||
);
|
||
}
|
||
|
||
/** 把种子任务的第一个任务改成已完成并登记指定产物,用于覆盖任务产物正规化门禁。 */
|
||
function createManifestWithProducingTask(
|
||
status: 'completed' | 'running',
|
||
artifactPath: string,
|
||
) {
|
||
const manifest = createManifest();
|
||
manifest.tasks = manifest.tasks.map((task, index) =>
|
||
index === 0 ? { ...task, status, artifacts: [artifactPath] } : task,
|
||
);
|
||
return manifest;
|
||
}
|
||
|
||
function toolbarActions(
|
||
manifest: ReturnType<typeof createManifest>,
|
||
resource: ProjectResource,
|
||
) {
|
||
return [...resolveResourceCanvasToolbarActions(manifest, resource)];
|
||
}
|
||
|
||
/**
|
||
* AGC 资源画布工具条的放行判据只有一条:**进集合的动作必须真能跑通**。
|
||
*
|
||
* `ImageCanvasSelectedLayerToolbarView` 收到 `supportedActions` 后完全以集合为准,
|
||
* 宿主编排层里还是空回调的动作一旦进集合就会渲染成"点了没反应"的按钮,因此这里
|
||
* 逐条钉住当前真正接通的动作集合(见
|
||
* `docs/project-memory/shared-memory/pitfalls.md` 的「资源画布工具条的『改造』
|
||
* 『角色动画』按钮点了没反应」)。
|
||
*
|
||
* 当前真实接通的是「快速编辑」(normalize → derive(editKind='image-reference'))、
|
||
* 「生成动画」(normalize → derive(editKind='character-animation') → 服务端
|
||
* `/api/editor/character-animations/generations`)与「下载」(资源面板同一条原生保存
|
||
* 对话框 + `save_local_project_asset_file` 链路)。
|
||
*/
|
||
describe('resource canvas toolbar actions', () => {
|
||
it('已登记 manifest 资产的栅格图片放行快速编辑与下载', () => {
|
||
expect(toolbarActions(createManifest(), createResource())).toEqual([
|
||
'quick-edit',
|
||
'download',
|
||
]);
|
||
});
|
||
|
||
it('音频与视频资源不再放行空回调的改造动作,但仍放行下载', () => {
|
||
const manifest = createManifest();
|
||
expect(
|
||
toolbarActions(
|
||
manifest,
|
||
createResource({
|
||
id: 'asset:bgm',
|
||
category: 'audio',
|
||
subtype: 'background-music',
|
||
label: '背景音乐',
|
||
path: 'assets/bgm.mp3',
|
||
mediaType: 'audio/mpeg',
|
||
manifestAssetId: 'bgm',
|
||
}),
|
||
),
|
||
).toEqual(['download']);
|
||
expect(
|
||
toolbarActions(
|
||
manifest,
|
||
createResource({
|
||
id: 'asset:intro',
|
||
subtype: 'video',
|
||
label: '开场动画',
|
||
path: 'assets/intro.mp4',
|
||
mediaType: 'video/mp4',
|
||
manifestAssetId: 'intro',
|
||
}),
|
||
),
|
||
).toEqual(['download']);
|
||
});
|
||
|
||
it('角色资源放行「生成动画」:源是已登记的栅格图片', () => {
|
||
// 这条断言由「不再放行空回调的角色动画动作」改写而来(四不写:旧断言描述的是
|
||
// 未接通状态,已被真实链路取代,不留墓碑)。宿主编排层现在把
|
||
// `onOpenCharacterAnimationPanel` 接到 `submitResourceCharacterAnimation`
|
||
// (derive editKind='character-animation'),所以进集合不再是假按钮。
|
||
expect(
|
||
toolbarActions(
|
||
createManifest(),
|
||
createResource({ id: 'asset:hero-action', subtype: 'character' }),
|
||
),
|
||
).toEqual(['quick-edit', 'character-animation', 'download']);
|
||
});
|
||
|
||
it('非角色的图片资源不放行生成动画,避免渲染出与源类型不匹配的按钮', () => {
|
||
expect(
|
||
toolbarActions(
|
||
createManifest(),
|
||
createResource({ id: 'asset:scene', subtype: 'scene' }),
|
||
),
|
||
).toEqual(['quick-edit', 'download']);
|
||
expect(
|
||
toolbarActions(
|
||
createManifest(),
|
||
createResource({ id: 'asset:icon', subtype: 'icon' }),
|
||
),
|
||
).toEqual(['quick-edit', 'download']);
|
||
});
|
||
|
||
it('角色资源没登记成正式素材时不放行生成动画,只留下真能跑通的下载', () => {
|
||
expect(
|
||
toolbarActions(
|
||
createManifest(),
|
||
createResource({
|
||
id: 'asset:hero-artifact',
|
||
subtype: 'character',
|
||
manifestAssetId: null,
|
||
producerTaskId: 'task-hero',
|
||
}),
|
||
),
|
||
).toEqual(['download']);
|
||
});
|
||
|
||
it('非栅格的角色资源不放行生成动画:Rust 要解码源图取宽高,SVG 会直接失败', () => {
|
||
expect(
|
||
toolbarActions(
|
||
createManifest(),
|
||
createResource({
|
||
id: 'asset:hero-svg',
|
||
subtype: 'character',
|
||
path: 'assets/hero.svg',
|
||
mediaType: 'image/svg+xml',
|
||
}),
|
||
),
|
||
).toEqual(['download']);
|
||
});
|
||
|
||
it('未登记 manifest 资产且没有已完成任务产物背书的资源只放行下载', () => {
|
||
expect(
|
||
toolbarActions(
|
||
createManifest(),
|
||
createResource({ manifestAssetId: null, producerTaskId: 'task-art' }),
|
||
),
|
||
).toEqual(['download']);
|
||
});
|
||
|
||
it('只认已完成任务的栅格产物,未完成任务的产物不放行快速编辑但可下载', () => {
|
||
const artifactPath = 'assets/hero.png';
|
||
const completedManifest = createManifestWithProducingTask(
|
||
'completed',
|
||
artifactPath,
|
||
);
|
||
expect(
|
||
toolbarActions(
|
||
completedManifest,
|
||
createResource({
|
||
manifestAssetId: null,
|
||
producerTaskId: completedManifest.tasks[0].id,
|
||
}),
|
||
),
|
||
).toEqual(['quick-edit', 'download']);
|
||
|
||
const runningManifest = createManifestWithProducingTask(
|
||
'running',
|
||
artifactPath,
|
||
);
|
||
expect(
|
||
toolbarActions(
|
||
runningManifest,
|
||
createResource({
|
||
manifestAssetId: null,
|
||
producerTaskId: runningManifest.tasks[0].id,
|
||
}),
|
||
),
|
||
).toEqual(['download']);
|
||
});
|
||
|
||
it('非栅格图片不进快速编辑的派生门禁,但仍可下载', () => {
|
||
expect(
|
||
toolbarActions(
|
||
createManifest(),
|
||
createResource({
|
||
id: 'asset:icon',
|
||
path: 'assets/icon.svg',
|
||
mediaType: 'image/svg+xml',
|
||
}),
|
||
),
|
||
).toEqual(['download']);
|
||
});
|
||
|
||
it('拿得到本地文件路径的资源才放行下载', () => {
|
||
expect(toolbarActions(createManifest(), createResource())).toContain(
|
||
'download',
|
||
);
|
||
});
|
||
|
||
it('拿不到本地文件路径的资源不放行下载,避免渲染出点了报错的按钮', () => {
|
||
const manifest = createManifest();
|
||
expect(
|
||
toolbarActions(
|
||
manifest,
|
||
createResource({ path: '', manifestAssetId: 'virtual' }),
|
||
),
|
||
).not.toContain('download');
|
||
expect(
|
||
toolbarActions(
|
||
manifest,
|
||
createResource({
|
||
id: 'version:1',
|
||
category: 'version',
|
||
subtype: 'version',
|
||
label: '版本 1',
|
||
path: '',
|
||
mediaType: 'application/vnd.genarrative.project-version+json',
|
||
manifestAssetId: null,
|
||
version: createVersionSummary(),
|
||
}),
|
||
),
|
||
).not.toContain('download');
|
||
});
|
||
});
|