调整图片编辑器参考图选择交互
- 常规参考图入口改为先弹出来源菜单,支持从画布选择和上传图片。 - 角色规范、图标规范和常规参考图来源菜单统一向上弹出。 - 画布参考图选择拦截普通图层选中逻辑,保持生成面板不隐藏。 - 补充图片编辑器交互测试与技术文档说明。
This commit is contained in:
@@ -35,7 +35,10 @@ import {
|
||||
CHARACTER_ANIMATION_RATIO_OPTIONS,
|
||||
CHARACTER_SPEC_VIEW_OPTIONS,
|
||||
DEFAULT_ICON_DESCRIPTIONS,
|
||||
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||||
EDITOR_IMAGE_MODEL_OPTIONS,
|
||||
ICON_DESCRIPTION_LIMIT,
|
||||
IMAGE_MODEL_NANOBANANA2,
|
||||
SPEC_GENERATION_COST,
|
||||
SPEC_TYPE_LABEL,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
@@ -52,11 +55,14 @@ import type {
|
||||
type ImageCanvasGenerationComposerViewProps = {
|
||||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
isSpecMenuOpen: boolean;
|
||||
isCharacterSpecMenuOpen: boolean;
|
||||
isCharacterReferenceMenuOpen: boolean;
|
||||
isIconSpecMenuOpen: boolean;
|
||||
isPickingCharacterSpecFromCanvas: boolean;
|
||||
isPickingCharacterReferenceFromCanvas: boolean;
|
||||
isPickingIconSpecFromCanvas: boolean;
|
||||
generateDialog: GenerateDialogState | null;
|
||||
generationComposerStyle: CSSProperties | null;
|
||||
@@ -76,8 +82,10 @@ type ImageCanvasGenerationComposerViewProps = {
|
||||
SetStateAction<CharacterAnimationPanelState | null>
|
||||
>;
|
||||
setIsCharacterSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsCharacterReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingCharacterSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingCharacterReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||
onOpenSpecDialog: (specType: SpecGenerationType) => void;
|
||||
onRequestUpload: (target: UploadTarget) => void;
|
||||
@@ -90,6 +98,7 @@ type ImageCanvasGenerationComposerViewProps = {
|
||||
onUpdateIconDescription: (index: number, value: string) => void;
|
||||
onAddIconDescription: () => void;
|
||||
onUpdateCharacterAnimationDuration: (frameCountValue: string) => void;
|
||||
onRememberImageModel: (model: string) => void;
|
||||
};
|
||||
|
||||
function triggerPlaceholderAction(label: string) {
|
||||
@@ -152,14 +161,161 @@ function resetFailedPanelStatus<T extends { status: string; errorMessage?: strin
|
||||
};
|
||||
}
|
||||
|
||||
function getImageDimensionOptions(model: string | null | undefined) {
|
||||
return (
|
||||
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
||||
(model ?? IMAGE_MODEL_NANOBANANA2) as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
||||
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[IMAGE_MODEL_NANOBANANA2]
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeImageDialogSelection(dialog: GenerateDialogState) {
|
||||
const model = dialog.imageModel ?? IMAGE_MODEL_NANOBANANA2;
|
||||
const options = getImageDimensionOptions(model);
|
||||
const aspectRatios = options.aspectRatios as readonly string[];
|
||||
const imageSizes = options.imageSizes as readonly string[];
|
||||
return {
|
||||
model,
|
||||
aspectRatio:
|
||||
dialog.aspectRatio && aspectRatios.includes(dialog.aspectRatio)
|
||||
? dialog.aspectRatio
|
||||
: options.aspectRatios[0],
|
||||
imageSize:
|
||||
dialog.imageSize && imageSizes.includes(dialog.imageSize)
|
||||
? dialog.imageSize
|
||||
: (options.imageSizes.find((size) => size === '1K') ?? options.imageSizes[0]),
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
function renderImageOptionButtons({
|
||||
dialog,
|
||||
setGenerateDialog,
|
||||
includeDimensions,
|
||||
onRememberImageModel,
|
||||
}: {
|
||||
dialog: GenerateDialogState;
|
||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
includeDimensions: boolean;
|
||||
onRememberImageModel: (model: string) => void;
|
||||
}) {
|
||||
const selection = normalizeImageDialogSelection(dialog);
|
||||
const updateDialog = (patch: Partial<GenerateDialogState>) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog && currentDialog.mode === dialog.mode
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
...patch,
|
||||
}
|
||||
: currentDialog,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{includeDimensions ? (
|
||||
<>
|
||||
<div className="image-canvas-editor__option-field">
|
||||
<PlatformFieldLabel
|
||||
variant="field"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
画面比例
|
||||
</PlatformFieldLabel>
|
||||
<div className="image-canvas-editor__inline-option-group">
|
||||
{selection.options.aspectRatios.map((aspectRatio) => (
|
||||
<PlatformInlineOptionButton
|
||||
key={aspectRatio}
|
||||
className="image-canvas-editor__generation-ratio"
|
||||
disabled={dialog.status === 'generating'}
|
||||
aria-pressed={selection.aspectRatio === aspectRatio}
|
||||
onClick={() => updateDialog({ aspectRatio })}
|
||||
>
|
||||
{aspectRatio}
|
||||
</PlatformInlineOptionButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="image-canvas-editor__option-field">
|
||||
<PlatformFieldLabel
|
||||
variant="field"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
大小尺寸
|
||||
</PlatformFieldLabel>
|
||||
<div className="image-canvas-editor__inline-option-group">
|
||||
{selection.options.imageSizes.map((imageSize) => (
|
||||
<PlatformInlineOptionButton
|
||||
key={imageSize}
|
||||
className="image-canvas-editor__generation-ratio"
|
||||
disabled={dialog.status === 'generating'}
|
||||
aria-pressed={selection.imageSize === imageSize}
|
||||
onClick={() => updateDialog({ imageSize })}
|
||||
>
|
||||
{imageSize}
|
||||
</PlatformInlineOptionButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div className="image-canvas-editor__option-field">
|
||||
<PlatformFieldLabel
|
||||
variant="field"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
模型
|
||||
</PlatformFieldLabel>
|
||||
<div className="image-canvas-editor__inline-option-group">
|
||||
{EDITOR_IMAGE_MODEL_OPTIONS.map((option) => {
|
||||
const nextOptions = getImageDimensionOptions(option.value);
|
||||
const nextAspectRatios = nextOptions.aspectRatios as readonly string[];
|
||||
const nextImageSizes = nextOptions.imageSizes as readonly string[];
|
||||
return (
|
||||
<PlatformInlineOptionButton
|
||||
key={option.value}
|
||||
className="image-canvas-editor__generation-model"
|
||||
disabled={dialog.status === 'generating'}
|
||||
aria-pressed={selection.model === option.value}
|
||||
onClick={() => {
|
||||
onRememberImageModel(option.value);
|
||||
updateDialog({
|
||||
imageModel: option.value,
|
||||
aspectRatio:
|
||||
dialog.aspectRatio &&
|
||||
nextAspectRatios.includes(dialog.aspectRatio)
|
||||
? dialog.aspectRatio
|
||||
: nextOptions.aspectRatios[0],
|
||||
imageSize:
|
||||
dialog.imageSize &&
|
||||
nextImageSizes.includes(dialog.imageSize)
|
||||
? dialog.imageSize
|
||||
: (nextOptions.imageSizes.find((size) => size === '1K') ??
|
||||
nextOptions.imageSizes[0]),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</PlatformInlineOptionButton>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ImageCanvasGenerationComposerView({
|
||||
specToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
iconSpecButtonRef,
|
||||
isSpecMenuOpen,
|
||||
isCharacterSpecMenuOpen,
|
||||
isCharacterReferenceMenuOpen,
|
||||
isIconSpecMenuOpen,
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
isPickingCharacterReferenceFromCanvas,
|
||||
isPickingIconSpecFromCanvas,
|
||||
generateDialog,
|
||||
generationComposerStyle,
|
||||
@@ -177,8 +333,10 @@ export function ImageCanvasGenerationComposerView({
|
||||
setQuickEditPanel,
|
||||
setCharacterAnimationPanel,
|
||||
setIsCharacterSpecMenuOpen,
|
||||
setIsCharacterReferenceMenuOpen,
|
||||
setIsIconSpecMenuOpen,
|
||||
setIsPickingCharacterSpecFromCanvas,
|
||||
setIsPickingCharacterReferenceFromCanvas,
|
||||
setIsPickingIconSpecFromCanvas,
|
||||
onOpenSpecDialog,
|
||||
onRequestUpload,
|
||||
@@ -191,6 +349,7 @@ export function ImageCanvasGenerationComposerView({
|
||||
onUpdateIconDescription,
|
||||
onAddIconDescription,
|
||||
onUpdateCharacterAnimationDuration,
|
||||
onRememberImageModel,
|
||||
}: ImageCanvasGenerationComposerViewProps) {
|
||||
return (
|
||||
<>
|
||||
@@ -544,10 +703,10 @@ export function ImageCanvasGenerationComposerView({
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
||||
label="角色形象规范来源"
|
||||
placement="bottom-start"
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
characterSpecButtonRef.current,
|
||||
'below',
|
||||
'above',
|
||||
)}
|
||||
>
|
||||
<PlatformFloatingMenuItem
|
||||
@@ -603,10 +762,13 @@ export function ImageCanvasGenerationComposerView({
|
||||
),
|
||||
)}
|
||||
<button
|
||||
ref={characterReferenceButtonRef}
|
||||
type="button"
|
||||
className="image-canvas-editor__character-reference-add image-canvas-editor__reference-tile image-canvas-editor__reference-tile--upload"
|
||||
disabled={generateDialog.status === 'generating'}
|
||||
onClick={() => onRequestUpload('character-reference')}
|
||||
onClick={() =>
|
||||
setIsCharacterReferenceMenuOpen((open) => !open)
|
||||
}
|
||||
>
|
||||
<span className="image-canvas-editor__reference-tile-visual">
|
||||
<ImagePlus className="h-4 w-4" aria-hidden="true" />
|
||||
@@ -615,6 +777,38 @@ export function ImageCanvasGenerationComposerView({
|
||||
上传常规参考图
|
||||
</span>
|
||||
</button>
|
||||
{isCharacterReferenceMenuOpen
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
||||
label="常规参考图来源"
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
characterReferenceButtonRef.current,
|
||||
'above',
|
||||
)}
|
||||
>
|
||||
<PlatformFloatingMenuItem
|
||||
className="image-canvas-editor__context-menu-item"
|
||||
onClick={() => {
|
||||
setIsPickingCharacterReferenceFromCanvas(true);
|
||||
setIsCharacterReferenceMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
从画布中选择
|
||||
</PlatformFloatingMenuItem>
|
||||
<PlatformFloatingMenuItem
|
||||
className="image-canvas-editor__context-menu-item"
|
||||
onClick={() => {
|
||||
setIsCharacterReferenceMenuOpen(false);
|
||||
onRequestUpload('character-reference');
|
||||
}}
|
||||
>
|
||||
上传图片
|
||||
</PlatformFloatingMenuItem>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -657,36 +851,12 @@ export function ImageCanvasGenerationComposerView({
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
<div className="image-canvas-editor__generation-composer-footer">
|
||||
<div className="image-canvas-editor__option-field">
|
||||
<PlatformFieldLabel
|
||||
variant="field"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
画面比例
|
||||
</PlatformFieldLabel>
|
||||
<PlatformInlineOptionButton
|
||||
className="image-canvas-editor__generation-ratio"
|
||||
disabled={generateDialog.status === 'generating'}
|
||||
onClick={() => triggerPlaceholderAction('角色比例')}
|
||||
>
|
||||
1:1
|
||||
</PlatformInlineOptionButton>
|
||||
</div>
|
||||
<div className="image-canvas-editor__option-field">
|
||||
<PlatformFieldLabel
|
||||
variant="field"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
模型
|
||||
</PlatformFieldLabel>
|
||||
<PlatformInlineOptionButton
|
||||
className="image-canvas-editor__generation-model"
|
||||
disabled={generateDialog.status === 'generating'}
|
||||
onClick={() => triggerPlaceholderAction('角色模型')}
|
||||
>
|
||||
GPT Image
|
||||
</PlatformInlineOptionButton>
|
||||
</div>
|
||||
{renderImageOptionButtons({
|
||||
dialog: generateDialog,
|
||||
setGenerateDialog,
|
||||
includeDimensions: true,
|
||||
onRememberImageModel,
|
||||
})}
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
tone="secondary"
|
||||
@@ -764,10 +934,10 @@ export function ImageCanvasGenerationComposerView({
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
||||
label="图标素材规范来源"
|
||||
placement="bottom-start"
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
iconSpecButtonRef.current,
|
||||
'below',
|
||||
'above',
|
||||
)}
|
||||
>
|
||||
<PlatformFloatingMenuItem
|
||||
@@ -897,21 +1067,12 @@ export function ImageCanvasGenerationComposerView({
|
||||
>
|
||||
添加素材描述
|
||||
</button>
|
||||
<div className="image-canvas-editor__option-field">
|
||||
<PlatformFieldLabel
|
||||
variant="field"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
模型
|
||||
</PlatformFieldLabel>
|
||||
<PlatformInlineOptionButton
|
||||
className="image-canvas-editor__generation-model"
|
||||
disabled={generateDialog.status === 'generating'}
|
||||
onClick={() => triggerPlaceholderAction('图标模型')}
|
||||
>
|
||||
nanobanana2
|
||||
</PlatformInlineOptionButton>
|
||||
</div>
|
||||
{renderImageOptionButtons({
|
||||
dialog: generateDialog,
|
||||
setGenerateDialog,
|
||||
includeDimensions: true,
|
||||
onRememberImageModel,
|
||||
})}
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
tone="secondary"
|
||||
@@ -932,6 +1093,11 @@ export function ImageCanvasGenerationComposerView({
|
||||
请选择画布中的图片作为角色形象规范,按 Esc 退出
|
||||
</div>
|
||||
) : null}
|
||||
{isPickingCharacterReferenceFromCanvas ? (
|
||||
<div className="image-canvas-editor__canvas-pick-hint">
|
||||
请选择画布中的图片作为常规参考图,按 Esc 退出
|
||||
</div>
|
||||
) : null}
|
||||
{isPickingIconSpecFromCanvas ? (
|
||||
<div className="image-canvas-editor__canvas-pick-hint">
|
||||
请选择画布中的图标素材规范,按 Esc 退出
|
||||
|
||||
Reference in New Issue
Block a user