资源卡工具条接通「生成动画」:只放行真能跑通的角色图派生
- 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 与泥点价档位,并断言参数入口不存在。
This commit is contained in:
+14
-4
@@ -109,10 +109,14 @@ export function resolveResourceCanvasToolbarActions(
|
||||
resource: ProjectResource,
|
||||
): ReadonlySet<ImageCanvasSelectedToolbarAction> {
|
||||
// 判据只有一条:**放进来就必须真能跑通**。工具条按这个集合渲染按钮,未接通的动作
|
||||
// 一律不渲染,避免出现"点了没反应"的假按钮。当前真实接通的只有「快速编辑」
|
||||
// (normalize → derive(editKind='image-reference'))与「下载」(复用资源面板同一条
|
||||
// 本地文件复制链路);「改造」(redraw) 与「角色动画」(character-animation) 在宿主
|
||||
// 编排层仍是空回调,接上真实链路后再放回来。
|
||||
// 一律不渲染,避免出现"点了没反应"的假按钮。当前真实接通的是「快速编辑」
|
||||
// (normalize → derive(editKind='image-reference'))、「生成动画」
|
||||
// (derive(editKind='character-animation'),见 `index.tsx` 的
|
||||
// `submitResourceCharacterAnimation`)与「下载」(复用资源面板同一条本地文件复制链路)。
|
||||
//
|
||||
// 其余动作按本轮口径不做(改造 / 裁扩 / 去背景 / 完美像素 / 拆分图集 / 提取素材):
|
||||
// 它们在后端各自缺 editKind,或不在 `resource_editor.rs` 的远端路由白名单里,接上只会
|
||||
// 得到必然失败的请求或名不副实的按钮。「改造」的语义由「快速编辑」承担,不再另起入口。
|
||||
const supported = new Set<ImageCanvasSelectedToolbarAction>();
|
||||
const mediaType = resourceCanvasMediaType(resource);
|
||||
const canDeriveFromResource =
|
||||
@@ -121,6 +125,12 @@ export function resolveResourceCanvasToolbarActions(
|
||||
if (mediaType === 'image') {
|
||||
if (isResourceRasterImage(resource) && canDeriveFromResource) {
|
||||
supported.add('quick-edit');
|
||||
// 门禁与美术画布一致:只有角色类资源才出「生成动画」。Rust 侧
|
||||
// `resolve_resource_edit_source` 对 CharacterAnimation 会拿 `image::load_from_memory`
|
||||
// 解出源图宽高,所以非栅格图片(如 SVG)必须在这里就挡住,不能留给后端报错。
|
||||
if (resource.subtype === 'character') {
|
||||
supported.add('character-animation');
|
||||
}
|
||||
}
|
||||
}
|
||||
// 「下载」的判据不是媒体类型,而是"拿得到可落盘的本地文件路径":虚拟版本条目没有
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -194,6 +194,19 @@ export function defaultDerivedResourceName(resource: ProjectResource) {
|
||||
return `${baseName || '资源'}-编辑版`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色动画派生资源的默认名称。
|
||||
*
|
||||
* 与 `defaultDerivedResourceName` 同一条口径(去扩展名 + 兜底名),只换后缀:动作序列帧
|
||||
* 不是"编辑版",用「-角色动画」让新素材在资源卡上一眼认出来源与产出类型。
|
||||
*/
|
||||
export function defaultCharacterAnimationResourceName(
|
||||
resource: ProjectResource,
|
||||
) {
|
||||
const baseName = resource.label.replace(/\.[^.]+$/u, '').trim();
|
||||
return `${baseName || '资源'}-角色动画`;
|
||||
}
|
||||
|
||||
export function resourceEditPromptMaxLength(
|
||||
editKind: LocalProjectResourceEditKind,
|
||||
) {
|
||||
|
||||
@@ -9,6 +9,10 @@ const canvasFixture = vi.hoisted(() => ({
|
||||
revision: 0,
|
||||
}));
|
||||
|
||||
import {
|
||||
calculateCharacterAnimationPrice,
|
||||
CHARACTER_ANIMATION_MODEL,
|
||||
} from '../../../src/components/image-editor/ImageCanvasGenerationModel';
|
||||
import ProjectDevelopmentView from '../src/view/project-development';
|
||||
import {
|
||||
act,
|
||||
@@ -126,7 +130,13 @@ function LiveWorkbench() {
|
||||
);
|
||||
}
|
||||
|
||||
function DerivedWorkbench({ includeArt = false }: { includeArt?: boolean }) {
|
||||
function DerivedWorkbench({
|
||||
includeArt = false,
|
||||
includeCharacter = false,
|
||||
}: {
|
||||
includeArt?: boolean;
|
||||
includeCharacter?: boolean;
|
||||
}) {
|
||||
const initial = createGameCreationAppManifest(
|
||||
'live-canvas-project',
|
||||
'实时画布项目',
|
||||
@@ -150,6 +160,18 @@ function DerivedWorkbench({ includeArt = false }: { includeArt?: boolean }) {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(includeCharacter
|
||||
? [
|
||||
{
|
||||
id: 'source-character',
|
||||
kind: 'character',
|
||||
category: 'character' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/hero.png',
|
||||
source: { kind: 'generated', resourceId: 'hero-resource' },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
const [manifest, setManifest] = useState(initial);
|
||||
canvasFixture.manifest = manifest;
|
||||
@@ -530,6 +552,51 @@ describe('project resource live canvas integration', () => {
|
||||
expect(deriveCalls[1]).not.toHaveProperty('apiKey');
|
||||
});
|
||||
|
||||
it('从角色资源卡生成动画:走 character-animation 派生,且不渲染会被后端忽略的参数入口', async () => {
|
||||
const { deriveCalls } = installTauri();
|
||||
render(<DerivedWorkbench includeCharacter />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开角色与对象' }));
|
||||
fireEvent.click(await findResourceSelectButton('hero.png'));
|
||||
const toolbar = await screen.findByRole('toolbar', {
|
||||
name: '图片工具栏',
|
||||
});
|
||||
fireEvent.click(within(toolbar).getByRole('button', { name: '生成动画' }));
|
||||
const panel = await screen.findByRole('dialog', {
|
||||
name: '角色动画生成面板',
|
||||
});
|
||||
// 参数入口必须不存在:Rust 固定 720p / same / 32 帧 / 4 秒,改它不会改变请求。
|
||||
expect(
|
||||
within(panel).queryByRole('button', { name: /动画参数/ }),
|
||||
).toBeNull();
|
||||
await setComposerText(
|
||||
within(panel).getByLabelText('动画描述'),
|
||||
'角色挥手打招呼',
|
||||
);
|
||||
// 泥点价必须按 Rust 实际固定的 720p×4 秒算,而不是共享 draft 工厂的 480p 默认档:
|
||||
// 档位与 Rust 常量不一致,按钮上就是一个与实际计费不符的数字(内置价表 20 vs 10 每秒)。
|
||||
const submitButton = within(panel).getByRole('button', {
|
||||
name: /生成[\d.]+泥点/,
|
||||
});
|
||||
expect(submitButton.textContent).toContain(
|
||||
`${calculateCharacterAnimationPrice(CHARACTER_ANIMATION_MODEL, '720p', 4)}泥点`,
|
||||
);
|
||||
expect(submitButton.textContent).not.toContain(
|
||||
`${calculateCharacterAnimationPrice(CHARACTER_ANIMATION_MODEL, '480p', 4)}泥点`,
|
||||
);
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => expect(deriveCalls).toHaveLength(1));
|
||||
expect(deriveCalls[0]?.editKind).toBe('character-animation');
|
||||
expect(deriveCalls[0]?.generationMode).toBe('derive');
|
||||
expect(deriveCalls[0]?.prompt).toBe('角色挥手打招呼');
|
||||
expect(deriveCalls[0]?.assetName).toBe('hero-角色动画');
|
||||
expect(deriveCalls[0]?.sourceAssetId).toBe('source-character');
|
||||
expect(deriveCalls[0]?.sourceMediaType).toBe('image/png');
|
||||
expect(deriveCalls[0]).not.toHaveProperty('accessToken');
|
||||
expect(deriveCalls[0]).not.toHaveProperty('apiKey');
|
||||
});
|
||||
|
||||
it('快速编辑里润色提示词:带场景约束回填,提示词变了就换请求身份', async () => {
|
||||
const { deriveCalls, polishCalls } = installTauri({
|
||||
failFirstDerive: true,
|
||||
|
||||
@@ -78,8 +78,10 @@ function toolbarActions(
|
||||
* `docs/project-memory/shared-memory/pitfalls.md` 的「资源画布工具条的『改造』
|
||||
* 『角色动画』按钮点了没反应」)。
|
||||
*
|
||||
* 当前真实接通的是「快速编辑」(normalize → derive(editKind='image-reference'))
|
||||
* 与「下载」(资源面板同一条原生保存对话框 + `save_local_project_asset_file` 链路)。
|
||||
* 当前真实接通的是「快速编辑」(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 资产的栅格图片放行快速编辑与下载', () => {
|
||||
@@ -120,13 +122,60 @@ describe('resource canvas toolbar actions', () => {
|
||||
).toEqual(['download']);
|
||||
});
|
||||
|
||||
it('角色资源只放行快速编辑与下载,不再放行空回调的角色动画动作', () => {
|
||||
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 资产且没有已完成任务产物背书的资源只放行下载', () => {
|
||||
|
||||
Reference in New Issue
Block a user