Merge branch 'codex/editor-asset-metadata-migration' into codex/editor-asset-library
# Conflicts: # docs/project-memory/shared-memory/decision-log.md # server-rs/crates/api-server/src/editor_project.rs # src/components/image-editor/ImageCanvasEditorModel.ts # src/components/image-editor/ImageCanvasEditorView.test-utils.ts # src/components/image-editor/ImageCanvasEditorView.tsx # src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts # src/services/image-editor/editorProjectClient.test.ts
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -164,10 +164,24 @@ describe('ImageCanvasEditorModel', () => {
|
||||
|
||||
const snapshot = serializeLayer(layer);
|
||||
expect(snapshot).not.toHaveProperty('src');
|
||||
expect(snapshot).not.toHaveProperty('assetKind');
|
||||
expect(snapshot).not.toHaveProperty('generationInputs');
|
||||
|
||||
const hydrated = hydrateLayer(
|
||||
snapshot,
|
||||
new Map([['resource-generated', { imageSrc: '/read/generated.png' }]]),
|
||||
new Map([
|
||||
[
|
||||
'resource-generated',
|
||||
{
|
||||
imageSrc: '/read/generated.png',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '骑士' }],
|
||||
references: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
expect(hydrated).toMatchObject({
|
||||
@@ -178,6 +192,7 @@ describe('ImageCanvasEditorModel', () => {
|
||||
objectKey: 'generated/object.png',
|
||||
locked: true,
|
||||
});
|
||||
expect(hydrated?.generationInputs?.fields[0]?.value).toBe('骑士');
|
||||
});
|
||||
|
||||
it('hydrates manually selected publication material tags from saved layout', () => {
|
||||
|
||||
@@ -144,7 +144,8 @@ export function createLayerFromAsset(
|
||||
assetObjectId: asset.assetObjectId,
|
||||
durationSeconds: asset.durationSeconds,
|
||||
sourceAssetId: asset.id,
|
||||
assetKind,
|
||||
assetKind: asset.assetKind ?? assetKind,
|
||||
generationInputs: asset.generationInputs,
|
||||
} satisfies CanvasLayer;
|
||||
}
|
||||
|
||||
@@ -175,8 +176,6 @@ export function serializeLayer(
|
||||
sourceResourceId: layer.sourceResourceId,
|
||||
sourceAssetId: layer.sourceAssetId,
|
||||
groupId: layer.groupId,
|
||||
assetKind: layer.assetKind,
|
||||
generationInputs: layer.generationInputs,
|
||||
hidden: layer.hidden,
|
||||
locked: layer.locked,
|
||||
flipX: layer.flipX,
|
||||
@@ -364,13 +363,14 @@ export function hydrateCanvasGenerationDialog(
|
||||
|
||||
export function hydrateLayer(
|
||||
snapshot: EditorProjectLayerSnapshot,
|
||||
resourcesById: Map<string, { imageSrc: string }>,
|
||||
resourcesById: Map<string, CanvasLayerResourceMetadata>,
|
||||
): CanvasLayer | null {
|
||||
const resourceId =
|
||||
typeof snapshot.resourceId === 'string' ? snapshot.resourceId : '';
|
||||
const layerId = typeof snapshot.layerId === 'string' ? snapshot.layerId : '';
|
||||
const resource = resourcesById.get(resourceId);
|
||||
const snapshotSrc = typeof snapshot.src === 'string' ? snapshot.src : '';
|
||||
const src = snapshotSrc || resourcesById.get(resourceId)?.imageSrc || '';
|
||||
const src = snapshotSrc || resource?.imageSrc || '';
|
||||
const title =
|
||||
typeof snapshot.title === 'string' ? snapshot.title : '画布图片';
|
||||
if (!resourceId || !layerId || !src) {
|
||||
@@ -413,16 +413,16 @@ export function hydrateLayer(
|
||||
assetObjectId: stringOrNull(snapshot.assetObjectId),
|
||||
durationSeconds:
|
||||
audioDurationOrNull(snapshot.durationSeconds) ??
|
||||
audioDurationOrNull(
|
||||
resourcesById.get(resourceId) as
|
||||
| ({ imageSrc: string } & { durationSeconds?: unknown })
|
||||
| undefined,
|
||||
),
|
||||
audioDurationOrNull(resource?.durationSeconds),
|
||||
sourceResourceId: stringOrNull(snapshot.sourceResourceId),
|
||||
sourceAssetId: stringOrNull(snapshot.sourceAssetId),
|
||||
groupId: stringOrNull(snapshot.groupId),
|
||||
assetKind: canvasAssetKindOrNull(snapshot.assetKind),
|
||||
generationInputs: generationInputsOrNull(snapshot.generationInputs),
|
||||
assetKind:
|
||||
canvasAssetKindOrNull(resource?.assetKind) ??
|
||||
canvasAssetKindOrNull(snapshot.assetKind),
|
||||
generationInputs:
|
||||
generationInputsOrNull(resource?.generationInputs) ??
|
||||
generationInputsOrNull(snapshot.generationInputs),
|
||||
hidden: booleanFromSnapshot(snapshot.hidden),
|
||||
locked: booleanFromSnapshot(snapshot.locked),
|
||||
flipX: booleanFromSnapshot(snapshot.flipX),
|
||||
@@ -467,6 +467,8 @@ export function mapAssetLibrarySnapshot(
|
||||
taskId: asset.taskId ?? undefined,
|
||||
objectKey: asset.objectKey ?? undefined,
|
||||
assetObjectId: asset.assetObjectId ?? undefined,
|
||||
assetKind: canvasAssetKindOrNull(asset.assetKind),
|
||||
generationInputs: generationInputsOrNull(asset.generationInputs),
|
||||
durationSeconds: asset.durationSeconds ?? undefined,
|
||||
};
|
||||
}),
|
||||
@@ -494,6 +496,13 @@ function inferAudioAssetKindFromLabel(label: string): CanvasAssetKind {
|
||||
: 'sound-effect';
|
||||
}
|
||||
|
||||
export type CanvasLayerResourceMetadata = {
|
||||
imageSrc: string;
|
||||
durationSeconds?: number | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: unknown;
|
||||
};
|
||||
|
||||
export function normalizeAssetLibrary(library: EditorAssetLibrarySnapshot) {
|
||||
const mapped = mapAssetLibrarySnapshot(library);
|
||||
let hasDefaultFolder = false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
EditorAssetSnapshot,
|
||||
EditorCharacterAnimationFrameCount,
|
||||
EditorCharacterAnimationGenerationResult,
|
||||
EditorCharacterAnimationRatio,
|
||||
@@ -41,6 +42,8 @@ export type EditorAsset = {
|
||||
taskId?: string;
|
||||
objectKey?: string;
|
||||
assetObjectId?: string;
|
||||
assetKind?: CanvasAssetKind | null;
|
||||
generationInputs?: CanvasGenerationInputs | null;
|
||||
durationSeconds?: number;
|
||||
uploadStatus?: 'uploading' | 'failed';
|
||||
uploadProgress?: number;
|
||||
@@ -86,6 +89,7 @@ export type CanvasLayer = {
|
||||
assetObjectId?: string | null;
|
||||
sourceResourceId?: string | null;
|
||||
sourceAssetId?: string | null;
|
||||
generatedAssetSnapshot?: EditorAssetSnapshot | null;
|
||||
groupId?: string | null;
|
||||
assetKind?: CanvasAssetKind | null;
|
||||
generationInputs?: CanvasGenerationInputs | null;
|
||||
|
||||
@@ -266,6 +266,8 @@ export function setupImageCanvasEditorViewTestLifecycle({
|
||||
width: input.width,
|
||||
height: input.height,
|
||||
sourceType: input.sourceType,
|
||||
assetKind: input.assetKind,
|
||||
generationInputs: input.generationInputs,
|
||||
durationSeconds: input.durationSeconds,
|
||||
}));
|
||||
createEditorAssetFolderMock.mockResolvedValue({
|
||||
|
||||
@@ -11,10 +11,15 @@ import {
|
||||
import {
|
||||
createEditorAsset,
|
||||
createEditorProjectResource,
|
||||
type EditorAssetSnapshot,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog';
|
||||
import { resolveContextMenuPosition } from './ImageCanvasEditorModel';
|
||||
import {
|
||||
canvasAssetKindOrNull,
|
||||
generationInputsOrNull,
|
||||
resolveContextMenuPosition,
|
||||
} from './ImageCanvasEditorModel';
|
||||
import { ImageCanvasEditorShellView } from './ImageCanvasEditorShellView';
|
||||
import type {
|
||||
AssetPointerDragState,
|
||||
@@ -343,6 +348,8 @@ export function ImageCanvasEditorView() {
|
||||
provider: layer.provider,
|
||||
taskId: layer.taskId,
|
||||
durationSeconds: layer.durationSeconds,
|
||||
assetKind: layer.assetKind,
|
||||
generationInputs: layer.generationInputs,
|
||||
})
|
||||
.then((asset) => {
|
||||
setAssets((currentAssets) => [
|
||||
@@ -368,6 +375,8 @@ export function ImageCanvasEditorView() {
|
||||
objectKey: asset.objectKey ?? undefined,
|
||||
assetObjectId: asset.assetObjectId ?? undefined,
|
||||
durationSeconds: asset.durationSeconds ?? layer.durationSeconds,
|
||||
assetKind: canvasAssetKindOrNull(asset.assetKind),
|
||||
generationInputs: generationInputsOrNull(asset.generationInputs),
|
||||
},
|
||||
]);
|
||||
setAssetFolders((currentFolders) =>
|
||||
@@ -406,6 +415,55 @@ export function ImageCanvasEditorView() {
|
||||
},
|
||||
[activeUploadFolderId, openEditorLoginModal, setAssetFolders, setAssets],
|
||||
);
|
||||
const upsertGeneratedAsset = useCallback(
|
||||
(asset: EditorAssetSnapshot) => {
|
||||
setAssets((currentAssets) => [
|
||||
...currentAssets.filter(
|
||||
(currentAsset) => currentAsset.id !== asset.assetId,
|
||||
),
|
||||
{
|
||||
id: asset.assetId,
|
||||
label: asset.label,
|
||||
src: asset.imageSrc,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
folderId: asset.folderId,
|
||||
sourceKind: 'uploaded',
|
||||
sourceType: asset.sourceType,
|
||||
persisted: true,
|
||||
prompt: asset.prompt ?? undefined,
|
||||
actualPrompt: asset.actualPrompt ?? undefined,
|
||||
model: asset.model ?? undefined,
|
||||
provider: asset.provider ?? undefined,
|
||||
taskId: asset.taskId ?? undefined,
|
||||
objectKey: asset.objectKey ?? undefined,
|
||||
assetObjectId: asset.assetObjectId ?? undefined,
|
||||
durationSeconds: asset.durationSeconds ?? undefined,
|
||||
assetKind: canvasAssetKindOrNull(asset.assetKind),
|
||||
generationInputs: generationInputsOrNull(asset.generationInputs),
|
||||
},
|
||||
]);
|
||||
setAssetFolders((currentFolders) =>
|
||||
currentFolders.some((folder) => folder.id === asset.folderId)
|
||||
? currentFolders.map((folder) =>
|
||||
folder.id === asset.folderId
|
||||
? { ...folder, collapsed: false }
|
||||
: folder,
|
||||
)
|
||||
: [
|
||||
...currentFolders,
|
||||
{
|
||||
id: asset.folderId,
|
||||
label: '项目素材',
|
||||
collapsed: false,
|
||||
systemDefault: true,
|
||||
persisted: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
[setAssetFolders, setAssets],
|
||||
);
|
||||
const projectPersistenceRefs = useMemo(
|
||||
() => ({
|
||||
layersRef,
|
||||
@@ -548,6 +606,9 @@ export function ImageCanvasEditorView() {
|
||||
requestUpload,
|
||||
persistGeneratedAsset,
|
||||
persistUpdatedLayerResource,
|
||||
projectId,
|
||||
assetFolderId: activeUploadFolderId,
|
||||
upsertGeneratedAsset,
|
||||
});
|
||||
const {
|
||||
setQuickEditPanel,
|
||||
|
||||
@@ -98,6 +98,57 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
expect(layer.generationInputs?.fields[0]?.value).toBe('生成提示词');
|
||||
});
|
||||
|
||||
it('uses backend-persisted resource and asset snapshots for generated layers', () => {
|
||||
const generationInputs = createGenerationInputs();
|
||||
const layer = createGeneratedResultLayer({
|
||||
generated: createGenerated({
|
||||
resource: {
|
||||
resourceId: 'resource-generated-bff',
|
||||
projectId: 'editor-project-1',
|
||||
imageSrc: 'data:image/png;base64,generated',
|
||||
objectKey: 'generated/from-resource.png',
|
||||
assetObjectId: 'asset-object-from-resource',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character',
|
||||
generationInputs,
|
||||
},
|
||||
asset: {
|
||||
assetId: 'asset-generated-bff',
|
||||
folderId: 'user-1:asset-folder:project',
|
||||
label: '角色形象',
|
||||
imageSrc: 'data:image/png;base64,generated',
|
||||
objectKey: 'generated/from-asset.png',
|
||||
assetObjectId: 'asset-object-from-asset',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character',
|
||||
generationInputs,
|
||||
},
|
||||
}),
|
||||
generatedIndex: 9,
|
||||
canvasSize: { width: 900, height: 640 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
assetKind: 'spec',
|
||||
generationInputs: {
|
||||
fields: [{ title: '旧输入', value: '本地兜底' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(layer).toMatchObject({
|
||||
resourceId: 'resource-generated-bff',
|
||||
sourceAssetId: 'asset-generated-bff',
|
||||
objectKey: 'generated/from-resource.png',
|
||||
assetObjectId: 'asset-object-from-resource',
|
||||
assetKind: 'character',
|
||||
});
|
||||
expect(layer.generationInputs?.fields[0]?.value).toBe('生成提示词');
|
||||
expect(layer.generatedAssetSnapshot?.assetId).toBe('asset-generated-bff');
|
||||
});
|
||||
|
||||
it('creates edit result layers beside the source image', () => {
|
||||
const sourceLayer = createSourceLayer();
|
||||
const layer = createGeneratedResultLayer({
|
||||
|
||||
@@ -101,16 +101,24 @@ function applyGeneratedMetadata(
|
||||
generated: EditorImageGenerationResult,
|
||||
generationInputs: CanvasGenerationInputs | undefined,
|
||||
): CanvasLayer {
|
||||
const resource = generated.resource;
|
||||
const asset = generated.asset;
|
||||
return {
|
||||
...layer,
|
||||
resourceId: resource?.resourceId ?? layer.resourceId,
|
||||
sourceAssetId: asset?.assetId ?? layer.sourceAssetId,
|
||||
prompt: generated.prompt,
|
||||
actualPrompt: generated.actualPrompt ?? generated.prompt,
|
||||
model: generated.model,
|
||||
provider: generated.provider,
|
||||
taskId: generated.taskId,
|
||||
objectKey: generated.objectKey,
|
||||
assetObjectId: generated.assetObjectId,
|
||||
generationInputs,
|
||||
objectKey: resource?.objectKey ?? generated.objectKey,
|
||||
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
|
||||
assetKind:
|
||||
(resource?.assetKind as CanvasLayer['assetKind'] | undefined) ??
|
||||
layer.assetKind,
|
||||
generationInputs: resource?.generationInputs ?? generationInputs,
|
||||
generatedAssetSnapshot: asset,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -232,6 +240,8 @@ export function createIconSpritesheetResultLayers({
|
||||
frame,
|
||||
spritesheetTitle,
|
||||
}: IconSpritesheetResultLayerOptions): CanvasLayer[] {
|
||||
const spritesheetResource = generated.spritesheetResource;
|
||||
const spritesheetAsset = generated.spritesheetAsset;
|
||||
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
||||
const startX =
|
||||
frame?.x ?? worldCenter.x - ICON_FRAME_DISPLAY_SIZE.width / 2;
|
||||
@@ -250,7 +260,9 @@ export function createIconSpritesheetResultLayers({
|
||||
);
|
||||
const spritesheetLayer: CanvasLayer = {
|
||||
id: `layer-icon-spritesheet-${startIndex}`,
|
||||
resourceId: `local-resource-icon-spritesheet-${startIndex}`,
|
||||
resourceId:
|
||||
spritesheetResource?.resourceId ??
|
||||
`local-resource-icon-spritesheet-${startIndex}`,
|
||||
title: spritesheetTitle ?? `图标素材图集 ${startIndex}`,
|
||||
src: generated.spritesheetImageSrc,
|
||||
x: startX,
|
||||
@@ -267,7 +279,12 @@ export function createIconSpritesheetResultLayers({
|
||||
provider: generated.provider,
|
||||
taskId: generated.taskId,
|
||||
assetKind: 'icon-spritesheet',
|
||||
generationInputs,
|
||||
sourceAssetId: spritesheetAsset?.assetId,
|
||||
objectKey: spritesheetResource?.objectKey ?? undefined,
|
||||
assetObjectId: spritesheetResource?.assetObjectId ?? undefined,
|
||||
generationInputs:
|
||||
spritesheetResource?.generationInputs ?? generationInputs,
|
||||
generatedAssetSnapshot: spritesheetAsset,
|
||||
};
|
||||
const iconStartX = startX + spritesheetDisplaySize.width + 32;
|
||||
let cursorX = iconStartX;
|
||||
@@ -275,6 +292,8 @@ export function createIconSpritesheetResultLayers({
|
||||
let rowHeight = 0;
|
||||
|
||||
const iconLayers = iconResults.map((icon, index) => {
|
||||
const resource = icon.resource;
|
||||
const asset = icon.asset;
|
||||
const originalWidth = icon.width || 128;
|
||||
const originalHeight = icon.height || 128;
|
||||
const { width, height } = resolveLayerResolutionSize(
|
||||
@@ -291,7 +310,7 @@ export function createIconSpritesheetResultLayers({
|
||||
const generatedIndex = startIndex + index + 1;
|
||||
const layer: CanvasLayer = {
|
||||
id: `layer-icon-${generatedIndex}`,
|
||||
resourceId: `local-resource-icon-${generatedIndex}`,
|
||||
resourceId: resource?.resourceId ?? `local-resource-icon-${generatedIndex}`,
|
||||
title: icon.name,
|
||||
src: icon.imageSrc,
|
||||
x: cursorX,
|
||||
@@ -308,7 +327,11 @@ export function createIconSpritesheetResultLayers({
|
||||
provider: generated.provider,
|
||||
taskId: generated.taskId,
|
||||
assetKind: 'icon',
|
||||
generationInputs,
|
||||
sourceAssetId: asset?.assetId,
|
||||
objectKey: resource?.objectKey ?? undefined,
|
||||
assetObjectId: resource?.assetObjectId ?? undefined,
|
||||
generationInputs: resource?.generationInputs ?? generationInputs,
|
||||
generatedAssetSnapshot: asset,
|
||||
};
|
||||
|
||||
cursorX += width + spacing;
|
||||
|
||||
@@ -58,6 +58,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
imageSize: '1K',
|
||||
},
|
||||
result: {
|
||||
title: '生成图片 3',
|
||||
generationInputs: {
|
||||
fields: [{ title: '生成提示词', value: '一张发光主视觉' }],
|
||||
references: [],
|
||||
|
||||
@@ -453,6 +453,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
: {}),
|
||||
},
|
||||
result: {
|
||||
title: `生成图片 ${nextGeneratedIndex}`,
|
||||
generationInputs: buildImageGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.generationReferences,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
generateEditorSoundEffect,
|
||||
generateEditorVideo,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import type { EditorAssetSnapshot } from '../../services/image-editor/editorProjectClient';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasGenerationInputs,
|
||||
@@ -96,7 +97,9 @@ type GenerationSubmissionWorkflowOptions = {
|
||||
setActiveTool: Dispatch<SetStateAction<CanvasTool>>;
|
||||
setActiveSidebarPanel: Dispatch<SetStateAction<SidebarPanel | null>>;
|
||||
rememberImageModel: (imageModel: string) => void;
|
||||
persistGeneratedAsset?: (layer: CanvasLayer) => void;
|
||||
projectId?: string | null;
|
||||
assetFolderId?: string | null;
|
||||
upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void;
|
||||
};
|
||||
|
||||
async function normalizePublicationReferenceImages(
|
||||
@@ -142,14 +145,21 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
setActiveTool,
|
||||
setActiveSidebarPanel,
|
||||
rememberImageModel,
|
||||
persistGeneratedAsset,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
upsertGeneratedAsset,
|
||||
}: GenerationSubmissionWorkflowOptions) {
|
||||
const addGeneratedLayersToCanvas = useCallback(
|
||||
(nextLayers: CanvasLayer[]) => {
|
||||
appendCanvasLayersWithResources(nextLayers);
|
||||
nextLayers.forEach((layer) => persistGeneratedAsset?.(layer));
|
||||
nextLayers.forEach((layer) => {
|
||||
const asset = layer.generatedAssetSnapshot;
|
||||
if (asset) {
|
||||
upsertGeneratedAsset?.(asset);
|
||||
}
|
||||
});
|
||||
},
|
||||
[appendCanvasLayersWithResources, persistGeneratedAsset],
|
||||
[appendCanvasLayersWithResources, upsertGeneratedAsset],
|
||||
);
|
||||
|
||||
const addGeneratedResultLayer = useCallback(
|
||||
@@ -510,8 +520,14 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
const sourceImageSrc = await resolveEditorImageReferenceDataUrl(
|
||||
sourceLayer.objectKey?.trim() || sourceLayer.src,
|
||||
);
|
||||
const generationInputs =
|
||||
buildUiDesignAssetExtractionGenerationInputs(sourceLayer);
|
||||
const generated = await extractEditorUiDesignAssets({
|
||||
sourceImageSrc,
|
||||
projectId,
|
||||
generationInputs,
|
||||
assetFolderId,
|
||||
spritesheetLabel: `${sourceLayer.title} 素材图集`,
|
||||
});
|
||||
const extractionFrame = {
|
||||
x: sourceLayer.x + sourceLayer.width + 32,
|
||||
@@ -524,7 +540,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
const nextLayers = addIconSpritesheetResultLayers(
|
||||
generated,
|
||||
generated.iconImageSrcs,
|
||||
buildUiDesignAssetExtractionGenerationInputs(sourceLayer),
|
||||
generationInputs,
|
||||
extractionFrame,
|
||||
undefined,
|
||||
{
|
||||
@@ -542,7 +558,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
);
|
||||
}
|
||||
},
|
||||
[addIconSpritesheetResultLayers, fitLayers],
|
||||
[addIconSpritesheetResultLayers, fitLayers, projectId, assetFolderId],
|
||||
);
|
||||
|
||||
const submitIconSpritesheetGeneration = useCallback(
|
||||
@@ -583,9 +599,12 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
});
|
||||
|
||||
try {
|
||||
const generated = await generateEditorIconSpritesheet(
|
||||
submissionPlan.input,
|
||||
);
|
||||
const generated = await generateEditorIconSpritesheet({
|
||||
...submissionPlan.input,
|
||||
projectId,
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
assetFolderId,
|
||||
});
|
||||
rememberImageModel(submissionPlan.rememberImageModel);
|
||||
addIconSpritesheetResultLayers(
|
||||
generated,
|
||||
@@ -608,6 +627,8 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
addIconSpritesheetResultLayers,
|
||||
getGeneratingDialogPlaceholder,
|
||||
rememberImageModel,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
updateCanvasGenerationDialogById,
|
||||
],
|
||||
);
|
||||
@@ -663,19 +684,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
const sourceReferenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
||||
quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src,
|
||||
);
|
||||
const generated = await generateEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
size: quickEditPanel.size,
|
||||
kind: 'quick-edit',
|
||||
model: normalizeEditorImageModel(quickEditPanel.model),
|
||||
referenceImageSrcs: [
|
||||
...extraReferenceImageSrcs,
|
||||
sourceReferenceImageSrc,
|
||||
],
|
||||
});
|
||||
addQuickEditResultLayer(
|
||||
generated,
|
||||
quickEditSourceLayer,
|
||||
const generationInputs =
|
||||
panelMode === 'redraw'
|
||||
? buildEditGenerationInputs(
|
||||
'重绘提示词',
|
||||
@@ -687,7 +696,29 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
normalizedPrompt,
|
||||
quickEditSourceLayer,
|
||||
quickEditReferences,
|
||||
),
|
||||
);
|
||||
const generated = await generateEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
size: quickEditPanel.size,
|
||||
kind: 'quick-edit',
|
||||
model: normalizeEditorImageModel(quickEditPanel.model),
|
||||
referenceImageSrcs: [
|
||||
...extraReferenceImageSrcs,
|
||||
sourceReferenceImageSrc,
|
||||
],
|
||||
projectId,
|
||||
assetKind: quickEditSourceLayer.assetKind,
|
||||
generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: `${quickEditSourceLayer.title} ${
|
||||
panelMode === 'redraw' ? '重绘' : '快速编辑'
|
||||
}`,
|
||||
sourceResourceId: quickEditSourceLayer.resourceId,
|
||||
});
|
||||
addQuickEditResultLayer(
|
||||
generated,
|
||||
quickEditSourceLayer,
|
||||
generationInputs,
|
||||
panelMode,
|
||||
quickEditDialogId
|
||||
? {
|
||||
@@ -722,6 +753,8 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
addQuickEditResultLayer,
|
||||
getGeneratingDialogPlaceholder,
|
||||
openCanvasGenerationDialog,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
quickEditPanel,
|
||||
quickEditSourceLayer,
|
||||
setQuickEditPanel,
|
||||
@@ -764,11 +797,18 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
});
|
||||
if (submissionPlan.kind === 'edit') {
|
||||
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
||||
submissionPlan.sourceLayer.src,
|
||||
submissionPlan.sourceLayer.objectKey?.trim() ||
|
||||
submissionPlan.sourceLayer.src,
|
||||
);
|
||||
const generated = await editEditorImage({
|
||||
prompt: submissionPlan.normalizedPrompt,
|
||||
sourceImageSrc: referenceImageSrc,
|
||||
projectId,
|
||||
assetKind: submissionPlan.sourceLayer.assetKind,
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: `${submissionPlan.sourceLayer.title} 修改结果`,
|
||||
sourceResourceId: submissionPlan.sourceLayer.resourceId,
|
||||
});
|
||||
addGeneratedResultLayer(generated, {
|
||||
sourceLayer: submissionPlan.sourceLayer,
|
||||
@@ -800,7 +840,14 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
submissionPlan.input.kind === 'publication-material'
|
||||
? await normalizePublicationReferenceImages(submissionPlan.input)
|
||||
: submissionPlan.input;
|
||||
const generated = await generateEditorImage(imageGenerationInput);
|
||||
const generated = await generateEditorImage({
|
||||
...imageGenerationInput,
|
||||
projectId,
|
||||
assetKind: submissionPlan.result.assetKind,
|
||||
generationInputs: submissionPlan.result.generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: submissionPlan.result.title,
|
||||
});
|
||||
if (submissionPlan.rememberImageModel) {
|
||||
rememberImageModel(submissionPlan.rememberImageModel);
|
||||
}
|
||||
@@ -840,6 +887,8 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
layerCounterRef,
|
||||
layers,
|
||||
rememberImageModel,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
setGenerateDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
],
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
PlatformFloatingMenu,
|
||||
PlatformFloatingMenuItem,
|
||||
} from '../common/PlatformFloatingMenu';
|
||||
import type { EditorAssetSnapshot } from '../../services/image-editor/editorProjectClient';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
@@ -87,6 +88,9 @@ type ImageCanvasGenerationSurfaceOptions = {
|
||||
requestUpload: (target: UploadTarget) => void;
|
||||
persistGeneratedAsset?: (layer: CanvasLayer) => void;
|
||||
persistUpdatedLayerResource?: (layer: CanvasLayer) => void;
|
||||
projectId?: string | null;
|
||||
assetFolderId?: string | null;
|
||||
upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void;
|
||||
};
|
||||
|
||||
function renderEditorPortal(node: ReactNode) {
|
||||
@@ -160,6 +164,9 @@ export function useImageCanvasGenerationSurface({
|
||||
requestUpload,
|
||||
persistGeneratedAsset,
|
||||
persistUpdatedLayerResource,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
upsertGeneratedAsset,
|
||||
}: ImageCanvasGenerationSurfaceOptions) {
|
||||
const toolbarOptionCloseTimerRef = useRef<ReturnType<
|
||||
typeof setTimeout
|
||||
@@ -190,6 +197,9 @@ export function useImageCanvasGenerationSurface({
|
||||
setImageContextMenu,
|
||||
persistGeneratedAsset,
|
||||
persistUpdatedLayerResource,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
upsertGeneratedAsset,
|
||||
});
|
||||
|
||||
const activeGenerationLayer = activeCanvasGenerationDialog?.generatedLayerId
|
||||
|
||||
@@ -603,6 +603,14 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
projectId: undefined,
|
||||
assetFolderId: undefined,
|
||||
assetKind: undefined,
|
||||
assetLabel: '生成图片 1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '生成提示词', value: '一张生成图' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'generate:generating:closed:-:placeholder',
|
||||
@@ -644,7 +652,21 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
projectId: undefined,
|
||||
assetFolderId: undefined,
|
||||
assetKind: undefined,
|
||||
assetLabel: '生成图片 1',
|
||||
referenceImageSrcs: ['data:image/png;base64,source'],
|
||||
generationInputs: {
|
||||
fields: [{ title: '生成提示词', value: '一张生成图' }],
|
||||
references: [
|
||||
{
|
||||
title: '参考图 1',
|
||||
label: '画布参考图',
|
||||
src: 'data:image/png;base64,source',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -706,6 +728,21 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
kind: 'quick-edit',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
referenceImageSrcs: ['data:image/png;base64,source'],
|
||||
projectId: undefined,
|
||||
assetFolderId: undefined,
|
||||
assetKind: undefined,
|
||||
assetLabel: '源图 快速编辑',
|
||||
sourceResourceId: 'resource-source',
|
||||
generationInputs: {
|
||||
fields: [{ title: '快速编辑提示词', value: '快速修图' }],
|
||||
references: [
|
||||
{
|
||||
title: '参考图 1',
|
||||
label: '源图',
|
||||
src: 'data:image/png;base64,source',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { EditorAssetSnapshot } from '../../services/image-editor/editorProjectClient';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
@@ -183,6 +184,9 @@ type GenerationWorkflowOptions = {
|
||||
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
|
||||
persistGeneratedAsset?: (layer: CanvasLayer) => void;
|
||||
persistUpdatedLayerResource?: (layer: CanvasLayer) => void;
|
||||
projectId?: string | null;
|
||||
assetFolderId?: string | null;
|
||||
upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void;
|
||||
};
|
||||
|
||||
export function useImageCanvasGenerationWorkflow({
|
||||
@@ -211,6 +215,9 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setImageContextMenu,
|
||||
persistGeneratedAsset,
|
||||
persistUpdatedLayerResource,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
upsertGeneratedAsset,
|
||||
}: GenerationWorkflowOptions) {
|
||||
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
|
||||
const [isGenerationReferenceMenuOpen, setIsGenerationReferenceMenuOpen] =
|
||||
@@ -966,7 +973,9 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setActiveTool,
|
||||
setActiveSidebarPanel,
|
||||
rememberImageModel: setLastImageModel,
|
||||
persistGeneratedAsset,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
upsertGeneratedAsset,
|
||||
});
|
||||
|
||||
const setEffectiveCharacterAnimationPanel: Dispatch<
|
||||
|
||||
@@ -29,7 +29,10 @@ vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
function createLayer(id: string): CanvasLayer {
|
||||
function createLayer(
|
||||
id: string,
|
||||
overrides: Partial<CanvasLayer> = {},
|
||||
): CanvasLayer {
|
||||
return {
|
||||
id,
|
||||
resourceId: `local-${id}`,
|
||||
@@ -44,6 +47,7 @@ function createLayer(id: string): CanvasLayer {
|
||||
zIndex: 3,
|
||||
sourceType: 'uploaded',
|
||||
sourceAssetId: 'asset-a',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,7 +122,14 @@ function ProjectPersistenceHarness({
|
||||
<span data-testid="project-title">{projectTitle}</span>
|
||||
<span data-testid="project-rename">{projectRenameValue}</span>
|
||||
<span data-testid="layers">
|
||||
{layers.map((layer) => `${layer.id}:${layer.resourceId}`).join(',')}
|
||||
{layers
|
||||
.map(
|
||||
(layer) =>
|
||||
`${layer.id}:${layer.resourceId}:${layer.assetKind ?? '-'}:${
|
||||
layer.generationInputs?.fields[0]?.value ?? '-'
|
||||
}`,
|
||||
)
|
||||
.join(',')}
|
||||
</span>
|
||||
<span data-testid="selected">{selectedLayerRef.current ?? '-'}</span>
|
||||
<span data-testid="counter">{layerCounterRef.current}</span>
|
||||
@@ -135,6 +146,38 @@ function ProjectPersistenceHarness({
|
||||
>
|
||||
append
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
persistence.appendCanvasLayersWithResources([
|
||||
createLayer('layer-server-resource', {
|
||||
resourceId: 'resource-from-generation-bff',
|
||||
title: '后端已保存图片',
|
||||
sourceType: 'generated',
|
||||
}),
|
||||
]);
|
||||
}}
|
||||
>
|
||||
append server resource
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
persistence.appendCanvasLayersWithResources([
|
||||
createLayer('layer-character', {
|
||||
title: '角色形象',
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '银发游侠' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}}
|
||||
>
|
||||
append character
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -218,11 +261,11 @@ describe('useImageCanvasProjectPersistence', () => {
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('layers').textContent).toBe(
|
||||
'layer-a:local-layer-a',
|
||||
'layer-a:local-layer-a:-:-',
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toBe(
|
||||
'layer-a:resource-added-asset-a',
|
||||
'layer-a:resource-added-asset-a:-:-',
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
@@ -241,6 +284,111 @@ describe('useImageCanvasProjectPersistence', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not create a project resource again for backend-persisted generated layers', async () => {
|
||||
render(<ProjectPersistenceHarness />);
|
||||
|
||||
expect(await screen.findByText('editor-project-default')).toBeTruthy();
|
||||
act(() => {
|
||||
screen.getByRole('button', { name: 'append server resource' }).click();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('layers').textContent).toBe(
|
||||
'layer-server-resource:resource-from-generation-bff:-:-',
|
||||
);
|
||||
expect(createEditorProjectResourceMock).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(saveEditorProjectLayoutMock).toHaveBeenCalledWith(
|
||||
'editor-project-default',
|
||||
expect.objectContaining({
|
||||
layers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
layerId: 'layer-server-resource',
|
||||
resourceId: 'resource-from-generation-bff',
|
||||
sourceType: 'generated',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('persists generated layer metadata on the project resource and restores it from resources', async () => {
|
||||
loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce({
|
||||
projectId: 'editor-project-default',
|
||||
title: '已有角色项目',
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [
|
||||
{
|
||||
layerId: 'layer-character',
|
||||
resourceId: 'resource-character',
|
||||
title: '角色形象',
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 320,
|
||||
height: 240,
|
||||
originalWidth: 320,
|
||||
originalHeight: 240,
|
||||
zIndex: 3,
|
||||
sourceType: 'generated',
|
||||
},
|
||||
],
|
||||
resources: [
|
||||
{
|
||||
resourceId: 'resource-character',
|
||||
projectId: 'editor-project-default',
|
||||
imageSrc: '/generated/character.png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '银发游侠' }],
|
||||
references: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
updatedAt: '2026-06-12T00:00:00.000Z',
|
||||
});
|
||||
createEditorProjectResourceMock.mockResolvedValueOnce({
|
||||
resourceId: 'resource-character-new',
|
||||
projectId: 'editor-project-default',
|
||||
imageSrc: 'data:image/png;base64,YQ==',
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '银发游侠' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProjectPersistenceHarness />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toBe(
|
||||
'layer-character:resource-character:character:银发游侠',
|
||||
);
|
||||
});
|
||||
act(() => {
|
||||
screen.getByRole('button', { name: 'append character' }).click();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createEditorProjectResourceMock).toHaveBeenCalledWith(
|
||||
'editor-project-default',
|
||||
expect.objectContaining({
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '银发游侠' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('saves generation dialogs in the project layout and restores them on load', async () => {
|
||||
const savedDialog: CanvasGenerationDialogState = {
|
||||
id: 'generation-dialog-1',
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
saveEditorProjectLayout,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import {
|
||||
type CanvasLayerResourceMetadata,
|
||||
hydrateLayer,
|
||||
serializeCanvasLayout,
|
||||
splitCanvasLayoutItems,
|
||||
@@ -53,6 +54,10 @@ function isEditorAuthError(error: unknown) {
|
||||
);
|
||||
}
|
||||
|
||||
function isLocalProjectResourceId(resourceId: string) {
|
||||
return resourceId.startsWith('local-');
|
||||
}
|
||||
|
||||
export function useImageCanvasProjectPersistence({
|
||||
refs,
|
||||
setters,
|
||||
@@ -108,6 +113,8 @@ export function useImageCanvasProjectPersistence({
|
||||
taskId: layer.taskId,
|
||||
durationSeconds: layer.durationSeconds,
|
||||
sourceResourceId: layer.sourceResourceId,
|
||||
assetKind: layer.assetKind,
|
||||
generationInputs: layer.generationInputs,
|
||||
})
|
||||
.then((resource) => {
|
||||
options.onCreated?.(resource.resourceId);
|
||||
@@ -168,7 +175,9 @@ export function useImageCanvasProjectPersistence({
|
||||
refs.layersRef.current = snapshotLayers;
|
||||
setLayers(snapshotLayers);
|
||||
nextLayers.forEach((layer) =>
|
||||
createProjectResourceForLayer(layer, { snapshotLayers }),
|
||||
isLocalProjectResourceId(layer.resourceId)
|
||||
? createProjectResourceForLayer(layer, { snapshotLayers })
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
[createProjectResourceForLayer, refs, setLayers],
|
||||
@@ -205,12 +214,14 @@ export function useImageCanvasProjectPersistence({
|
||||
createProjectResourceForLayer(layer, options);
|
||||
});
|
||||
setViewport(project.viewport);
|
||||
const resourcesById = new Map(
|
||||
const resourcesById = new Map<string, CanvasLayerResourceMetadata>(
|
||||
project.resources.map((resource) => [
|
||||
resource.resourceId,
|
||||
{
|
||||
imageSrc: resource.imageSrc,
|
||||
durationSeconds: resource.durationSeconds,
|
||||
assetKind: resource.assetKind,
|
||||
generationInputs: resource.generationInputs,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -279,6 +279,11 @@ describe('editorProjectClient', () => {
|
||||
prompt: 'dragon knight',
|
||||
model: 'gpt-image-2',
|
||||
sourceResourceId: 'resource-source',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: 'dragon knight' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
@@ -295,6 +300,11 @@ describe('editorProjectClient', () => {
|
||||
prompt: 'dragon knight',
|
||||
model: 'gpt-image-2',
|
||||
sourceResourceId: 'resource-source',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: 'dragon knight' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'创建图片画布资源失败',
|
||||
@@ -425,6 +435,11 @@ describe('editorProjectClient', () => {
|
||||
width: 640,
|
||||
height: 480,
|
||||
sourceType: 'uploaded',
|
||||
assetKind: 'icon',
|
||||
generationInputs: {
|
||||
fields: [{ title: '素材描述', value: '返回按钮' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
await updateEditorAsset('asset-1', {
|
||||
label: '角色主视觉.png',
|
||||
@@ -444,6 +459,11 @@ describe('editorProjectClient', () => {
|
||||
width: 640,
|
||||
height: 480,
|
||||
sourceType: 'uploaded',
|
||||
assetKind: 'icon',
|
||||
generationInputs: {
|
||||
fields: [{ title: '素材描述', value: '返回按钮' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'创建图片画布素材失败',
|
||||
@@ -551,6 +571,15 @@ describe('editorProjectClient', () => {
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '角色形象',
|
||||
sourceResourceId: 'resource-spec-1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '角色设定' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
@@ -564,6 +593,15 @@ describe('editorProjectClient', () => {
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '角色设定' }],
|
||||
references: [],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '角色形象',
|
||||
sourceResourceId: 'resource-spec-1',
|
||||
}),
|
||||
}),
|
||||
'生成图片失败',
|
||||
@@ -643,6 +681,12 @@ describe('editorProjectClient', () => {
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '2K',
|
||||
priceMudPoints: 20,
|
||||
projectId: 'editor-project-1',
|
||||
assetFolderId: 'project',
|
||||
generationInputs: {
|
||||
fields: [{ title: '素材描述', value: '返回按钮' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
@@ -657,6 +701,12 @@ describe('editorProjectClient', () => {
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '2K',
|
||||
priceMudPoints: 20,
|
||||
projectId: 'editor-project-1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '素材描述', value: '返回按钮' }],
|
||||
references: [],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
}),
|
||||
}),
|
||||
'生成图标素材失败',
|
||||
@@ -690,6 +740,13 @@ describe('editorProjectClient', () => {
|
||||
|
||||
const result = await extractEditorUiDesignAssets({
|
||||
sourceImageSrc: 'data:image/png;base64,ui-design',
|
||||
projectId: 'editor-project-1',
|
||||
assetFolderId: 'project',
|
||||
spritesheetLabel: '战斗UI设计图 素材图集',
|
||||
generationInputs: {
|
||||
fields: [{ title: '来源', value: '战斗UI设计图' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.taskId).toBe('ui-asset-extraction-1');
|
||||
@@ -700,6 +757,13 @@ describe('editorProjectClient', () => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourceImageSrc: 'data:image/png;base64,ui-design',
|
||||
projectId: 'editor-project-1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '来源', value: '战斗UI设计图' }],
|
||||
references: [],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
spritesheetLabel: '战斗UI设计图 素材图集',
|
||||
}),
|
||||
}),
|
||||
'提取UI设计图素材失败',
|
||||
@@ -1108,6 +1172,15 @@ describe('editorProjectClient', () => {
|
||||
sourceImageSrc: 'data:image/png;base64,source',
|
||||
size: '1024x1024',
|
||||
model: 'gpt-image-2',
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '角色形象 修改结果',
|
||||
sourceResourceId: 'resource-character-1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '修改提示词', value: '把画面改成黄昏光线' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.taskId).toBe('vector-edit-1');
|
||||
@@ -1121,6 +1194,15 @@ describe('editorProjectClient', () => {
|
||||
sourceImageSrc: 'data:image/png;base64,source',
|
||||
size: '1024x1024',
|
||||
model: 'gpt-image-2',
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '修改提示词', value: '把画面改成黄昏光线' }],
|
||||
references: [],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '角色形象 修改结果',
|
||||
sourceResourceId: 'resource-character-1',
|
||||
}),
|
||||
}),
|
||||
'修改图片失败',
|
||||
|
||||
@@ -29,6 +29,22 @@ export type EditorProjectLayerSnapshot = Record<string, unknown> & {
|
||||
resourceId: string;
|
||||
};
|
||||
|
||||
export type EditorAssetGenerationInputField = {
|
||||
title: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type EditorAssetGenerationInputReference = {
|
||||
title: string;
|
||||
label: string;
|
||||
src: string;
|
||||
};
|
||||
|
||||
export type EditorAssetGenerationInputs = {
|
||||
fields: EditorAssetGenerationInputField[];
|
||||
references: EditorAssetGenerationInputReference[];
|
||||
};
|
||||
|
||||
export type EditorProjectResourceSourceType =
|
||||
| 'uploaded'
|
||||
| 'generated'
|
||||
@@ -50,6 +66,8 @@ export type EditorProjectResourceSnapshot = {
|
||||
taskId?: string | null;
|
||||
durationSeconds?: number | null;
|
||||
sourceResourceId?: string | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
@@ -80,6 +98,8 @@ export type EditorAssetSnapshot = {
|
||||
provider?: string | null;
|
||||
taskId?: string | null;
|
||||
durationSeconds?: number | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
@@ -102,6 +122,12 @@ export type EditorImageGenerationInput = {
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
referenceImageSrcs?: string[];
|
||||
projectId?: string | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
assetLabel?: string | null;
|
||||
sourceResourceId?: string | null;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetGenerationInput = {
|
||||
@@ -111,10 +137,17 @@ export type EditorIconSpritesheetGenerationInput = {
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
priceMudPoints: number;
|
||||
projectId?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
};
|
||||
|
||||
export type EditorUiDesignAssetExtractionInput = {
|
||||
sourceImageSrc: string;
|
||||
projectId?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
spritesheetLabel?: string | null;
|
||||
};
|
||||
|
||||
export type EditorImageEditInput = {
|
||||
@@ -122,6 +155,12 @@ export type EditorImageEditInput = {
|
||||
sourceImageSrc: string;
|
||||
size?: string;
|
||||
model?: string;
|
||||
projectId?: string | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
assetLabel?: string | null;
|
||||
sourceResourceId?: string | null;
|
||||
};
|
||||
|
||||
export type EditorImageGenerationResult = {
|
||||
@@ -136,6 +175,8 @@ export type EditorImageGenerationResult = {
|
||||
model: string;
|
||||
provider: string;
|
||||
taskId: string;
|
||||
resource?: EditorProjectResourceSnapshot | null;
|
||||
asset?: EditorAssetSnapshot | null;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetIconResult = {
|
||||
@@ -143,6 +184,8 @@ export type EditorIconSpritesheetIconResult = {
|
||||
imageSrc: string;
|
||||
width: number;
|
||||
height: number;
|
||||
resource?: EditorProjectResourceSnapshot | null;
|
||||
asset?: EditorAssetSnapshot | null;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetGenerationResult = {
|
||||
@@ -156,6 +199,8 @@ export type EditorIconSpritesheetGenerationResult = {
|
||||
provider: string;
|
||||
taskId: string;
|
||||
priceMudPoints: number;
|
||||
spritesheetResource?: EditorProjectResourceSnapshot | null;
|
||||
spritesheetAsset?: EditorAssetSnapshot | null;
|
||||
};
|
||||
|
||||
export type EditorCharacterAnimationResolution = '480p' | '720p';
|
||||
@@ -315,6 +360,8 @@ export type EditorProjectResourceCreateInput = {
|
||||
taskId?: string | null;
|
||||
durationSeconds?: number | null;
|
||||
sourceResourceId?: string | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
};
|
||||
|
||||
export type EditorAssetCreateInput = {
|
||||
@@ -332,6 +379,8 @@ export type EditorAssetCreateInput = {
|
||||
provider?: string | null;
|
||||
taskId?: string | null;
|
||||
durationSeconds?: number | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
};
|
||||
|
||||
export type EditorAssetUpdateInput = {
|
||||
@@ -558,6 +607,16 @@ export async function generateEditorImage(input: EditorImageGenerationInput) {
|
||||
...(input.referenceImageSrcs?.length
|
||||
? { 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 }
|
||||
: {}),
|
||||
}),
|
||||
'生成图片失败',
|
||||
{
|
||||
@@ -581,6 +640,11 @@ export async function generateEditorIconSpritesheet(
|
||||
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
|
||||
...(input.imageSize ? { imageSize: input.imageSize } : {}),
|
||||
priceMudPoints: input.priceMudPoints,
|
||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||
...(input.generationInputs
|
||||
? { generationInputs: input.generationInputs }
|
||||
: {}),
|
||||
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
|
||||
}),
|
||||
'生成图标素材失败',
|
||||
{
|
||||
@@ -599,6 +663,14 @@ export async function extractEditorUiDesignAssets(
|
||||
EDITOR_UI_DESIGN_ASSET_EXTRACTION_API,
|
||||
jsonRequest('POST', {
|
||||
sourceImageSrc: input.sourceImageSrc,
|
||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||
...(input.generationInputs
|
||||
? { generationInputs: input.generationInputs }
|
||||
: {}),
|
||||
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
|
||||
...(input.spritesheetLabel
|
||||
? { spritesheetLabel: input.spritesheetLabel }
|
||||
: {}),
|
||||
}),
|
||||
'提取UI设计图素材失败',
|
||||
{
|
||||
@@ -618,6 +690,16 @@ export async function editEditorImage(input: EditorImageEditInput) {
|
||||
sourceImageSrc: input.sourceImageSrc,
|
||||
...(input.size ? { size: input.size } : {}),
|
||||
...(input.model ? { model: input.model } : {}),
|
||||
...(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 }
|
||||
: {}),
|
||||
}),
|
||||
'修改图片失败',
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user