统一画布快速编辑白名单与入口行为 (#140)
- 使用白名单控制快速编辑功能的显示 - 同步浮动工具栏、右键菜单、打开入口和提交门禁 - 禁用角色动作序列帧(整体)、音频和单个图标(explain: backend reject)的快速编辑 - 把快速编辑api改为只接受resource Id | asset Id,删除 image_src 参数, 以保证校验 其中包含生产数据迁移。迁移用于清理历史普通图片错误持久化的 assetKind="image",覆盖 asset → project-resource → showcase → canvas 四个作用域,并包含 operator 鉴权、分批 dry-run、批次 SHA-256 绑定 apply、active 结构化画 > 布迁移摘要同步及最终零残留复核。 --------- Co-authored-by: kdletters <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/140 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
This commit was merged in pull request #140.
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ImageCanvasContextMenusView } from './ImageCanvasContextMenusView';
|
||||
@@ -159,7 +165,7 @@ describe('ImageCanvasContextMenusView', () => {
|
||||
fireEvent.click(
|
||||
screen.getByRole('menuitem', { name: 'Spine 导出(zip)' }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '快速编辑' }));
|
||||
expect(screen.queryByRole('menuitem', { name: '快速编辑' })).toBeNull();
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '生成动画' }));
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '删除' }));
|
||||
|
||||
@@ -178,10 +184,10 @@ describe('ImageCanvasContextMenusView', () => {
|
||||
2,
|
||||
expect.objectContaining({ mode: 'spine-json' }),
|
||||
);
|
||||
expect(props.onOpenQuickEditPanel).toHaveBeenCalledWith(layer);
|
||||
expect(props.onOpenQuickEditPanel).not.toHaveBeenCalled();
|
||||
expect(props.onOpenCharacterAnimationPanel).toHaveBeenCalledWith(layer);
|
||||
expect(props.onCloseContextMenu).toHaveBeenCalledTimes(2);
|
||||
expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(2);
|
||||
expect(props.onCloseContextMenu).toHaveBeenCalledTimes(1);
|
||||
expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(1);
|
||||
expect(props.onDeleteContextLayers).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -349,6 +355,46 @@ describe('ImageCanvasContextMenusView', () => {
|
||||
expect(props.onDeleteLayerById).toHaveBeenCalledWith(layer.id);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
title: '角色动作',
|
||||
mediaType: 'image-sequence' as const,
|
||||
assetKind: 'character-animation' as const,
|
||||
},
|
||||
{
|
||||
title: '点击音效',
|
||||
mediaType: 'audio' as const,
|
||||
assetKind: 'sound-effect' as const,
|
||||
},
|
||||
{
|
||||
title: '背景音乐',
|
||||
mediaType: 'audio' as const,
|
||||
assetKind: 'background-music' as const,
|
||||
},
|
||||
])('hides quick edit from both menus for $title', (overrides) => {
|
||||
const layer = createLayer(overrides);
|
||||
renderContextMenus({
|
||||
contextMenu: {
|
||||
kind: 'layer',
|
||||
x: 16,
|
||||
y: 18,
|
||||
layerId: layer.id,
|
||||
canvasPoint: { x: 40, y: 42 },
|
||||
},
|
||||
imageContextMenuLayer: layer,
|
||||
contextMenuLayer: layer,
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('menuitem', { name: '快速编辑' })).toBeNull();
|
||||
|
||||
cleanup();
|
||||
renderContextMenus({
|
||||
imageContextMenu: { layerId: layer.id, x: 20, y: 22 },
|
||||
imageContextMenuLayer: layer,
|
||||
});
|
||||
expect(screen.queryByRole('menuitem', { name: '快速编辑' })).toBeNull();
|
||||
});
|
||||
|
||||
it('hides quick edit from the standalone menu for individual icons', () => {
|
||||
const layer = createLayer({ assetKind: 'icon' });
|
||||
renderContextMenus({
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
CanvasViewport,
|
||||
ImageContextMenuState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { isQuickEditUnsupportedAssetKind } from './ImageCanvasGenerationModel';
|
||||
import { isQuickEditSupportedLayer } from './ImageCanvasGenerationModel';
|
||||
import type { CanvasLayerCopyBlockReason } from './ImageCanvasLayerCommandModel';
|
||||
|
||||
type ImageCanvasContextMenusViewProps = {
|
||||
@@ -501,7 +501,7 @@ export function ImageCanvasContextMenusView({
|
||||
<hr />
|
||||
{imageContextMenuLayer ? (
|
||||
<>
|
||||
{!isQuickEditUnsupportedAssetKind(imageContextMenuLayer) ? (
|
||||
{isQuickEditSupportedLayer(imageContextMenuLayer) ? (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@@ -565,7 +565,7 @@ export function ImageCanvasContextMenusView({
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<PlatformFloatingMenu label="图片功能面板" placement="bottom-start">
|
||||
{!isQuickEditUnsupportedAssetKind(imageContextMenuLayer) ? (
|
||||
{isQuickEditSupportedLayer(imageContextMenuLayer) ? (
|
||||
<PlatformFloatingMenuItem
|
||||
className="image-canvas-editor__context-menu-item"
|
||||
onClick={() => {
|
||||
|
||||
@@ -168,7 +168,7 @@ function createTestImageAsset({
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType,
|
||||
assetKind: 'image' as const,
|
||||
assetKind: null,
|
||||
sourceResourceId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import {
|
||||
CANVAS_WORLD_ORIGIN,
|
||||
canvasAssetKindOrNull,
|
||||
canvasDisplayScaleToViewportScale,
|
||||
canvasDisplayViewportToViewport,
|
||||
collectExpiredInlineGenerationDialogIds,
|
||||
@@ -548,7 +549,7 @@ describe('ImageCanvasEditorModel', () => {
|
||||
width: 640,
|
||||
height: 640,
|
||||
sourceType: 'generated',
|
||||
assetKind: 'image',
|
||||
assetKind: null,
|
||||
sourceResourceId: 'resource-generated',
|
||||
},
|
||||
],
|
||||
@@ -557,6 +558,7 @@ describe('ImageCanvasEditorModel', () => {
|
||||
expect(library.assets[0]).toMatchObject({
|
||||
id: 'asset-generated',
|
||||
sourceResourceId: 'resource-generated',
|
||||
assetKind: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1118,7 +1120,7 @@ describe('ImageCanvasEditorModel', () => {
|
||||
assetKindOverride: 'character-animation',
|
||||
},
|
||||
new Map([
|
||||
['resource-image', { imageSrc: '/read/image.png', assetKind: 'image' }],
|
||||
['resource-image', { imageSrc: '/read/image.png', assetKind: null }],
|
||||
]),
|
||||
{ onAssetKindOverrideFallback },
|
||||
);
|
||||
@@ -1127,29 +1129,28 @@ describe('ImageCanvasEditorModel', () => {
|
||||
id: 'layer-image-with-action-label',
|
||||
src: '/read/image.png',
|
||||
mediaType: 'image',
|
||||
resourceAssetKind: 'image',
|
||||
resourceAssetKind: null,
|
||||
assetKindOverride: null,
|
||||
assetKind: 'image',
|
||||
assetKind: null,
|
||||
});
|
||||
expect(onAssetKindOverrideFallback).toHaveBeenCalledWith({
|
||||
layerId: 'layer-image-with-action-label',
|
||||
resourceId: 'resource-image',
|
||||
resourceAssetKind: 'image',
|
||||
resourceAssetKind: null,
|
||||
rejectedAssetKindOverride: 'character-animation',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the same media-family compatibility matrix for layer labels', () => {
|
||||
expect(isCanvasAssetKindOverrideCompatible('image', 'character')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(canvasAssetKindOrNull('image')).toBeNull();
|
||||
expect(isCanvasAssetKindOverrideCompatible(null, 'character')).toBe(true);
|
||||
expect(isCanvasAssetKindOverrideCompatible(null, 'icon')).toBe(true);
|
||||
expect(
|
||||
isCanvasAssetKindOverrideCompatible('sound-effect', 'background-music'),
|
||||
).toBe(true);
|
||||
expect(isCanvasAssetKindOverrideCompatible('image', 'video')).toBe(false);
|
||||
expect(isCanvasAssetKindOverrideCompatible(null, 'video')).toBe(false);
|
||||
expect(
|
||||
isCanvasAssetKindOverrideCompatible('image', 'character-animation'),
|
||||
isCanvasAssetKindOverrideCompatible(null, 'character-animation'),
|
||||
).toBe(false);
|
||||
expect(isCanvasAssetKindOverrideCompatible('video', 'character')).toBe(
|
||||
false,
|
||||
|
||||
@@ -1806,7 +1806,7 @@ function inferEditorAssetKind(
|
||||
objectKey?: string | null,
|
||||
assetKind?: string | null,
|
||||
mediaType?: CanvasMediaType,
|
||||
): CanvasAssetKind {
|
||||
): CanvasAssetKind | null {
|
||||
const normalizedAssetKind = canvasAssetKindOrNull(assetKind);
|
||||
if (normalizedAssetKind) {
|
||||
return normalizedAssetKind;
|
||||
@@ -1822,7 +1822,7 @@ function inferEditorAssetKind(
|
||||
if (inferredMediaType === 'audio') {
|
||||
return inferAudioAssetKindFromLabel(label);
|
||||
}
|
||||
return 'image';
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasMediaFileExtension(
|
||||
@@ -2143,8 +2143,7 @@ export function generationInputsOrNull(
|
||||
}
|
||||
|
||||
export function canvasAssetKindOrNull(value: unknown): CanvasAssetKind | null {
|
||||
return value === 'image' ||
|
||||
value === 'audio' ||
|
||||
return value === 'audio' ||
|
||||
value === 'spec' ||
|
||||
value === 'character' ||
|
||||
value === 'character-animation' ||
|
||||
|
||||
@@ -22,7 +22,6 @@ import type {
|
||||
export type CanvasSourceType = 'uploaded' | 'generated' | 'mock_generated';
|
||||
|
||||
export type CanvasAssetKind =
|
||||
| 'image'
|
||||
| 'audio'
|
||||
| 'spec'
|
||||
| 'character'
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
IMAGE_MODEL_NANOBANANA2,
|
||||
isNormalizedCanvasGenerationInputs,
|
||||
isQuickEditUnsupportedAssetKind,
|
||||
isQuickEditSupportedLayer,
|
||||
resolveCharacterAnimationSourceImageSrc,
|
||||
resolveEditorImageGenerationPixelSize,
|
||||
resolveImageGenerationErrorMessage,
|
||||
@@ -530,16 +530,6 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
canOpenRedrawPanel({
|
||||
...generatedLayer,
|
||||
assetKind: 'image',
|
||||
generationInputs: {
|
||||
fields: [{ title: '画面内容', value: '非场景展示字段' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
canOpenRedrawPanel({
|
||||
...generatedLayer,
|
||||
@@ -931,17 +921,52 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
{ label: 'gpt-image-2', value: 'gpt-image-2' },
|
||||
]);
|
||||
expect(
|
||||
isQuickEditUnsupportedAssetKind({
|
||||
isQuickEditSupportedLayer({
|
||||
...buildSourceLayer(),
|
||||
assetKind: 'icon',
|
||||
mediaType: 'image',
|
||||
assetKind: null,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isQuickEditUnsupportedAssetKind({
|
||||
isQuickEditSupportedLayer({
|
||||
...buildSourceLayer(),
|
||||
assetKind: 'icon',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isQuickEditSupportedLayer({
|
||||
...buildSourceLayer(),
|
||||
assetKind: 'icon-spritesheet',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isQuickEditSupportedLayer({
|
||||
...buildSourceLayer(),
|
||||
assetKind: 'scene',
|
||||
resourceAssetKind: 'icon',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isQuickEditSupportedLayer({
|
||||
...buildSourceLayer(),
|
||||
mediaType: 'image-sequence',
|
||||
assetKind: 'character-animation',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isQuickEditSupportedLayer({
|
||||
...buildSourceLayer(),
|
||||
mediaType: 'audio',
|
||||
assetKind: 'sound-effect',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isQuickEditSupportedLayer({
|
||||
...buildSourceLayer(),
|
||||
mediaType: 'video',
|
||||
assetKind: 'video',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('adds reference image semantics and snapshots for spec generation references', () => {
|
||||
|
||||
@@ -709,8 +709,30 @@ export function buildQuickEditModelOptions(currentModel: string) {
|
||||
return options;
|
||||
}
|
||||
|
||||
export function isQuickEditUnsupportedAssetKind(layer: CanvasLayer) {
|
||||
return layer.assetKind === 'icon';
|
||||
const QUICK_EDIT_SUPPORTED_MEDIA_TYPES = new Set<CanvasLayer['mediaType']>([
|
||||
undefined,
|
||||
'image',
|
||||
'video',
|
||||
]);
|
||||
|
||||
const QUICK_EDIT_SUPPORTED_ASSET_KINDS = new Set<CanvasLayer['assetKind']>([
|
||||
undefined,
|
||||
null,
|
||||
'spec',
|
||||
'character',
|
||||
'icon-spritesheet',
|
||||
'icon-spec',
|
||||
'publication-material',
|
||||
'ui-design',
|
||||
'scene',
|
||||
'video',
|
||||
]);
|
||||
|
||||
export function isQuickEditSupportedLayer(layer: CanvasLayer) {
|
||||
return (
|
||||
QUICK_EDIT_SUPPORTED_MEDIA_TYPES.has(layer.mediaType) &&
|
||||
QUICK_EDIT_SUPPORTED_ASSET_KINDS.has(layer.assetKind)
|
||||
);
|
||||
}
|
||||
|
||||
export function buildCharacterSpecPrompt(values: SpecFormValues) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
buildCharacterAnimationSubmissionPlan,
|
||||
buildIconSpritesheetGenerationSubmissionPlan,
|
||||
buildImageGenerationSubmissionPlan,
|
||||
resolveRegisteredEditorReferenceId,
|
||||
} from './ImageCanvasGenerationSubmissionModel';
|
||||
|
||||
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
@@ -31,6 +32,33 @@ function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
}
|
||||
|
||||
describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
it('resolves registered image-edit sources by resource ID first', () => {
|
||||
expect(
|
||||
resolveRegisteredEditorReferenceId({
|
||||
resourceId: ' resource-source ',
|
||||
sourceAssetId: 'asset-source',
|
||||
}),
|
||||
).toBe('resource-source');
|
||||
});
|
||||
|
||||
it('falls back to a registered asset ID for image edits', () => {
|
||||
expect(
|
||||
resolveRegisteredEditorReferenceId({
|
||||
resourceId: 'local-resource-source',
|
||||
sourceAssetId: ' asset-source ',
|
||||
}),
|
||||
).toBe('asset-source');
|
||||
});
|
||||
|
||||
it('rejects unregistered image-edit sources', () => {
|
||||
expect(() =>
|
||||
resolveRegisteredEditorReferenceId({
|
||||
resourceId: 'generation-dialog:source',
|
||||
sourceAssetId: 'upload-source',
|
||||
}),
|
||||
).toThrow('原图尚未登记为项目资源或素材');
|
||||
});
|
||||
|
||||
it('builds normal image generation submission plans', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
|
||||
@@ -133,10 +133,13 @@ export function resolveImageReferenceSubmissionSource(reference: {
|
||||
return reference.objectKey?.trim() || reference.src;
|
||||
}
|
||||
|
||||
export function resolveRegisteredEditorReferenceId(reference: {
|
||||
resourceId?: string | null;
|
||||
sourceAssetId?: string | null;
|
||||
}) {
|
||||
export function resolveRegisteredEditorReferenceId(
|
||||
reference: {
|
||||
resourceId?: string | null;
|
||||
sourceAssetId?: string | null;
|
||||
},
|
||||
referenceLabel: '原图' | '参考图' = '原图',
|
||||
) {
|
||||
const resourceId = reference.resourceId?.trim();
|
||||
if (
|
||||
resourceId &&
|
||||
@@ -146,10 +149,12 @@ export function resolveRegisteredEditorReferenceId(reference: {
|
||||
return resourceId;
|
||||
}
|
||||
const assetId = reference.sourceAssetId?.trim();
|
||||
if (assetId) {
|
||||
if (assetId && !assetId.startsWith('upload-')) {
|
||||
return assetId;
|
||||
}
|
||||
throw new Error('参考图尚未登记为项目资源或素材,请重新选择或上传后再试');
|
||||
throw new Error(
|
||||
`${referenceLabel}尚未登记为项目资源或素材,请重新选择或上传后再试`,
|
||||
);
|
||||
}
|
||||
|
||||
function buildSeedanceVideoReferenceInput(
|
||||
@@ -735,6 +740,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
? {
|
||||
referenceId: resolveRegisteredEditorReferenceId(
|
||||
dialog.specReference,
|
||||
'参考图',
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
@@ -1090,7 +1096,10 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
}
|
||||
let referenceId: string;
|
||||
try {
|
||||
referenceId = resolveRegisteredEditorReferenceId(dialog.iconSpecReference);
|
||||
referenceId = resolveRegisteredEditorReferenceId(
|
||||
dialog.iconSpecReference,
|
||||
'参考图',
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
@@ -476,7 +476,7 @@ describe('ImageCanvasSelectedLayerToolbarView', () => {
|
||||
within(actionToolbar)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['快速编辑', '改造', '下载按钮']);
|
||||
).toEqual(['改造', '下载按钮']);
|
||||
});
|
||||
|
||||
it('keeps redraw visible for supported actions with a missing non-replaceable source', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
canOpenRedrawPanel,
|
||||
isQuickEditUnsupportedAssetKind,
|
||||
isQuickEditSupportedLayer,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
|
||||
type ImageCanvasSelectedLayerToolbarViewProps = {
|
||||
@@ -102,7 +102,7 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
aria-label="图片工具栏"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
{!isQuickEditUnsupportedAssetKind(selectedLayer) ? (
|
||||
{isQuickEditSupportedLayer(selectedLayer) ? (
|
||||
<>
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resolveUploadedAssetKind } from './ImageCanvasUploadUtils';
|
||||
|
||||
describe('ImageCanvasUploadUtils', () => {
|
||||
it('maps supported upload media types to persisted asset kinds', () => {
|
||||
expect(resolveUploadedAssetKind('image')).toBe('image');
|
||||
expect(resolveUploadedAssetKind('image')).toBeUndefined();
|
||||
expect(resolveUploadedAssetKind('video')).toBe('video');
|
||||
expect(resolveUploadedAssetKind('audio')).toBe('audio');
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
|
||||
export function resolveUploadedAssetKind(
|
||||
mediaType: CanvasMediaType,
|
||||
): CanvasAssetKind {
|
||||
): CanvasAssetKind | undefined {
|
||||
switch (mediaType) {
|
||||
case 'video':
|
||||
return 'video';
|
||||
@@ -14,6 +14,6 @@ export function resolveUploadedAssetKind(
|
||||
case 'image-sequence':
|
||||
throw new Error('序列帧素材不支持按单个媒体文件上传');
|
||||
case 'image':
|
||||
return 'image';
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1088,7 +1088,7 @@ describe('ImageCanvasWorldView', () => {
|
||||
|
||||
it('disables labels from other media families', () => {
|
||||
const imageLayer = createLayer({
|
||||
resourceAssetKind: 'image',
|
||||
resourceAssetKind: null,
|
||||
assetKind: 'character',
|
||||
});
|
||||
renderWorldView({ layers: [imageLayer] });
|
||||
@@ -1108,6 +1108,28 @@ describe('ImageCanvasWorldView', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('offers scene as a user-selectable label for image layers', () => {
|
||||
const imageLayer = createLayer({
|
||||
resourceAssetKind: null,
|
||||
assetKind: 'character',
|
||||
});
|
||||
const { props } = renderWorldView({ layers: [imageLayer] });
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材标签:角色' }));
|
||||
|
||||
const sceneItem = within(
|
||||
screen.getByRole('menu', { name: '选择素材标签' }),
|
||||
).getByRole('menuitem', { name: '场景' });
|
||||
expect(sceneItem.hasAttribute('disabled')).toBe(false);
|
||||
|
||||
fireEvent.click(sceneItem);
|
||||
|
||||
expect(props.onUpdateLayerAssetKind).toHaveBeenCalledWith(
|
||||
imageLayer.id,
|
||||
'scene',
|
||||
);
|
||||
});
|
||||
|
||||
it('hides resolution and duration text for focused audio generation frames', () => {
|
||||
const dialog = createGenerationDialog({
|
||||
id: 'dialog-audio',
|
||||
|
||||
@@ -73,7 +73,7 @@ function createAssetSnapshot(
|
||||
height: 240,
|
||||
folderId: 'project',
|
||||
sourceType: 'uploaded',
|
||||
assetKind: 'image',
|
||||
assetKind: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -869,7 +869,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '快速修图',
|
||||
sourceImageSrc: 'resource-source',
|
||||
sourceReferenceId: 'resource-source',
|
||||
size: '2048x1365',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '3:2',
|
||||
@@ -978,10 +978,100 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
|
||||
expect(editEditorImageMock).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('quick-edit').textContent).toContain(
|
||||
'failed:修改图标:图标类素材不支持快速编辑',
|
||||
'failed:修改图标:当前素材类型不支持快速编辑',
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['scene', 'spec'] as const)(
|
||||
'submits quick edits for icon resources overridden to %s',
|
||||
async (assetKind) => {
|
||||
editEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: `修改${assetKind}` }),
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
resourceAssetKind: 'icon',
|
||||
assetKind,
|
||||
}),
|
||||
]}
|
||||
initialQuickEditPanel={{
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: `修改${assetKind}`,
|
||||
size: '1024x1024',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
model: 'gpt-image-2',
|
||||
status: 'idle',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceReferenceId: 'resource-source',
|
||||
targetLayerId: 'layer-source',
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('rejects unregistered quick-edit sources before uploading marked references', async () => {
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
resourceId: 'local-resource-source',
|
||||
sourceAssetId: 'upload-source',
|
||||
}),
|
||||
]}
|
||||
initialQuickEditPanel={{
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '修改未登记图片',
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
size: '1024x1024',
|
||||
status: 'idle',
|
||||
}}
|
||||
initialQuickEditSelectionState={{
|
||||
sourceLayerId: 'layer-source',
|
||||
tool: 'rect',
|
||||
model: 'gpt-image-2',
|
||||
marks: [
|
||||
{
|
||||
id: 'quick-edit-selection-mark-1',
|
||||
tool: 'rect',
|
||||
x: 24,
|
||||
y: 36,
|
||||
width: 160,
|
||||
height: 120,
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
draftMark: null,
|
||||
status: 'idle',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('quick-edit').textContent).toContain(
|
||||
'failed:修改未登记图片:原图尚未登记为项目资源或素材',
|
||||
);
|
||||
});
|
||||
expect(renderUiDesignAssetExtractionMarkedImageMock).not.toHaveBeenCalled();
|
||||
expect(uploadEditorMediaAssetObjectFileMock).not.toHaveBeenCalled();
|
||||
expect(editEditorImageMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows direct quick edit submission for icon spritesheets', async () => {
|
||||
editEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '调整图集布局' }),
|
||||
@@ -1088,10 +1178,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '把当前图改成雨天',
|
||||
sourceImageSrc: 'resource-source',
|
||||
sourceReferenceId: 'resource-source',
|
||||
size: '1024x768',
|
||||
model: 'gpt-image-2',
|
||||
sourceResourceId: 'resource-source',
|
||||
targetLayerId: 'layer-source',
|
||||
assetLabel: '源图 快速编辑',
|
||||
}),
|
||||
@@ -1281,8 +1370,10 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
);
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceImageSrc:
|
||||
sourceReferenceId: 'resource-source',
|
||||
referenceImageSrcs: [
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
],
|
||||
targetLayerId: 'layer-source',
|
||||
}),
|
||||
);
|
||||
@@ -1290,9 +1381,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(
|
||||
uploadEditorMediaAssetObjectFileMock.mock.invocationCallOrder[0],
|
||||
).toBeLessThan(editEditorImageMock.mock.invocationCallOrder[0] ?? 0);
|
||||
expect(editEditorImageMock.mock.calls[0]?.[0]).not.toHaveProperty(
|
||||
'referenceImageSrcs',
|
||||
);
|
||||
expect(generateEditorImageMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1371,8 +1459,10 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
await waitFor(() => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceImageSrc:
|
||||
sourceReferenceId: 'resource-source',
|
||||
referenceImageSrcs: [
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
],
|
||||
model: 'gpt-image-2',
|
||||
targetLayerId: 'layer-source',
|
||||
}),
|
||||
@@ -1581,13 +1671,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('submits character action quick edits through image edits', async () => {
|
||||
editEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({
|
||||
prompt: '让当前图跳得更高',
|
||||
imageSrc: 'data:image/png;base64,action-frame-edited',
|
||||
}),
|
||||
);
|
||||
it('rejects character action quick edits', async () => {
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialLayers={[
|
||||
@@ -1624,25 +1708,13 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '让当前图跳得更高',
|
||||
sourceImageSrc: 'resource-source',
|
||||
model: 'gpt-image-2',
|
||||
assetLabel: '角色动作 快速编辑',
|
||||
targetLayerId: 'layer-source',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(editEditorImageMock).not.toHaveBeenCalled();
|
||||
expect(generateEditorCharacterAnimationMock).not.toHaveBeenCalled();
|
||||
expect(generateEditorImageMock).not.toHaveBeenCalled();
|
||||
expect(generateEditorVideoMock).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-source:角色动作 快速编辑:resource-source:character-animation:image-sequence:data:image/png;base64,action-frame-edited',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('quick-edit').textContent).toContain(
|
||||
'failed:让当前图跳得更高:当前素材类型不支持快速编辑',
|
||||
);
|
||||
});
|
||||
|
||||
it('captures a local edit fallback when the project response has no snapshot', async () => {
|
||||
@@ -1677,7 +1749,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '修改当前图片',
|
||||
sourceImageSrc: 'resource-source',
|
||||
sourceReferenceId: 'resource-source',
|
||||
canvasCompletion: expect.objectContaining({
|
||||
placeholder: {
|
||||
x: 472,
|
||||
|
||||
@@ -72,7 +72,7 @@ import {
|
||||
inferEditorImageAspectRatio,
|
||||
inferEditorImageSizeLabel,
|
||||
isCanvasGenerationDialog,
|
||||
isQuickEditUnsupportedAssetKind,
|
||||
isQuickEditSupportedLayer,
|
||||
normalizeEditorImageModel,
|
||||
resolveEditorImageGenerationPixelSize,
|
||||
resolveImageGenerationErrorMessage,
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
buildImageGenerationSubmissionPlan,
|
||||
resolveGenerationAssetLabel,
|
||||
resolveImageGenerationDialogPrompt,
|
||||
resolveRegisteredEditorReferenceId,
|
||||
} from './ImageCanvasGenerationSubmissionModel';
|
||||
import { validateSoundEffectPrompt } from './ImageCanvasSoundEffectPromptModel';
|
||||
import type {
|
||||
@@ -1421,11 +1422,11 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isQuickEditUnsupportedAssetKind(quickEditSourceLayer)) {
|
||||
if (!isQuickEditSupportedLayer(quickEditSourceLayer)) {
|
||||
setQuickEditPanel({
|
||||
...quickEditPanel,
|
||||
status: 'failed',
|
||||
errorMessage: '图标类素材不支持快速编辑',
|
||||
errorMessage: '当前素材类型不支持快速编辑',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1617,7 +1618,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
quickEditSelectionState?.sourceLayerId === quickEditSourceLayer.id
|
||||
? quickEditSelectionState.marks
|
||||
: [];
|
||||
let sourceReferenceImageSrc: string;
|
||||
const sourceReferenceId =
|
||||
resolveRegisteredEditorReferenceId(quickEditSourceLayer);
|
||||
let markedReferenceImageSrc: string | null = null;
|
||||
if (quickEditSelectionMarks.length > 0) {
|
||||
const renderSourceImageSrc = await resolveEditorImageReferenceDataUrl(
|
||||
quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src,
|
||||
@@ -1628,17 +1631,15 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
marks: quickEditSelectionMarks,
|
||||
showIndexLabels: true,
|
||||
});
|
||||
sourceReferenceImageSrc = await uploadEditorGenerationImageDataUrl(
|
||||
markedReferenceImageSrc = await uploadEditorGenerationImageDataUrl(
|
||||
markedImageDataUrl,
|
||||
projectId,
|
||||
);
|
||||
} else {
|
||||
sourceReferenceImageSrc = await resolveEditorGenerationMediaReference(
|
||||
quickEditSourceLayer,
|
||||
'image',
|
||||
projectId,
|
||||
);
|
||||
}
|
||||
const imageEditReferenceSrcs = [
|
||||
...extraReferenceImageSrcs,
|
||||
...(markedReferenceImageSrc ? [markedReferenceImageSrc] : []),
|
||||
];
|
||||
const generationInputs = buildQuickEditGenerationInputs(
|
||||
'快速编辑提示词',
|
||||
normalizedPrompt,
|
||||
@@ -1653,20 +1654,18 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
const generated = await runEditorGenerationWithWalletRefresh(
|
||||
editEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
sourceImageSrc: sourceReferenceImageSrc,
|
||||
sourceReferenceId,
|
||||
size: quickEditSize,
|
||||
model: normalizedQuickEditModel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
...(extraReferenceImageSrcs.length
|
||||
? { referenceImageSrcs: extraReferenceImageSrcs }
|
||||
...(imageEditReferenceSrcs.length
|
||||
? { referenceImageSrcs: imageEditReferenceSrcs }
|
||||
: {}),
|
||||
projectId,
|
||||
assetKind: quickEditSourceLayer.assetKind,
|
||||
generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: quickEditResultTitle,
|
||||
sourceResourceId: quickEditSourceLayer.resourceId,
|
||||
targetLayerId: quickEditSourceLayer.id,
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
@@ -1887,10 +1886,8 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
canonicalBackgroundMusicPrompt: backgroundMusicSubmission?.prompt,
|
||||
});
|
||||
if (submissionPlan.kind === 'edit') {
|
||||
const referenceImageSrc = await resolveEditorGenerationMediaReference(
|
||||
const sourceReferenceId = resolveRegisteredEditorReferenceId(
|
||||
submissionPlan.sourceLayer,
|
||||
'image',
|
||||
projectId,
|
||||
);
|
||||
const editPlaceholderSize =
|
||||
getCanvasCompletionPlaceholderSizeFromPlan({
|
||||
@@ -1913,14 +1910,12 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
const generated = await runEditorGenerationWithWalletRefresh(
|
||||
editEditorImage({
|
||||
prompt: submissionPlan.normalizedPrompt,
|
||||
sourceImageSrc: referenceImageSrc,
|
||||
sourceReferenceId,
|
||||
...submissionPlan.editInput,
|
||||
projectId,
|
||||
assetKind: submissionPlan.sourceLayer.assetKind,
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: submissionPlan.resultTitle,
|
||||
sourceResourceId: submissionPlan.sourceLayer.resourceId,
|
||||
...(projectId && editCanvasCompletionPlaceholder
|
||||
? {
|
||||
canvasCompletion: {
|
||||
@@ -1954,12 +1949,15 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
});
|
||||
} else if (submissionPlan.kind === 'quick-edit') {
|
||||
const sourceReferenceId = resolveRegisteredEditorReferenceId(
|
||||
submissionPlan.sourceLayer,
|
||||
);
|
||||
const quickEditSelectionMarks =
|
||||
quickEditSelectionState?.sourceLayerId ===
|
||||
submissionPlan.sourceLayer.id
|
||||
? quickEditSelectionState.marks
|
||||
: [];
|
||||
let referenceImageSrc: string;
|
||||
let markedReferenceImageSrc: string | null = null;
|
||||
if (quickEditSelectionMarks.length > 0) {
|
||||
const renderSourceImageSrc =
|
||||
await resolveEditorImageReferenceDataUrl(
|
||||
@@ -1972,16 +1970,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
marks: quickEditSelectionMarks,
|
||||
showIndexLabels: true,
|
||||
});
|
||||
referenceImageSrc = await uploadEditorGenerationImageDataUrl(
|
||||
markedReferenceImageSrc = await uploadEditorGenerationImageDataUrl(
|
||||
markedImageDataUrl,
|
||||
projectId,
|
||||
);
|
||||
} else {
|
||||
referenceImageSrc = await resolveEditorGenerationMediaReference(
|
||||
submissionPlan.sourceLayer,
|
||||
'image',
|
||||
projectId,
|
||||
);
|
||||
}
|
||||
const normalizedReferenceImageSrcs = await Promise.all(
|
||||
(submissionPlan.editInput.referenceImageSrcs ?? []).map(
|
||||
@@ -1998,20 +1990,20 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
),
|
||||
),
|
||||
);
|
||||
const referenceImageSrcs = [
|
||||
...normalizedReferenceImageSrcs,
|
||||
...(markedReferenceImageSrc ? [markedReferenceImageSrc] : []),
|
||||
];
|
||||
const generated = await runEditorGenerationWithWalletRefresh(
|
||||
editEditorImage({
|
||||
prompt: submissionPlan.normalizedPrompt,
|
||||
sourceImageSrc: referenceImageSrc,
|
||||
sourceReferenceId,
|
||||
...submissionPlan.editInput,
|
||||
...(normalizedReferenceImageSrcs.length
|
||||
? { referenceImageSrcs: normalizedReferenceImageSrcs }
|
||||
: {}),
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
projectId,
|
||||
assetKind: submissionPlan.result.assetKind,
|
||||
generationInputs: submissionPlan.result.generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: submissionPlan.result.title,
|
||||
sourceResourceId: submissionPlan.sourceLayer.resourceId,
|
||||
targetLayerId: submissionPlan.sourceLayer.id,
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
|
||||
@@ -2463,9 +2463,8 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '快速修图',
|
||||
sourceImageSrc: 'resource-source',
|
||||
sourceReferenceId: 'resource-source',
|
||||
assetLabel: '源图 快速编辑',
|
||||
sourceResourceId: 'resource-source',
|
||||
generationInputs: expect.objectContaining({
|
||||
version: 2,
|
||||
action: 'image.edit',
|
||||
@@ -2485,6 +2484,12 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(editEditorImageMock.mock.calls[0]?.[0]).not.toHaveProperty(
|
||||
'sourceImageSrc',
|
||||
);
|
||||
expect(editEditorImageMock.mock.calls[0]?.[0]).not.toHaveProperty(
|
||||
'sourceResourceId',
|
||||
);
|
||||
expect(generateEditorImageMock).not.toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -101,7 +101,7 @@ import {
|
||||
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
isCanvasGenerationDialog,
|
||||
isQuickEditUnsupportedAssetKind,
|
||||
isQuickEditSupportedLayer,
|
||||
normalizeEditorImageModel,
|
||||
resizeGenerationPlaceholderToImageSelection,
|
||||
resolveDialogExtraImageReferenceLimit,
|
||||
@@ -1760,7 +1760,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
|
||||
const openQuickEditPanel = useCallback(
|
||||
(sourceLayer: CanvasLayer) => {
|
||||
if (isQuickEditUnsupportedAssetKind(sourceLayer)) {
|
||||
if (!isQuickEditSupportedLayer(sourceLayer)) {
|
||||
return;
|
||||
}
|
||||
setImageContextMenu(null);
|
||||
|
||||
@@ -1044,7 +1044,7 @@ describe('useImageCanvasProjectPersistence', () => {
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'uploaded',
|
||||
assetKind: 'image',
|
||||
assetKind: null,
|
||||
},
|
||||
],
|
||||
updatedAt: '2026-08-05T00:00:00.000Z',
|
||||
@@ -1054,7 +1054,7 @@ describe('useImageCanvasProjectPersistence', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-image:resource-image:image',
|
||||
'layer-image:resource-image:-',
|
||||
);
|
||||
});
|
||||
expect(
|
||||
|
||||
@@ -784,7 +784,6 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
width: 420,
|
||||
height: 315,
|
||||
sourceType: 'uploaded',
|
||||
assetKind: 'image',
|
||||
}),
|
||||
);
|
||||
expect(createEditorAssetMock).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1118,7 +1118,7 @@ describe('editorProjectClient', () => {
|
||||
await expect(
|
||||
editEditorImage({
|
||||
prompt: '修改图片',
|
||||
sourceImageSrc: '/generated-images/editor/source.png',
|
||||
sourceReferenceId: 'resource-source',
|
||||
referenceImageSrcs: references.slice(0, 5),
|
||||
model: 'gpt-image-2',
|
||||
}),
|
||||
@@ -2445,17 +2445,15 @@ describe('editorProjectClient', () => {
|
||||
|
||||
const result = await editEditorImage({
|
||||
prompt: '把画面改成黄昏光线',
|
||||
sourceImageSrc: '/generated-images/editor/source.png',
|
||||
sourceReferenceId: 'resource-character-1',
|
||||
size: '1024x1024',
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['/generated-images/editor/style.png'],
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '角色形象 修改结果',
|
||||
sourceResourceId: 'resource-character-1',
|
||||
targetLayerId: 'layer-character-1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '修改提示词', value: '把画面改成黄昏光线' }],
|
||||
@@ -2471,21 +2469,19 @@ describe('editorProjectClient', () => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt: '把画面改成黄昏光线',
|
||||
sourceImageSrc: '/generated-images/editor/source.png',
|
||||
sourceReferenceId: 'resource-character-1',
|
||||
size: '1024x1024',
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['/generated-images/editor/style.png'],
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '修改提示词', value: '把画面改成黄昏光线' }],
|
||||
references: [],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '角色形象 修改结果',
|
||||
sourceResourceId: 'resource-character-1',
|
||||
targetLayerId: 'layer-character-1',
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -412,18 +412,16 @@ export type EditorUiDesignAssetExtractionInput = {
|
||||
|
||||
export type EditorImageEditInput = {
|
||||
prompt: string;
|
||||
sourceImageSrc: string;
|
||||
sourceReferenceId: string;
|
||||
size?: string;
|
||||
model?: string;
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
referenceImageSrcs?: string[];
|
||||
projectId?: string | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
assetLabel?: string | null;
|
||||
sourceResourceId?: string | null;
|
||||
targetLayerId?: string | null;
|
||||
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
|
||||
};
|
||||
@@ -1383,7 +1381,6 @@ export async function extractEditorUiDesignAssets(
|
||||
}
|
||||
|
||||
export async function editEditorImage(input: EditorImageEditInput) {
|
||||
assertStableEditorMediaReference(input.sourceImageSrc, '待修改图片');
|
||||
assertStableEditorMediaReferences(input.referenceImageSrcs, '修改参考图');
|
||||
assertEditorReferenceLimit(
|
||||
input.referenceImageSrcs,
|
||||
@@ -1397,7 +1394,7 @@ export async function editEditorImage(input: EditorImageEditInput) {
|
||||
EDITOR_IMAGE_EDIT_API,
|
||||
jsonRequest('POST', {
|
||||
prompt: input.prompt,
|
||||
sourceImageSrc: input.sourceImageSrc,
|
||||
sourceReferenceId: input.sourceReferenceId,
|
||||
...(input.size ? { size: input.size } : {}),
|
||||
...(input.model ? { model: input.model } : {}),
|
||||
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
|
||||
@@ -1406,15 +1403,11 @@ export async function editEditorImage(input: EditorImageEditInput) {
|
||||
? { referenceImageSrcs: input.referenceImageSrcs }
|
||||
: {}),
|
||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||
...(input.assetKind ? { assetKind: input.assetKind } : {}),
|
||||
...(input.generationInputs
|
||||
? { generationInputs: input.generationInputs }
|
||||
: {}),
|
||||
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
|
||||
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
|
||||
...(input.sourceResourceId
|
||||
? { sourceResourceId: input.sourceResourceId }
|
||||
: {}),
|
||||
...(input.targetLayerId ? { targetLayerId: input.targetLayerId } : {}),
|
||||
...(input.canvasCompletion
|
||||
? { canvasCompletion: input.canvasCompletion }
|
||||
|
||||
Reference in New Issue
Block a user