支持画布生图选择抠图背景色
新增画布生图背景色选项并在角色、图标和 UI 素材提取入口展示选择控件。 将 screenColor 贯穿前端提交、后端请求解析、prompt 构建和生成后本地后处理调用。 保留角色动作旧绿幕路径,避免影响既有动画生成流程。 补充相关前后端单元测试和编辑器文档说明。
This commit is contained in:
@@ -291,6 +291,7 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
dialog={dialog}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
includeDimensions
|
||||
includeBackgroundColor
|
||||
onRememberImageModel={onRememberImageModel}
|
||||
optionLabelPrefix="生成图片"
|
||||
cost={calculateEditorImageGenerationPrice({
|
||||
|
||||
@@ -782,17 +782,19 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByRole('status').textContent).toContain('生成中');
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '一张明亮的拼图主视觉',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-default',
|
||||
assetFolderId: 'project',
|
||||
assetLabel: expect.stringMatching(/生成图片/u),
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '一张明亮的拼图主视觉',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-default',
|
||||
assetFolderId: 'project',
|
||||
assetLabel: expect.stringMatching(/生成图片/u),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
|
||||
@@ -3029,9 +3031,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
},
|
||||
],
|
||||
prompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
actualPrompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'ui-extract-task-1',
|
||||
@@ -3124,6 +3126,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceImageSrc: 'data:image/png;base64,marked-ui-design',
|
||||
screenColor: '#CFEFFF',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-ui-extract',
|
||||
@@ -3134,7 +3137,11 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect.objectContaining({
|
||||
title: '提取提示词',
|
||||
value:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: '抠图背景色',
|
||||
value: '浅雾蓝 #CFEFFF',
|
||||
}),
|
||||
]),
|
||||
references: expect.arrayContaining([
|
||||
@@ -3163,7 +3170,11 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect.objectContaining({
|
||||
title: '提取提示词',
|
||||
value:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: '抠图背景色',
|
||||
value: '浅雾蓝 #CFEFFF',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
|
||||
@@ -551,6 +551,7 @@ export function hydrateCanvasGenerationDialog(
|
||||
currentUserId,
|
||||
),
|
||||
imageModel: stringOrUndefined(snapshot.imageModel),
|
||||
screenColor: stringOrUndefined(snapshot.screenColor),
|
||||
videoModel:
|
||||
typeof snapshot.videoModel === 'string'
|
||||
? snapshot.videoModel
|
||||
|
||||
@@ -189,6 +189,7 @@ function createStageProps(): ImageCanvasStageViewProps {
|
||||
onExtractUiDesignAssets: vi.fn(),
|
||||
onUiAssetExtractionToolChange: vi.fn(),
|
||||
onUiAssetExtractionModelChange: vi.fn(),
|
||||
onUiAssetExtractionScreenColorChange: vi.fn(),
|
||||
onUiAssetExtractionPointerStart: vi.fn(),
|
||||
onUiAssetExtractionPointerMove: vi.fn(),
|
||||
onUiAssetExtractionPointerEnd: vi.fn(),
|
||||
@@ -337,6 +338,7 @@ describe('ImageCanvasEditorShellView', () => {
|
||||
sourceLayerId: layer.id,
|
||||
tool: 'rect' as const,
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
screenColor: '#CFEFFF',
|
||||
marks: [],
|
||||
references: [],
|
||||
draftMark: null,
|
||||
|
||||
@@ -208,6 +208,7 @@ export type GenerateDialogState = {
|
||||
publicationReferences?: CharacterReferenceImage[];
|
||||
uiDesignSpecReference?: CharacterReferenceImage | null;
|
||||
imageModel?: string;
|
||||
screenColor?: string;
|
||||
videoModel?: EditorVideoModel;
|
||||
videoAspectRatio?: EditorVideoAspectRatio;
|
||||
videoResolution?: EditorVideoResolution;
|
||||
|
||||
@@ -1179,6 +1179,7 @@ export function ImageCanvasEditorView() {
|
||||
quickEditSelectionSourceLayer,
|
||||
changeUiAssetExtractionTool,
|
||||
changeUiAssetExtractionModel,
|
||||
changeUiAssetExtractionScreenColor,
|
||||
changeQuickEditSelectionTool,
|
||||
startUiAssetExtractionPointer,
|
||||
startQuickEditSelectionPointer,
|
||||
@@ -1910,6 +1911,7 @@ export function ImageCanvasEditorView() {
|
||||
onExtractUiDesignAssets: extractUiDesignAssets,
|
||||
onUiAssetExtractionToolChange: changeUiAssetExtractionTool,
|
||||
onUiAssetExtractionModelChange: changeUiAssetExtractionModel,
|
||||
onUiAssetExtractionScreenColorChange: changeUiAssetExtractionScreenColor,
|
||||
onUiAssetExtractionPointerStart: startUiAssetExtractionPointer,
|
||||
onUiAssetExtractionPointerMove: moveUiAssetExtractionPointer,
|
||||
onUiAssetExtractionPointerEnd: endUiAssetExtractionPointer,
|
||||
|
||||
@@ -481,7 +481,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
{
|
||||
title: '提取提示词',
|
||||
value:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
},
|
||||
{ title: 'model', value: 'gpt-image-2' },
|
||||
],
|
||||
@@ -496,13 +496,16 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
const canvasSize = { width: 960, height: 720 };
|
||||
const viewport = { x: 0, y: 0, scale: 1 };
|
||||
const characterDraft = createSameSourceGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '红发骑士' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
sourceLayer: createLayer({
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '角色设定', value: '红发骑士' },
|
||||
{ title: '抠图背景色', value: '暖浅桃色 #FFD6C2' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
canvasSize,
|
||||
viewport,
|
||||
mode: 'redraw',
|
||||
@@ -512,6 +515,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
mode: 'character',
|
||||
prompt: '红发骑士',
|
||||
sourceLayerId: 'layer-source',
|
||||
screenColor: '#FFD6C2',
|
||||
placeholder: {
|
||||
x: 472,
|
||||
y: 140,
|
||||
@@ -579,6 +583,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
status: 'idle',
|
||||
generatedLayerId: 'layer-source',
|
||||
imageModel: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
},
|
||||
@@ -589,6 +594,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
prompt: '普通图',
|
||||
sourceLayerId: 'layer-source',
|
||||
imageModel: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
CHARACTER_ANIMATION_FRAME_ORIGINAL_SIZE,
|
||||
createCanvasLayerReference,
|
||||
DEFAULT_IMAGE_MODEL,
|
||||
DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
DEFAULT_PUBLICATION_GAME_INFO,
|
||||
DEFAULT_SOUND_EFFECT_DURATION_SECONDS,
|
||||
DEFAULT_SOUND_EFFECT_MODEL,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
inferEditorImageAspectRatio,
|
||||
inferEditorImageSizeLabel,
|
||||
normalizeEditorImageModel,
|
||||
normalizeEditorGenerationBackgroundColor,
|
||||
PUBLICATION_FRAME_ORIGINAL_SIZE,
|
||||
resolveEditorImageGenerationPixelSize,
|
||||
resolveEditorVideoGenerationPixelSize,
|
||||
@@ -272,6 +274,7 @@ export function createCharacterGenerationDialogDraft({
|
||||
characterSpecReference: null,
|
||||
characterReferences: [],
|
||||
imageModel: normalizedImageModel,
|
||||
screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
aspectRatio: dimensionDefaults.aspectRatio,
|
||||
imageSize: dimensionDefaults.imageSize,
|
||||
placeholder: {
|
||||
@@ -306,6 +309,7 @@ export function createIconGenerationDialogDraft({
|
||||
generationReferences: [],
|
||||
iconDescriptions: [],
|
||||
imageModel: normalizedImageModel,
|
||||
screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
aspectRatio: dimensionDefaults.aspectRatio,
|
||||
imageSize: dimensionDefaults.imageSize,
|
||||
placeholder: {
|
||||
@@ -738,9 +742,15 @@ function restoreSharedImageOptions(
|
||||
sourceLayer: CanvasLayer,
|
||||
sourceDialog?: CanvasGenerationDialogState | null,
|
||||
): Omit<CanvasGenerationDialogState, 'id'> {
|
||||
const fields = getGenerationInputFieldValues(sourceLayer);
|
||||
const sourceScreenColor =
|
||||
sourceDialog?.screenColor ?? fields.get('抠图背景色');
|
||||
return {
|
||||
...draft,
|
||||
imageModel: sourceDialog?.imageModel ?? draft.imageModel,
|
||||
screenColor: normalizeEditorGenerationBackgroundColor(
|
||||
sourceScreenColor ?? draft.screenColor,
|
||||
),
|
||||
aspectRatio:
|
||||
sourceDialog?.aspectRatio ??
|
||||
draft.aspectRatio ??
|
||||
|
||||
@@ -15,11 +15,13 @@ import {
|
||||
function ImageOptionsHarness({
|
||||
initialDialog,
|
||||
includeDimensions = true,
|
||||
includeBackgroundColor = false,
|
||||
onRememberImageModel = vi.fn(),
|
||||
lockedModel,
|
||||
}: {
|
||||
initialDialog: GenerateDialogState;
|
||||
includeDimensions?: boolean;
|
||||
includeBackgroundColor?: boolean;
|
||||
onRememberImageModel?: (model: string) => void;
|
||||
lockedModel?: string;
|
||||
}) {
|
||||
@@ -33,6 +35,7 @@ function ImageOptionsHarness({
|
||||
dialog={dialog}
|
||||
setGenerateDialog={setDialog}
|
||||
includeDimensions={includeDimensions}
|
||||
includeBackgroundColor={includeBackgroundColor}
|
||||
onRememberImageModel={onRememberImageModel}
|
||||
lockedModel={lockedModel}
|
||||
cost={calculateEditorImageGenerationPrice({
|
||||
@@ -45,6 +48,7 @@ function ImageOptionsHarness({
|
||||
<output aria-label="当前模型">{dialog.imageModel}</output>
|
||||
<output aria-label="当前比例">{dialog.aspectRatio}</output>
|
||||
<output aria-label="当前尺寸">{dialog.imageSize}</output>
|
||||
<output aria-label="当前背景色">{dialog.screenColor}</output>
|
||||
<output aria-label="当前状态">{dialog.status}</output>
|
||||
<output aria-label="当前错误">{dialog.errorMessage ?? '-'}</output>
|
||||
<output aria-label="当前占位">
|
||||
@@ -172,6 +176,49 @@ describe('ImageCanvasGenerationImageOptionsView', () => {
|
||||
).toBe('生成3泥点');
|
||||
});
|
||||
|
||||
it('updates the keyed background color from the color menu', () => {
|
||||
render(
|
||||
<ImageOptionsHarness
|
||||
includeBackgroundColor
|
||||
initialDialog={{
|
||||
mode: 'character',
|
||||
prompt: '',
|
||||
status: 'failed',
|
||||
errorMessage: '旧错误',
|
||||
imageModel: IMAGE_MODEL_NANOBANANA2,
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
screenColor: '#CFEFFF',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: '生成图片背景色 浅雾蓝 #CFEFFF',
|
||||
}),
|
||||
);
|
||||
const panel = screen.getByRole('menu', { name: '生成图片背景色选项' });
|
||||
fireEvent.click(
|
||||
within(panel).getByRole('button', { name: '淡薰衣草紫 #E6D8FF' }),
|
||||
);
|
||||
|
||||
expect(screen.getByRole('menu', { name: '生成图片背景色选项' })).toBeTruthy();
|
||||
expect(
|
||||
within(panel)
|
||||
.getByRole('button', { name: '淡薰衣草紫 #E6D8FF' })
|
||||
.getAttribute('aria-pressed'),
|
||||
).toBe('true');
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: '生成图片背景色 淡薰衣草紫 #E6D8FF',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByLabelText('当前背景色').textContent).toBe('#E6D8FF');
|
||||
expect(screen.getByLabelText('当前状态').textContent).toBe('idle');
|
||||
expect(screen.getByLabelText('当前错误').textContent).toBe('-');
|
||||
});
|
||||
|
||||
it('locks the model trigger when a fixed model is provided', () => {
|
||||
const rememberImageModel = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -16,8 +16,11 @@ import type { GenerateDialogState } from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||||
EDITOR_IMAGE_MODEL_OPTIONS,
|
||||
EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS,
|
||||
getEditorGenerationBackgroundColorOption,
|
||||
getEditorImageModelDisplayName,
|
||||
IMAGE_MODEL_NANOBANANA2,
|
||||
normalizeEditorGenerationBackgroundColor,
|
||||
normalizeEditorImageModel,
|
||||
resizeGenerationPlaceholderToImageSelection,
|
||||
resolveEditorImageSizeLabel,
|
||||
@@ -29,6 +32,7 @@ type ImageCanvasGenerationImageOptionsViewProps = {
|
||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
includeDimensions: boolean;
|
||||
includeModel?: boolean;
|
||||
includeBackgroundColor?: boolean;
|
||||
onRememberImageModel: (model: string) => void;
|
||||
optionLabelPrefix?: string;
|
||||
dimensionRatioAriaLabelPrefix?: string;
|
||||
@@ -45,7 +49,7 @@ type ImageCanvasGenerationImageOptionsViewProps = {
|
||||
) => CSSProperties;
|
||||
};
|
||||
|
||||
type OpenPanel = 'dimensions' | 'model' | null;
|
||||
type OpenPanel = 'dimensions' | 'model' | 'backgroundColor' | null;
|
||||
|
||||
function resetFailedDialogStatus(dialog: GenerateDialogState) {
|
||||
return {
|
||||
@@ -120,6 +124,7 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
setGenerateDialog,
|
||||
includeDimensions,
|
||||
includeModel = true,
|
||||
includeBackgroundColor = false,
|
||||
onRememberImageModel,
|
||||
optionLabelPrefix,
|
||||
dimensionRatioAriaLabelPrefix,
|
||||
@@ -134,6 +139,7 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
}: ImageCanvasGenerationImageOptionsViewProps) {
|
||||
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
|
||||
const dimensionsButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const backgroundColorButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const modelButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const normalizedLockedModel = lockedModel
|
||||
? normalizeEditorImageModel(lockedModel)
|
||||
@@ -144,6 +150,9 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
: dialog,
|
||||
);
|
||||
const selectedModelLabel = getEditorImageModelDisplayName(selection.model);
|
||||
const selectedBackgroundColor = getEditorGenerationBackgroundColorOption(
|
||||
dialog.screenColor,
|
||||
);
|
||||
const selectedSizeLabel = resolveEditorImageSizeLabel({
|
||||
aspectRatio: selection.aspectRatio,
|
||||
imageSize: selection.imageSize,
|
||||
@@ -160,7 +169,11 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
|
||||
useImageCanvasFloatingOptionDismiss({
|
||||
isOpen: openPanel !== null,
|
||||
boundaryRefs: [dimensionsButtonRef, modelButtonRef],
|
||||
boundaryRefs: [
|
||||
dimensionsButtonRef,
|
||||
backgroundColorButtonRef,
|
||||
modelButtonRef,
|
||||
],
|
||||
onDismiss: dismissOpenPanel,
|
||||
});
|
||||
|
||||
@@ -210,6 +223,17 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
});
|
||||
};
|
||||
|
||||
const updateBackgroundColor = (screenColor: string) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog && currentDialog.mode === dialog.mode
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
screenColor: normalizeEditorGenerationBackgroundColor(screenColor),
|
||||
}
|
||||
: currentDialog,
|
||||
);
|
||||
};
|
||||
|
||||
const togglePanel = (panel: Exclude<OpenPanel, null>) => {
|
||||
setOpenPanel((currentPanel) => (currentPanel === panel ? null : panel));
|
||||
};
|
||||
@@ -294,6 +318,76 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
{includeBackgroundColor ? (
|
||||
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--background-color">
|
||||
<PlatformInlineOptionButton
|
||||
ref={backgroundColorButtonRef}
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--background-color"
|
||||
aria-label={`${labelPrefix}背景色 ${selectedBackgroundColor.label} ${selectedBackgroundColor.value}`}
|
||||
aria-expanded={openPanel === 'backgroundColor'}
|
||||
disabled={isGenerating}
|
||||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||||
onClick={() => togglePanel('backgroundColor')}
|
||||
>
|
||||
<span className="image-canvas-editor__background-color-trigger-label">
|
||||
<span
|
||||
className="image-canvas-editor__background-color-swatch"
|
||||
style={{ backgroundColor: selectedBackgroundColor.value }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{selectedBackgroundColor.label}</span>
|
||||
<span>{selectedBackgroundColor.value}</span>
|
||||
</span>
|
||||
</PlatformInlineOptionButton>
|
||||
{openPanel === 'backgroundColor'
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__option-popover image-canvas-editor__option-popover--background-color image-canvas-editor__portal-menu"
|
||||
label={`${labelPrefix}背景色选项`}
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
backgroundColorButtonRef.current,
|
||||
'above',
|
||||
)}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="image-canvas-editor__option-popover-items image-canvas-editor__option-popover-items--background-color">
|
||||
{EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS.map(
|
||||
(option) => {
|
||||
const selected =
|
||||
selectedBackgroundColor.value === option.value;
|
||||
return (
|
||||
<OptionChoice
|
||||
key={option.value}
|
||||
selected={selected}
|
||||
className="image-canvas-editor__option-popover-choice--background-color"
|
||||
ariaLabel={`${option.label} ${option.value}`}
|
||||
onClick={() => updateBackgroundColor(option.value)}
|
||||
>
|
||||
<span
|
||||
className="image-canvas-editor__background-color-swatch"
|
||||
style={{ backgroundColor: option.value }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="image-canvas-editor__background-color-choice-text">
|
||||
<span>{option.label}</span>
|
||||
<span>{option.value}</span>
|
||||
</span>
|
||||
<Check
|
||||
className="image-canvas-editor__option-selected-check"
|
||||
data-visible={selected}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</OptionChoice>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
{includeModel ? (
|
||||
<div
|
||||
className={[
|
||||
|
||||
@@ -250,7 +250,10 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
],
|
||||
),
|
||||
).toEqual({
|
||||
fields: [{ title: '角色设定', value: '主角骑士' }],
|
||||
fields: [
|
||||
{ title: '角色设定', value: '主角骑士' },
|
||||
{ title: '抠图背景色', value: '浅雾蓝 #CFEFFF' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
title: '角色规范',
|
||||
@@ -275,7 +278,10 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
resourceId: 'resource-icon-spec',
|
||||
}),
|
||||
).toEqual({
|
||||
fields: [{ title: '素材描述', value: '返回按钮\n设置按钮' }],
|
||||
fields: [
|
||||
{ title: '素材描述', value: '返回按钮\n设置按钮' },
|
||||
{ title: '抠图背景色', value: '浅雾蓝 #CFEFFF' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
title: '图标规范',
|
||||
@@ -313,8 +319,9 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
{
|
||||
title: '提取提示词',
|
||||
value:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
},
|
||||
{ title: '抠图背景色', value: '浅雾蓝 #CFEFFF' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
|
||||
@@ -97,8 +97,20 @@ export const DEFAULT_ICON_DESCRIPTIONS = [
|
||||
'原图按钮',
|
||||
'冻结按钮',
|
||||
];
|
||||
export const EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS = [
|
||||
{ label: '浅雾蓝', value: '#CFEFFF' },
|
||||
{ label: '浅钢蓝', value: '#B0C2E0' },
|
||||
{ label: '暖浅桃色', value: '#FFD6C2' },
|
||||
{ label: '淡薰衣草紫', value: '#E6D8FF' },
|
||||
{ label: '浅粉灰', value: '#F4D8E8' },
|
||||
{ label: '中度天蓝', value: '#7FB3FF' },
|
||||
] as const;
|
||||
export type EditorGenerationBackgroundColorValue =
|
||||
(typeof EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS)[number]['value'];
|
||||
export const DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR =
|
||||
EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS[0].value;
|
||||
export const UI_DESIGN_ASSET_EXTRACTION_PROMPT =
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。';
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。';
|
||||
export const QUICK_EDIT_SIZE_PRESETS = [
|
||||
'1024x1024',
|
||||
'1536x1024',
|
||||
@@ -547,6 +559,36 @@ export function getEditorImageModelDisplayName(
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeEditorGenerationBackgroundColor(
|
||||
value: string | null | undefined,
|
||||
): EditorGenerationBackgroundColorValue {
|
||||
const normalizedValue = value?.trim().toUpperCase();
|
||||
const normalizedHexValue = normalizedValue?.match(/#[0-9A-F]{6}/)?.[0];
|
||||
const matched = EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS.find(
|
||||
(option) =>
|
||||
option.value === normalizedValue || option.value === normalizedHexValue,
|
||||
);
|
||||
return matched?.value ?? DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR;
|
||||
}
|
||||
|
||||
export function getEditorGenerationBackgroundColorOption(
|
||||
value: string | null | undefined,
|
||||
) {
|
||||
const normalizedValue = normalizeEditorGenerationBackgroundColor(value);
|
||||
return (
|
||||
EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS.find(
|
||||
(option) => option.value === normalizedValue,
|
||||
) ?? EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS[0]
|
||||
);
|
||||
}
|
||||
|
||||
export function formatEditorGenerationBackgroundColor(
|
||||
value: string | null | undefined,
|
||||
) {
|
||||
const option = getEditorGenerationBackgroundColorOption(value);
|
||||
return `${option.label} ${option.value}`;
|
||||
}
|
||||
|
||||
export function buildQuickEditModelOptions(currentModel: string) {
|
||||
const normalizedCurrentModel = normalizeEditorImageModel(currentModel);
|
||||
const options = [...QUICK_EDIT_MODEL_OPTIONS];
|
||||
@@ -1077,9 +1119,16 @@ export function buildCharacterGenerationInputs(
|
||||
prompt: string,
|
||||
specReference: CharacterReferenceImage | null | undefined,
|
||||
references: CharacterReferenceImage[] | undefined,
|
||||
screenColor?: string,
|
||||
): CanvasGenerationInputs {
|
||||
return {
|
||||
fields: createGenerationInputField('角色设定', prompt),
|
||||
fields: [
|
||||
...createGenerationInputField('角色设定', prompt),
|
||||
...createGenerationInputField(
|
||||
'抠图背景色',
|
||||
formatEditorGenerationBackgroundColor(screenColor),
|
||||
),
|
||||
],
|
||||
references: [
|
||||
...(specReference
|
||||
? createGenerationInputReference('角色规范', specReference)
|
||||
@@ -1184,6 +1233,7 @@ export function buildIconGenerationInputs(
|
||||
iconDescriptions: string[],
|
||||
specReference: CharacterReferenceImage,
|
||||
references?: CharacterReferenceImage[],
|
||||
screenColor?: string,
|
||||
): CanvasGenerationInputs {
|
||||
return {
|
||||
fields: [
|
||||
@@ -1191,6 +1241,10 @@ export function buildIconGenerationInputs(
|
||||
title: '素材描述',
|
||||
value: iconDescriptions.join('\n'),
|
||||
},
|
||||
...createGenerationInputField(
|
||||
'抠图背景色',
|
||||
formatEditorGenerationBackgroundColor(screenColor),
|
||||
),
|
||||
],
|
||||
references: [
|
||||
...createGenerationInputReference('图标规范', specReference),
|
||||
@@ -1204,12 +1258,19 @@ export function buildIconGenerationInputs(
|
||||
export function buildUiDesignAssetExtractionGenerationInputs(
|
||||
sourceLayer: CanvasLayer,
|
||||
references?: CharacterReferenceImage[],
|
||||
screenColor?: string,
|
||||
): CanvasGenerationInputs {
|
||||
return {
|
||||
fields: createGenerationInputField(
|
||||
'提取提示词',
|
||||
UI_DESIGN_ASSET_EXTRACTION_PROMPT,
|
||||
),
|
||||
fields: [
|
||||
...createGenerationInputField(
|
||||
'提取提示词',
|
||||
UI_DESIGN_ASSET_EXTRACTION_PROMPT,
|
||||
),
|
||||
...createGenerationInputField(
|
||||
'抠图背景色',
|
||||
formatEditorGenerationBackgroundColor(screenColor),
|
||||
),
|
||||
],
|
||||
references: [
|
||||
...createLayerGenerationInputReference('UI设计图', sourceLayer),
|
||||
...(references ?? []).flatMap((reference, index) =>
|
||||
|
||||
@@ -266,6 +266,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
prompt: ' 白发骑士 ',
|
||||
status: 'idle',
|
||||
imageModel: 'gpt-image-2',
|
||||
screenColor: '#FFD6C2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
characterSpecReference: {
|
||||
@@ -294,6 +295,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
prompt: '白发骑士',
|
||||
kind: 'character',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#FFD6C2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
referenceImageSrcs: [
|
||||
@@ -309,7 +311,10 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
});
|
||||
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual(
|
||||
{
|
||||
fields: [{ title: '角色设定', value: '白发骑士' }],
|
||||
fields: [
|
||||
{ title: '角色设定', value: '白发骑士' },
|
||||
{ title: '抠图背景色', value: '暖浅桃色 #FFD6C2' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
title: '角色规范',
|
||||
@@ -631,6 +636,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
prompt: ' 返回按钮 \n\n设置按钮',
|
||||
status: 'idle',
|
||||
imageModel: 'gpt-image-2',
|
||||
screenColor: '#7FB3FF',
|
||||
aspectRatio: '3:2',
|
||||
imageSize: '2K',
|
||||
iconSpecReference: {
|
||||
@@ -658,11 +664,15 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#7FB3FF',
|
||||
aspectRatio: '3:2',
|
||||
imageSize: '2K',
|
||||
},
|
||||
generationInputs: {
|
||||
fields: [{ title: '素材描述', value: '返回按钮\n设置按钮' }],
|
||||
fields: [
|
||||
{ title: '素材描述', value: '返回按钮\n设置按钮' },
|
||||
{ title: '抠图背景色', value: '中度天蓝 #7FB3FF' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
title: '图标规范',
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
inferEditorImageAspectRatio,
|
||||
inferEditorImageSizeLabel,
|
||||
normalizeEditorGenerationBackgroundColor,
|
||||
normalizeEditorImageModel,
|
||||
resolveCharacterAnimationSourceImageSrc,
|
||||
resolveEditorImageGenerationPixelSize,
|
||||
@@ -348,6 +349,9 @@ export function buildImageGenerationSubmissionPlan({
|
||||
),
|
||||
].filter((src): src is string => Boolean(src));
|
||||
const imageModel = normalizeEditorImageModel(dialog.imageModel);
|
||||
const screenColor = normalizeEditorGenerationBackgroundColor(
|
||||
dialog.screenColor,
|
||||
);
|
||||
return {
|
||||
kind: 'image',
|
||||
normalizedPrompt,
|
||||
@@ -355,6 +359,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
prompt: normalizedPrompt,
|
||||
kind: 'character',
|
||||
model: imageModel,
|
||||
screenColor,
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
@@ -366,6 +371,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
normalizedPrompt,
|
||||
dialog.characterSpecReference,
|
||||
dialog.characterReferences,
|
||||
screenColor,
|
||||
),
|
||||
},
|
||||
rememberImageModel: imageModel,
|
||||
@@ -594,6 +600,9 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
}
|
||||
|
||||
const rememberImageModel = normalizeEditorImageModel(dialog.imageModel);
|
||||
const screenColor = normalizeEditorGenerationBackgroundColor(
|
||||
dialog.screenColor,
|
||||
);
|
||||
return {
|
||||
ok: true,
|
||||
iconDescriptions,
|
||||
@@ -610,6 +619,7 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
: {}),
|
||||
iconDescriptions,
|
||||
model: rememberImageModel,
|
||||
screenColor,
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
},
|
||||
@@ -617,6 +627,7 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
iconDescriptions,
|
||||
dialog.iconSpecReference,
|
||||
dialog.generationReferences,
|
||||
screenColor,
|
||||
),
|
||||
rememberImageModel,
|
||||
};
|
||||
|
||||
@@ -239,6 +239,7 @@ export function ImageCanvasIconSpritesheetComposerView({
|
||||
dialog={dialog}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
includeDimensions
|
||||
includeBackgroundColor
|
||||
onRememberImageModel={onRememberImageModel}
|
||||
optionLabelPrefix="生成图片"
|
||||
cost={calculateEditorIconSpritesheetPrice(
|
||||
|
||||
@@ -128,6 +128,7 @@ export type ImageCanvasStageViewProps = {
|
||||
onExtractUiDesignAssets: (layer: CanvasLayer) => void;
|
||||
onUiAssetExtractionToolChange: (tool: UiAssetExtractionTool | null) => void;
|
||||
onUiAssetExtractionModelChange: (model: string) => void;
|
||||
onUiAssetExtractionScreenColorChange: (screenColor: string) => void;
|
||||
onUiAssetExtractionPointerStart: (point: { x: number; y: number }) => void;
|
||||
onUiAssetExtractionPointerMove: (point: { x: number; y: number }) => void;
|
||||
onUiAssetExtractionPointerEnd: () => void;
|
||||
@@ -250,6 +251,7 @@ export function ImageCanvasStageView({
|
||||
onExtractUiDesignAssets,
|
||||
onUiAssetExtractionToolChange,
|
||||
onUiAssetExtractionModelChange,
|
||||
onUiAssetExtractionScreenColorChange,
|
||||
onUiAssetExtractionPointerStart,
|
||||
onUiAssetExtractionPointerMove,
|
||||
onUiAssetExtractionPointerEnd,
|
||||
@@ -366,6 +368,7 @@ export function ImageCanvasStageView({
|
||||
onPointerMove={onUiAssetExtractionPointerMove}
|
||||
onPointerEnd={onUiAssetExtractionPointerEnd}
|
||||
onModelChange={onUiAssetExtractionModelChange}
|
||||
onScreenColorChange={onUiAssetExtractionScreenColorChange}
|
||||
onRequestUpload={onRequestUiAssetExtractionReferenceUpload}
|
||||
onRemoveReference={onRemoveUiAssetExtractionReference}
|
||||
onSubmit={onSubmitUiAssetExtraction}
|
||||
|
||||
@@ -30,6 +30,7 @@ export type UiAssetExtractionState = {
|
||||
sourceLayerId: string;
|
||||
tool: UiAssetExtractionTool | null;
|
||||
model: string;
|
||||
screenColor: string;
|
||||
marks: UiAssetExtractionMark[];
|
||||
references: CharacterReferenceImage[];
|
||||
draftMark: UiAssetExtractionMark | null;
|
||||
@@ -48,12 +49,17 @@ export type UiAssetExtractionImageSize =
|
||||
|
||||
export function createUiAssetExtractionState(
|
||||
sourceLayerId: string,
|
||||
options: { initialTool?: UiAssetExtractionTool | null; model: string },
|
||||
options: {
|
||||
initialTool?: UiAssetExtractionTool | null;
|
||||
model: string;
|
||||
screenColor: string;
|
||||
},
|
||||
): UiAssetExtractionState {
|
||||
return {
|
||||
sourceLayerId,
|
||||
tool: options.initialTool === undefined ? 'rect' : options.initialTool,
|
||||
model: options.model,
|
||||
screenColor: options.screenColor,
|
||||
marks: [],
|
||||
references: [],
|
||||
draftMark: null,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
IMAGE_MODEL_NANOBANANA2,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
@@ -65,6 +66,7 @@ function createState(
|
||||
sourceLayerId: 'layer-source',
|
||||
tool: 'rect',
|
||||
model: IMAGE_MODEL_NANOBANANA2,
|
||||
screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
marks: [],
|
||||
references: [],
|
||||
draftMark: null,
|
||||
@@ -172,6 +174,11 @@ describe('ImageCanvasUiAssetExtractionOverlayView', () => {
|
||||
expect(
|
||||
within(panel).getByRole('button', { name: '提取素材模型 nanobanana2' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(panel).getByRole('button', {
|
||||
name: '提取素材背景色 浅雾蓝 #CFEFFF',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(within(panel).getByLabelText('参考UI.png')).toBeTruthy();
|
||||
fireEvent.click(
|
||||
within(panel).getByRole('button', { name: '上传UI素材参考图' }),
|
||||
@@ -230,6 +237,50 @@ describe('ImageCanvasUiAssetExtractionOverlayView', () => {
|
||||
expect(onModelChange).toHaveBeenCalledWith(IMAGE_MODEL_GPT_IMAGE_2);
|
||||
});
|
||||
|
||||
it('changes the UI extraction screen color from the background color menu', () => {
|
||||
const onScreenColorChange = vi.fn();
|
||||
|
||||
render(
|
||||
<ImageCanvasUiAssetExtractionOverlayView
|
||||
sourceLayer={createLayer({ src: 'data:image/png;base64,ui' })}
|
||||
viewport={{ x: 10, y: 20, scale: 1 }}
|
||||
state={createState({
|
||||
marks: [
|
||||
{
|
||||
id: 'mark-1',
|
||||
tool: 'rect',
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 80,
|
||||
height: 60,
|
||||
},
|
||||
],
|
||||
})}
|
||||
onToolChange={vi.fn()}
|
||||
onPointerStart={vi.fn()}
|
||||
onPointerMove={vi.fn()}
|
||||
onPointerEnd={vi.fn()}
|
||||
onScreenColorChange={onScreenColorChange}
|
||||
onSubmit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: '提取素材背景色 浅雾蓝 #CFEFFF',
|
||||
}),
|
||||
);
|
||||
fireEvent.click(
|
||||
within(
|
||||
screen.getByRole('menu', { name: '提取素材背景色选项' }),
|
||||
).getByRole('button', {
|
||||
name: '暖浅桃色 #FFD6C2',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(onScreenColorChange).toHaveBeenCalledWith('#FFD6C2');
|
||||
});
|
||||
|
||||
it('crops mark previews from the same source coordinates as the red selection', () => {
|
||||
render(
|
||||
<ImageCanvasUiAssetExtractionOverlayView
|
||||
|
||||
@@ -20,7 +20,9 @@ import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
import type { CanvasLayer, CanvasViewport } from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
calculateEditorIconSpritesheetPrice,
|
||||
EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS,
|
||||
EDITOR_IMAGE_MODEL_OPTIONS,
|
||||
getEditorGenerationBackgroundColorOption,
|
||||
getEditorImageModelDisplayName,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
||||
@@ -45,6 +47,7 @@ type ImageCanvasUiAssetExtractionOverlayViewProps = {
|
||||
onPointerMove: (point: { x: number; y: number }) => void;
|
||||
onPointerEnd: () => void;
|
||||
onModelChange?: (model: string) => void;
|
||||
onScreenColorChange?: (screenColor: string) => void;
|
||||
onRequestUpload?: () => void;
|
||||
onRemoveReference?: (referenceId: string) => void;
|
||||
onSubmit?: () => void;
|
||||
@@ -272,11 +275,13 @@ export function ImageCanvasUiAssetExtractionOverlayView({
|
||||
onPointerMove,
|
||||
onPointerEnd,
|
||||
onModelChange = () => {},
|
||||
onScreenColorChange = () => {},
|
||||
onRequestUpload,
|
||||
onRemoveReference,
|
||||
onSubmit,
|
||||
}: ImageCanvasUiAssetExtractionOverlayViewProps) {
|
||||
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
|
||||
const [isScreenColorMenuOpen, setIsScreenColorMenuOpen] = useState(false);
|
||||
if (!sourceLayer || !state) {
|
||||
return null;
|
||||
}
|
||||
@@ -291,6 +296,9 @@ export function ImageCanvasUiAssetExtractionOverlayView({
|
||||
extractionPlan.imageSize,
|
||||
);
|
||||
const selectedModelLabel = getEditorImageModelDisplayName(state.model);
|
||||
const selectedScreenColor = getEditorGenerationBackgroundColorOption(
|
||||
state.screenColor,
|
||||
);
|
||||
const screenFrame = {
|
||||
left: viewport.x + sourceLayer.x * viewport.scale,
|
||||
top: viewport.y + sourceLayer.y * viewport.scale,
|
||||
@@ -451,6 +459,71 @@ export function ImageCanvasUiAssetExtractionOverlayView({
|
||||
>
|
||||
{`${extractionPlan.aspectRatio}·${extractionPlan.imageSize}`}
|
||||
</span>
|
||||
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--background-color image-canvas-editor__ui-extraction-screen-color">
|
||||
<PlatformInlineOptionButton
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--background-color"
|
||||
aria-label={`提取素材背景色 ${selectedScreenColor.label} ${selectedScreenColor.value}`}
|
||||
aria-expanded={isScreenColorMenuOpen}
|
||||
disabled={state.status === 'extracting'}
|
||||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||||
onClick={() => setIsScreenColorMenuOpen((open) => !open)}
|
||||
>
|
||||
<span className="image-canvas-editor__background-color-trigger-label">
|
||||
<span
|
||||
className="image-canvas-editor__background-color-swatch"
|
||||
style={{ backgroundColor: selectedScreenColor.value }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{selectedScreenColor.label}</span>
|
||||
<span>{selectedScreenColor.value}</span>
|
||||
</span>
|
||||
</PlatformInlineOptionButton>
|
||||
{isScreenColorMenuOpen ? (
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__option-popover image-canvas-editor__option-popover--background-color"
|
||||
label="提取素材背景色选项"
|
||||
placement="top-start"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="image-canvas-editor__option-popover-items image-canvas-editor__option-popover-items--background-color">
|
||||
{EDITOR_GENERATION_BACKGROUND_COLOR_OPTIONS.map(
|
||||
(option) => {
|
||||
const selected =
|
||||
selectedScreenColor.value === option.value;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className="image-canvas-editor__option-popover-choice image-canvas-editor__option-popover-choice--background-color"
|
||||
aria-label={`${option.label} ${option.value}`}
|
||||
aria-pressed={selected}
|
||||
onClick={() => {
|
||||
onScreenColorChange(option.value);
|
||||
setIsScreenColorMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="image-canvas-editor__background-color-swatch"
|
||||
style={{ backgroundColor: option.value }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="image-canvas-editor__background-color-choice-text">
|
||||
<span>{option.label}</span>
|
||||
<span>{option.value}</span>
|
||||
</span>
|
||||
<Check
|
||||
className="image-canvas-editor__option-selected-check"
|
||||
data-visible={selected}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</PlatformFloatingMenu>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model image-canvas-editor__ui-extraction-model">
|
||||
<PlatformInlineOptionButton
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
|
||||
|
||||
@@ -642,6 +642,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
sourceLayerId: 'layer-source',
|
||||
tool: 'rect',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
screenColor: '#CFEFFF',
|
||||
marks: [
|
||||
{
|
||||
id: 'quick-edit-selection-mark-1',
|
||||
@@ -1717,9 +1718,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
spritesheetWidth: 512,
|
||||
spritesheetHeight: 512,
|
||||
prompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
actualPrompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'task-ui-assets',
|
||||
@@ -1745,6 +1746,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect.objectContaining({
|
||||
sourceImageSrc: 'generated/ui-design.png',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
screenColor: '#CFEFFF',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1771,9 +1773,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
spritesheetWidth: 512,
|
||||
spritesheetHeight: 512,
|
||||
prompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
actualPrompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'task-ui-assets',
|
||||
@@ -1811,9 +1813,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
spritesheetWidth: 512,
|
||||
spritesheetHeight: 512,
|
||||
prompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
actualPrompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'task-ui-assets',
|
||||
@@ -1859,6 +1861,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect.objectContaining({
|
||||
sourceImageSrc: 'data:image/png;base64,marked-ui-design',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
screenColor: '#CFEFFF',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '2K',
|
||||
generationInputs: expect.objectContaining({
|
||||
@@ -1866,7 +1869,11 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect.objectContaining({
|
||||
title: '提取提示词',
|
||||
value:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: '抠图背景色',
|
||||
value: '浅雾蓝 #CFEFFF',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
@@ -1881,9 +1888,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
spritesheetWidth: 512,
|
||||
spritesheetHeight: 512,
|
||||
prompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
actualPrompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'task-ui-assets',
|
||||
@@ -1907,6 +1914,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
await waitFor(() => {
|
||||
expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
screenColor: '#CFEFFF',
|
||||
referenceImageSrcs: ['generated/ref-style.png'],
|
||||
generationInputs: expect.objectContaining({
|
||||
references: expect.arrayContaining([
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
inferEditorImageAspectRatio,
|
||||
inferEditorImageSizeLabel,
|
||||
isCanvasGenerationDialog,
|
||||
normalizeEditorGenerationBackgroundColor,
|
||||
normalizeEditorImageModel,
|
||||
resolveCharacterAnimationSourceImageSrc,
|
||||
resolveEditorImageGenerationPixelSize,
|
||||
@@ -92,6 +93,7 @@ type CanvasGenerationDialogUpdater = (
|
||||
type UiDesignAssetExtractionOptions = {
|
||||
marks?: UiAssetExtractionMark[];
|
||||
model?: string;
|
||||
screenColor?: string;
|
||||
references?: CharacterReferenceImage[];
|
||||
suppressAlert?: boolean;
|
||||
};
|
||||
@@ -752,9 +754,13 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
marks: options.marks,
|
||||
});
|
||||
}
|
||||
const screenColor = normalizeEditorGenerationBackgroundColor(
|
||||
options.screenColor,
|
||||
);
|
||||
const generationInputs = buildUiDesignAssetExtractionGenerationInputs(
|
||||
sourceLayer,
|
||||
options.references,
|
||||
screenColor,
|
||||
);
|
||||
const referenceImageSrcs = (options.references ?? [])
|
||||
.map((reference) => resolveImageReferenceSubmissionSource(reference))
|
||||
@@ -776,6 +782,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
model: normalizeEditorImageModel(
|
||||
options.model ?? DEFAULT_IMAGE_MODEL,
|
||||
),
|
||||
screenColor,
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
projectId,
|
||||
generationInputs,
|
||||
|
||||
@@ -238,7 +238,7 @@ function GenerationWorkflowHarness({
|
||||
</span>
|
||||
<span data-testid="ui-extraction">
|
||||
{workflow.uiAssetExtractionState
|
||||
? `${workflow.uiAssetExtractionState.sourceLayerId}:${workflow.uiAssetExtractionState.tool ?? 'none'}:${workflow.uiAssetExtractionState.marks.length}:${workflow.uiAssetExtractionState.status}:${workflow.uiAssetExtractionState.model}`
|
||||
? `${workflow.uiAssetExtractionState.sourceLayerId}:${workflow.uiAssetExtractionState.tool ?? 'none'}:${workflow.uiAssetExtractionState.marks.length}:${workflow.uiAssetExtractionState.status}:${workflow.uiAssetExtractionState.model}:${workflow.uiAssetExtractionState.screenColor}`
|
||||
: '-'}
|
||||
</span>
|
||||
<span data-testid="ui-extraction-source">
|
||||
@@ -1733,7 +1733,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开UI素材框选' }));
|
||||
|
||||
expect(screen.getByTestId('ui-extraction').textContent).toBe(
|
||||
'layer-source:rect:0:idle:gemini-3.1-flash-image-preview',
|
||||
'layer-source:rect:0:idle:gemini-3.1-flash-image-preview:#CFEFFF',
|
||||
);
|
||||
expect(screen.getByTestId('ui-extraction-source').textContent).toBe(
|
||||
'layer-source',
|
||||
@@ -1746,7 +1746,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '框选UI素材' }));
|
||||
|
||||
expect(screen.getByTestId('ui-extraction').textContent).toBe(
|
||||
'layer-source:ellipse:1:idle:gpt-image-2',
|
||||
'layer-source:ellipse:1:idle:gpt-image-2:#CFEFFF',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -69,12 +69,14 @@ import {
|
||||
calculateCharacterAnimationPrice,
|
||||
CHARACTER_ANIMATION_DURATION_OPTIONS,
|
||||
CHARACTER_ANIMATION_MODEL,
|
||||
DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
DEFAULT_ICON_DESCRIPTIONS,
|
||||
DEFAULT_IMAGE_MODEL,
|
||||
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||||
ICON_DESCRIPTION_LIMIT,
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
isCanvasGenerationDialog,
|
||||
normalizeEditorGenerationBackgroundColor,
|
||||
normalizeEditorImageModel,
|
||||
resizeGenerationPlaceholderToImageSelection,
|
||||
resolveEditorImageGenerationPixelSize,
|
||||
@@ -1083,6 +1085,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const nextSelectionState = createUiAssetExtractionState(sourceLayer.id, {
|
||||
initialTool: null,
|
||||
model: DEFAULT_IMAGE_MODEL,
|
||||
screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
});
|
||||
quickEditSelectionStateRef.current = nextSelectionState;
|
||||
setQuickEditSelectionState(nextSelectionState);
|
||||
@@ -1770,6 +1773,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
createUiAssetExtractionState(sourceLayer.id, {
|
||||
initialTool: 'rect',
|
||||
model: DEFAULT_IMAGE_MODEL,
|
||||
screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
}),
|
||||
);
|
||||
setViewport(resolveQuickEditFocusViewport({ sourceLayer, canvasSize }));
|
||||
@@ -1829,6 +1833,28 @@ export function useImageCanvasGenerationWorkflow({
|
||||
[rememberImageModel],
|
||||
);
|
||||
|
||||
const changeUiAssetExtractionScreenColor = useCallback(
|
||||
(screenColor: string) => {
|
||||
const normalizedScreenColor =
|
||||
normalizeEditorGenerationBackgroundColor(screenColor);
|
||||
setUiAssetExtractionState((currentState) =>
|
||||
currentState
|
||||
? {
|
||||
...currentState,
|
||||
screenColor: normalizedScreenColor,
|
||||
status:
|
||||
currentState.status === 'failed' ? 'idle' : currentState.status,
|
||||
errorMessage:
|
||||
currentState.status === 'failed'
|
||||
? undefined
|
||||
: currentState.errorMessage,
|
||||
}
|
||||
: currentState,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const appendUiAssetExtractionReferences = useCallback(
|
||||
(references: CharacterReferenceImage[]) => {
|
||||
if (!references.length) {
|
||||
@@ -2065,6 +2091,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
await submitUiDesignAssetExtraction(sourceLayer, {
|
||||
marks: currentState.marks,
|
||||
model: currentState.model,
|
||||
screenColor: currentState.screenColor,
|
||||
references: currentState.references,
|
||||
suppressAlert: true,
|
||||
});
|
||||
@@ -2194,6 +2221,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
quickEditSelectionSourceLayer,
|
||||
changeUiAssetExtractionTool,
|
||||
changeUiAssetExtractionModel,
|
||||
changeUiAssetExtractionScreenColor,
|
||||
appendUiAssetExtractionReferences,
|
||||
removeUiAssetExtractionReference,
|
||||
changeQuickEditSelectionTool,
|
||||
@@ -2298,6 +2326,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
quickEditSelectionSourceLayer,
|
||||
changeUiAssetExtractionTool,
|
||||
changeUiAssetExtractionModel,
|
||||
changeUiAssetExtractionScreenColor,
|
||||
appendUiAssetExtractionReferences,
|
||||
removeUiAssetExtractionReference,
|
||||
changeQuickEditSelectionTool,
|
||||
|
||||
+105
-4
@@ -7021,7 +7021,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
.image-canvas-editor__generation-composer-footer {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
||||
grid-template-columns: auto auto minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 0.46rem;
|
||||
position: relative;
|
||||
@@ -7313,6 +7313,14 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-cluster--background-color {
|
||||
max-width: min(16.5rem, 34vw);
|
||||
justify-self: start;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover-anchor {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
@@ -7324,10 +7332,15 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover-anchor--model {
|
||||
grid-column: 3;
|
||||
grid-column: 4;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover-anchor--background-color {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.image-canvas-editor__model-trigger-label {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
@@ -7360,6 +7373,40 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.image-canvas-editor__background-color-trigger-label {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
gap: 0.34rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-canvas-editor__background-color-trigger-label span:nth-child(2),
|
||||
.image-canvas-editor__background-color-trigger-label span:nth-child(3) {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-canvas-editor__background-color-trigger-label span:nth-child(3) {
|
||||
color: #64748b;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.image-canvas-editor__background-color-swatch {
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
border: 1px solid rgba(15, 23, 42, 0.18);
|
||||
border-radius: 0.18rem;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -7385,6 +7432,11 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover-anchor--background-color
|
||||
.image-canvas-editor__option-popover {
|
||||
min-width: 17.5rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover-close {
|
||||
position: absolute;
|
||||
right: 0.54rem;
|
||||
@@ -7437,6 +7489,10 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
min-width: 13.5rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover--background-color {
|
||||
padding-top: 0.95rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover-choice {
|
||||
display: inline-flex;
|
||||
min-width: 4.5rem;
|
||||
@@ -7467,6 +7523,42 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
box-shadow: inset 0 0 0 1px var(--image-canvas-brand-border-strong);
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover-items--background-color {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover-choice--background-color {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
justify-content: stretch;
|
||||
gap: 0.58rem;
|
||||
border-radius: 0.82rem;
|
||||
padding: 0.46rem 0.62rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.image-canvas-editor__background-color-choice-text {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__background-color-choice-text span:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-canvas-editor__background-color-choice-text span:last-child {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover--audio-options {
|
||||
display: grid;
|
||||
min-width: 16.4rem;
|
||||
@@ -7705,7 +7797,7 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-submit {
|
||||
grid-column: 4;
|
||||
grid-column: 5;
|
||||
min-width: 5.45rem;
|
||||
height: 2.25rem;
|
||||
gap: 0.4rem;
|
||||
@@ -8491,6 +8583,12 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.image-canvas-editor__ui-extraction-screen-color {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
width: min(16.5rem, 34vw);
|
||||
}
|
||||
|
||||
.image-canvas-editor__ui-extraction-panel
|
||||
.image-canvas-editor__readonly-generation-option {
|
||||
display: inline-flex;
|
||||
@@ -8507,11 +8605,13 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
}
|
||||
|
||||
.image-canvas-editor__ui-extraction-model {
|
||||
grid-column: 2;
|
||||
grid-column: 4;
|
||||
justify-self: end;
|
||||
width: min(15.5rem, 42vw);
|
||||
}
|
||||
|
||||
.image-canvas-editor__ui-extraction-screen-color
|
||||
.image-canvas-editor__option-cluster,
|
||||
.image-canvas-editor__ui-extraction-model .image-canvas-editor__option-cluster {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -9083,6 +9183,7 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-cluster--dimensions,
|
||||
.image-canvas-editor__option-cluster--background-color,
|
||||
.image-canvas-editor__option-cluster--model,
|
||||
.image-canvas-editor__readonly-generation-option,
|
||||
.image-canvas-editor__generation-submit,
|
||||
|
||||
@@ -699,6 +699,7 @@ describe('editorProjectClient', () => {
|
||||
prompt: '角色设定',
|
||||
kind: 'character',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#FFD6C2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-1',
|
||||
@@ -721,6 +722,7 @@ describe('editorProjectClient', () => {
|
||||
prompt: '角色设定',
|
||||
kind: 'character',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#FFD6C2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '1K',
|
||||
projectId: 'editor-project-1',
|
||||
@@ -862,6 +864,7 @@ describe('editorProjectClient', () => {
|
||||
referenceImageSrc: 'data:image/png;base64,spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '2K',
|
||||
projectId: 'editor-project-1',
|
||||
@@ -881,6 +884,7 @@ describe('editorProjectClient', () => {
|
||||
referenceImageSrc: 'data:image/png;base64,spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '2K',
|
||||
projectId: 'editor-project-1',
|
||||
@@ -913,9 +917,9 @@ describe('editorProjectClient', () => {
|
||||
},
|
||||
],
|
||||
prompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板或绿色阴影。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯色背景 中度天蓝 #7FB3FF / RGB(127,179,255),方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板或阴影。',
|
||||
actualPrompt:
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板或绿色阴影。',
|
||||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯色背景 中度天蓝 #7FB3FF / RGB(127,179,255),方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板或阴影。',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'ui-asset-extraction-1',
|
||||
@@ -924,6 +928,7 @@ describe('editorProjectClient', () => {
|
||||
const result = await extractEditorUiDesignAssets({
|
||||
sourceImageSrc: 'data:image/png;base64,ui-design',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#7FB3FF',
|
||||
referenceImageSrcs: ['data:image/png;base64,ui-ref'],
|
||||
projectId: 'editor-project-1',
|
||||
assetFolderId: 'project',
|
||||
@@ -945,6 +950,7 @@ describe('editorProjectClient', () => {
|
||||
body: JSON.stringify({
|
||||
sourceImageSrc: 'data:image/png;base64,ui-design',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#7FB3FF',
|
||||
referenceImageSrcs: ['data:image/png;base64,ui-ref'],
|
||||
projectId: 'editor-project-1',
|
||||
generationInputs: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user