角色多方向图生成
This commit is contained in:
@@ -90,6 +90,12 @@ function CharacterGenerationHarness({
|
||||
<output aria-label="当前角色设定">{dialog.prompt}</output>
|
||||
<output aria-label="当前状态">{dialog.status}</output>
|
||||
<output aria-label="当前错误">{dialog.errorMessage ?? '-'}</output>
|
||||
<output aria-label="当前多方向角色图">
|
||||
{String(dialog.characterSheetEnabled === true)}
|
||||
</output>
|
||||
<output aria-label="当前多方向图视角数">
|
||||
{dialog.characterSheetViewSet ?? '4'}
|
||||
</output>
|
||||
<output aria-label="选择角色规范">{String(isPickingSpec)}</output>
|
||||
<output aria-label="选择常规参考图">{String(isPickingReference)}</output>
|
||||
</div>
|
||||
@@ -122,6 +128,64 @@ describe('ImageCanvasCharacterGenerationComposerView', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('toggles the multi-direction character sheet option', () => {
|
||||
render(
|
||||
<CharacterGenerationHarness
|
||||
initialDialog={createDialog({ characterSheetEnabled: false })}
|
||||
/>,
|
||||
);
|
||||
|
||||
const toggle = screen.getByRole('button', { name: '多方向角色图' });
|
||||
expect(toggle.getAttribute('aria-pressed')).toBe('false');
|
||||
|
||||
fireEvent.click(toggle);
|
||||
|
||||
expect(screen.getByLabelText('当前多方向角色图').textContent).toBe('true');
|
||||
expect(
|
||||
screen.getByRole('button', { name: '多方向角色图' }).getAttribute(
|
||||
'aria-pressed',
|
||||
),
|
||||
).toBe('true');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '多方向角色图' }));
|
||||
expect(screen.getByLabelText('当前多方向角色图').textContent).toBe('false');
|
||||
});
|
||||
|
||||
it('shows the view-set dropdown only while the sheet is enabled', () => {
|
||||
render(
|
||||
<CharacterGenerationHarness
|
||||
initialDialog={createDialog({ characterSheetEnabled: false })}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: /多方向图视角/ })).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '多方向角色图' }));
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: '多方向图视角 四方向' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('switches the character sheet view set between four and eight', () => {
|
||||
render(
|
||||
<CharacterGenerationHarness
|
||||
initialDialog={createDialog({
|
||||
characterSheetEnabled: true,
|
||||
characterSheetViewSet: '4',
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '多方向图视角 四方向' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '八方向' }));
|
||||
|
||||
expect(screen.getByLabelText('当前多方向图视角数').textContent).toBe('8');
|
||||
expect(
|
||||
screen.getByRole('button', { name: '多方向图视角 八方向' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('updates character prompt, clears failed state and submits', () => {
|
||||
const submitCharacter = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ClipboardList, ImagePlus } from 'lucide-react';
|
||||
import { ChevronDown, ClipboardList, ImagePlus, LayoutGrid } from 'lucide-react';
|
||||
import {
|
||||
type CSSProperties,
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { AutoGrowTextArea } from '../common/AutoGrowTextArea';
|
||||
@@ -12,6 +14,7 @@ import {
|
||||
PlatformFloatingMenu,
|
||||
PlatformFloatingMenuItem,
|
||||
} from '../common/PlatformFloatingMenu';
|
||||
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import type {
|
||||
CharacterReferenceImage,
|
||||
@@ -101,6 +104,8 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
hasPendingImageReferenceUploads = false,
|
||||
onSubmit,
|
||||
}: ImageCanvasCharacterGenerationComposerViewProps) {
|
||||
const [isViewSetMenuOpen, setIsViewSetMenuOpen] = useState(false);
|
||||
const viewSetButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
useImageCanvasFloatingOptionDismiss({
|
||||
isOpen: isCharacterSpecMenuOpen || isCharacterReferenceMenuOpen,
|
||||
boundaryRefs: [characterSpecButtonRef, characterReferenceButtonRef],
|
||||
@@ -109,6 +114,25 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
setIsCharacterReferenceMenuOpen(false);
|
||||
},
|
||||
});
|
||||
useImageCanvasFloatingOptionDismiss({
|
||||
isOpen: isViewSetMenuOpen,
|
||||
boundaryRefs: [viewSetButtonRef],
|
||||
onDismiss: () => setIsViewSetMenuOpen(false),
|
||||
restoreFocusRef: viewSetButtonRef,
|
||||
});
|
||||
const setViewSet = (value: '4' | '8') => {
|
||||
setIsViewSetMenuOpen(false);
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'character'
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
characterSheetViewSet: value,
|
||||
}
|
||||
: currentDialog,
|
||||
);
|
||||
};
|
||||
const selectedViewSetLabel =
|
||||
(dialog.characterSheetViewSet ?? '4') === '8' ? '八方向' : '四方向';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -216,6 +240,30 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
isAdd
|
||||
onClick={() => setIsCharacterReferenceMenuOpen((open) => !open)}
|
||||
/>
|
||||
<ImageCanvasReferenceSlot
|
||||
tone="character-sheet"
|
||||
icon={<LayoutGrid className="h-4 w-4" aria-hidden="true" />}
|
||||
label="多方向图"
|
||||
ariaLabel="多方向角色图"
|
||||
title={
|
||||
dialog.characterSheetEnabled === true
|
||||
? '多方向角色图(开启)'
|
||||
: '多方向角色图'
|
||||
}
|
||||
disabled={dialog.status === 'generating'}
|
||||
pressed={dialog.characterSheetEnabled === true}
|
||||
onClick={() =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'character'
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
characterSheetEnabled:
|
||||
currentDialog.characterSheetEnabled !== true,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
{isCharacterReferenceMenuOpen
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
@@ -292,8 +340,12 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
model: dialog.imageModel,
|
||||
imageSize: dialog.imageSize,
|
||||
})}
|
||||
submitLabel="生成"
|
||||
submitAriaLabel="生成"
|
||||
submitLabel={
|
||||
dialog.characterSheetEnabled === true ? '生成多方向图集' : '生成'
|
||||
}
|
||||
submitAriaLabel={
|
||||
dialog.characterSheetEnabled === true ? '生成多方向图集' : '生成'
|
||||
}
|
||||
onSubmitRequest={() => {
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
@@ -301,6 +353,62 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
}}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
styleControl={
|
||||
dialog.characterSheetEnabled === true ? (
|
||||
<div className="image-canvas-editor__option-popover-anchor">
|
||||
<PlatformInlineOptionButton
|
||||
ref={viewSetButtonRef}
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--viewset"
|
||||
aria-label={`多方向图视角 ${selectedViewSetLabel}`}
|
||||
aria-expanded={isViewSetMenuOpen}
|
||||
disabled={dialog.status === 'generating'}
|
||||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||||
onClick={() => setIsViewSetMenuOpen((open) => !open)}
|
||||
>
|
||||
{selectedViewSetLabel}
|
||||
</PlatformInlineOptionButton>
|
||||
{isViewSetMenuOpen
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__option-popover image-canvas-editor__portal-menu"
|
||||
label="多方向图视角数"
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
viewSetButtonRef.current,
|
||||
'above',
|
||||
)}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="image-canvas-editor__option-popover-sections">
|
||||
<div className="image-canvas-editor__option-popover-section">
|
||||
<span className="image-canvas-editor__option-popover-title">
|
||||
视角数
|
||||
</span>
|
||||
<div className="image-canvas-editor__option-popover-items">
|
||||
{(['4', '8'] as const).map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
className="image-canvas-editor__option-popover-choice"
|
||||
aria-pressed={
|
||||
(dialog.characterSheetViewSet ?? '4') ===
|
||||
value
|
||||
}
|
||||
onClick={() => setViewSet(value)}
|
||||
>
|
||||
{value === '4' ? '四方向' : '八方向'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,6 +82,7 @@ export type CanvasGenerationAction =
|
||||
| 'scene.generate'
|
||||
| 'spec.generate'
|
||||
| 'character.generate'
|
||||
| 'character-sheet.generate'
|
||||
| 'icon.generate'
|
||||
| 'ui-design.generate'
|
||||
| 'publication.generate'
|
||||
@@ -278,6 +279,11 @@ export type GenerateDialogState = {
|
||||
generationReferences?: CharacterReferenceImage[];
|
||||
characterSpecReference?: CharacterReferenceImage | null;
|
||||
characterReferences?: CharacterReferenceImage[];
|
||||
// 中文注释:角色面板的"多方向角色图"开关——开启时生成一张四方向角色图集
|
||||
// (后端 kind=character-sheet,产物按 icon-spritesheet 落库,可走既有拆分图集)。
|
||||
characterSheetEnabled?: boolean;
|
||||
// 中文注释:多方向角色图的视角数(4 或 8),仅 characterSheetEnabled 时生效。
|
||||
characterSheetViewSet?: '4' | '8';
|
||||
iconSpecReference?: CharacterReferenceImage | null;
|
||||
iconDescriptions?: string[];
|
||||
publicationWorkflowId?: PublicationMaterialsWorkflowId;
|
||||
|
||||
@@ -1228,6 +1228,41 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('restores a multi-direction character sheet recipe into the character composer', () => {
|
||||
expect(
|
||||
createSameSourceGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
id: 'layer-sheet-result',
|
||||
resourceId: 'resource-sheet-result',
|
||||
sourceType: 'generated',
|
||||
assetKind: 'icon-spritesheet',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'character-sheet.generate',
|
||||
fields: [
|
||||
{ id: 'prompt', title: '角色设定', value: '日系魔法少女' },
|
||||
{ id: 'model', title: '模型', value: 'nanobanana2' },
|
||||
{ id: 'viewSet', title: '视角集合', value: '八视图' },
|
||||
{ id: 'aspectRatio', title: '比例', value: '1:1' },
|
||||
{ id: 'imageSize', title: '尺寸', value: '1K' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
mode: 'redraw',
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'character',
|
||||
prompt: '日系魔法少女',
|
||||
sourceLayerId: 'layer-sheet-result',
|
||||
style: 'none',
|
||||
characterSheetEnabled: true,
|
||||
characterSheetViewSet: '8',
|
||||
});
|
||||
});
|
||||
|
||||
it('maps legacy ui spec.generate inputs to the unified icon spec composer', () => {
|
||||
expect(
|
||||
createSameSourceGenerationDialogDraft({
|
||||
|
||||
@@ -316,6 +316,8 @@ export function createCharacterGenerationDialogDraft({
|
||||
style: 'none',
|
||||
characterSpecReference: null,
|
||||
characterReferences: [],
|
||||
characterSheetEnabled: false,
|
||||
characterSheetViewSet: '4',
|
||||
imageModel: normalizedImageModel,
|
||||
aspectRatio: dimensionDefaults.aspectRatio,
|
||||
imageSize: dimensionDefaults.imageSize,
|
||||
@@ -1061,6 +1063,41 @@ function createNormalizedGenerationDialogDraft({
|
||||
);
|
||||
}
|
||||
|
||||
if (inputs.action === 'character-sheet.generate') {
|
||||
// 多方向角色图回弹到角色面板并恢复图集模式与视角数。
|
||||
return placeDraftBesideSourceLayer(
|
||||
resizeGenerationPlaceholderToImageSelection({
|
||||
...createCharacterGenerationDialogDraft({
|
||||
canvasSize,
|
||||
viewport,
|
||||
imageModel: getNormalizedString(
|
||||
sourceLayer,
|
||||
'model',
|
||||
DEFAULT_IMAGE_MODEL,
|
||||
),
|
||||
}),
|
||||
prompt,
|
||||
sourceLayerId: sourceLayer.id,
|
||||
style: 'none',
|
||||
aspectRatio: getNormalizedString(sourceLayer, 'aspectRatio', '1:1'),
|
||||
imageSize: getNormalizedString(sourceLayer, 'imageSize', '1K'),
|
||||
characterSheetEnabled: true,
|
||||
characterSheetViewSet:
|
||||
getNormalizedString(sourceLayer, 'viewSet') === '八视图'
|
||||
? '8'
|
||||
: '4',
|
||||
characterSpecReference:
|
||||
resolveNormalizedReferences(
|
||||
sourceLayer,
|
||||
'specReference',
|
||||
availableLayers,
|
||||
)[0] ?? null,
|
||||
characterReferences: references,
|
||||
}),
|
||||
sourceLayer,
|
||||
);
|
||||
}
|
||||
|
||||
if (inputs.action === 'icon.generate') {
|
||||
return placeDraftBesideSourceLayer(
|
||||
resizeGenerationPlaceholderToImageSelection({
|
||||
|
||||
@@ -17,6 +17,7 @@ export const CANVAS_GENERATION_ACTIONS = [
|
||||
'scene.generate',
|
||||
'spec.generate',
|
||||
'character.generate',
|
||||
'character-sheet.generate',
|
||||
'icon.generate',
|
||||
'ui-design.generate',
|
||||
'publication.generate',
|
||||
@@ -37,6 +38,7 @@ const REMIXABLE_CANVAS_GENERATION_ACTIONS = new Set<CanvasGenerationAction>([
|
||||
'scene.generate',
|
||||
'spec.generate',
|
||||
'character.generate',
|
||||
'character-sheet.generate',
|
||||
'icon.generate',
|
||||
'ui-design.generate',
|
||||
'publication.generate',
|
||||
|
||||
@@ -2174,6 +2174,58 @@ export function buildCharacterGenerationInputs(
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCharacterSheetGenerationInputs(
|
||||
prompt: string,
|
||||
specReference: CharacterReferenceImage | null | undefined,
|
||||
references: CharacterReferenceImage[] | undefined,
|
||||
options: {
|
||||
model?: string;
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
viewSet?: '4' | '8';
|
||||
} = {},
|
||||
): CanvasGenerationInputs {
|
||||
return {
|
||||
version: 2,
|
||||
action: 'character-sheet.generate',
|
||||
fields: [
|
||||
...createGenerationInputField('角色设定', prompt, 'prompt'),
|
||||
...createGenerationInputField(
|
||||
'模型',
|
||||
options.model ?? DEFAULT_IMAGE_MODEL,
|
||||
'model',
|
||||
),
|
||||
...createGenerationInputField(
|
||||
'视角集合',
|
||||
options.viewSet === '8' ? '八视图' : '四视图',
|
||||
'viewSet',
|
||||
),
|
||||
...createGenerationInputField(
|
||||
'比例',
|
||||
options.aspectRatio ?? '1:1',
|
||||
'aspectRatio',
|
||||
),
|
||||
...createGenerationInputField(
|
||||
'尺寸',
|
||||
options.imageSize ?? '1K',
|
||||
'imageSize',
|
||||
),
|
||||
],
|
||||
references: [
|
||||
...(specReference
|
||||
? createGenerationInputReference('角色规范', specReference, {
|
||||
id: 'specReference',
|
||||
})
|
||||
: []),
|
||||
...(references ?? []).flatMap((reference, index) =>
|
||||
createGenerationInputReference(`常规参考图 ${index + 1}`, reference, {
|
||||
id: 'reference',
|
||||
}),
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildUiDesignGenerationInputs(
|
||||
prompt: string,
|
||||
specReference: CharacterReferenceImage | null | undefined,
|
||||
|
||||
@@ -642,6 +642,87 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('builds character-sheet plans when multi-direction sheet is enabled', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'character',
|
||||
prompt: '日系魔法少女',
|
||||
status: 'idle',
|
||||
characterSheetEnabled: true,
|
||||
characterSpecReference: {
|
||||
id: 'spec',
|
||||
label: '角色规范',
|
||||
src: 'data:image/png;base64,spec',
|
||||
resourceId: 'resource-character-spec',
|
||||
},
|
||||
},
|
||||
layers: [],
|
||||
nextGeneratedIndex: 3,
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
kind: 'image',
|
||||
normalizedPrompt: '日系魔法少女',
|
||||
input: {
|
||||
prompt: '日系魔法少女',
|
||||
kind: 'character-sheet',
|
||||
assetKind: 'icon-spritesheet',
|
||||
style: 'none',
|
||||
},
|
||||
result: {
|
||||
assetKind: 'icon-spritesheet',
|
||||
title: '角色多方向图 3',
|
||||
},
|
||||
});
|
||||
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual(
|
||||
expect.objectContaining({
|
||||
version: 2,
|
||||
action: 'character-sheet.generate',
|
||||
fields: expect.arrayContaining([
|
||||
{ id: 'prompt', title: '角色设定', value: '日系魔法少女' },
|
||||
{ id: 'viewSet', title: '视角集合', value: '四视图' },
|
||||
]),
|
||||
}),
|
||||
);
|
||||
expect(plan.kind === 'image' ? plan.input : null).toMatchObject({
|
||||
characterSheetViews: 4,
|
||||
});
|
||||
// 图集不做像素化、不去背景,screenColor/segModel 不携带。
|
||||
expect(plan.kind === 'image' ? plan.input : null).not.toHaveProperty(
|
||||
'screenColor',
|
||||
);
|
||||
expect(plan.kind === 'image' ? plan.input : null).not.toHaveProperty(
|
||||
'segModel',
|
||||
);
|
||||
});
|
||||
|
||||
it('builds eight-direction character-sheet plans when view set is eight', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'character',
|
||||
prompt: '日系魔法少女',
|
||||
status: 'idle',
|
||||
characterSheetEnabled: true,
|
||||
characterSheetViewSet: '8',
|
||||
},
|
||||
layers: [],
|
||||
nextGeneratedIndex: 4,
|
||||
});
|
||||
|
||||
expect(plan.kind === 'image' ? plan.input : null).toMatchObject({
|
||||
kind: 'character-sheet',
|
||||
characterSheetViews: 8,
|
||||
});
|
||||
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual(
|
||||
expect.objectContaining({
|
||||
action: 'character-sheet.generate',
|
||||
fields: expect.arrayContaining([
|
||||
{ id: 'viewSet', title: '视角集合', value: '八视图' },
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('locks UI design generation submissions to gpt-image-2', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
import {
|
||||
buildBackgroundMusicGenerationInputs,
|
||||
buildCharacterGenerationInputs,
|
||||
buildCharacterSheetGenerationInputs,
|
||||
buildEditGenerationInputs,
|
||||
buildIconGenerationInputs,
|
||||
buildImageGenerationInputs,
|
||||
@@ -800,37 +801,64 @@ export function buildImageGenerationSubmissionPlan({
|
||||
const imageModel = normalizeEditorImageModel(dialog.imageModel);
|
||||
const screenColor = DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR;
|
||||
const segModel = DEFAULT_EDITOR_BGFILTER_SEG_MODEL;
|
||||
const sheetEnabled = dialog.characterSheetEnabled === true;
|
||||
const sheetViewSet = dialog.characterSheetViewSet ?? '4';
|
||||
// 中文注释:多方向角色图走后端 kind=character-sheet(绿色间隔底,便于连通块拆分),
|
||||
// 产物按 icon-spritesheet 落库,复用既有「拆分图集」能力;不做像素化、不去背景。
|
||||
return {
|
||||
kind: 'image',
|
||||
normalizedPrompt,
|
||||
input: {
|
||||
prompt: normalizedPrompt,
|
||||
kind: 'character',
|
||||
kind: sheetEnabled ? 'character-sheet' : 'character',
|
||||
model: imageModel,
|
||||
screenColor,
|
||||
segModel,
|
||||
style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none',
|
||||
...(sheetEnabled ? {} : { screenColor, segModel }),
|
||||
style: sheetEnabled
|
||||
? 'none'
|
||||
: dialog.style === 'pixelArt'
|
||||
? 'pixelArt'
|
||||
: 'none',
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
...(sheetEnabled
|
||||
? {
|
||||
assetKind: 'icon-spritesheet',
|
||||
characterSheetViews: sheetViewSet === '8' ? 8 : 4,
|
||||
}
|
||||
: {}),
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
},
|
||||
result: {
|
||||
assetKind: 'character',
|
||||
assetKind: sheetEnabled ? 'icon-spritesheet' : 'character',
|
||||
title: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`角色形象 ${nextGeneratedIndex}`,
|
||||
),
|
||||
generationInputs: buildCharacterGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.characterSpecReference,
|
||||
dialog.characterReferences,
|
||||
{
|
||||
model: imageModel,
|
||||
style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none',
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
},
|
||||
sheetEnabled
|
||||
? `角色多方向图 ${nextGeneratedIndex}`
|
||||
: `角色形象 ${nextGeneratedIndex}`,
|
||||
),
|
||||
generationInputs: sheetEnabled
|
||||
? buildCharacterSheetGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.characterSpecReference,
|
||||
dialog.characterReferences,
|
||||
{
|
||||
model: imageModel,
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
viewSet: sheetViewSet,
|
||||
},
|
||||
)
|
||||
: buildCharacterGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.characterSpecReference,
|
||||
dialog.characterReferences,
|
||||
{
|
||||
model: imageModel,
|
||||
style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none',
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
},
|
||||
),
|
||||
},
|
||||
rememberImageModel: imageModel,
|
||||
};
|
||||
|
||||
@@ -11,7 +11,8 @@ type ImageCanvasReferenceSlotTone =
|
||||
| 'character'
|
||||
| 'quick-edit'
|
||||
| 'video'
|
||||
| 'audio';
|
||||
| 'audio'
|
||||
| 'character-sheet';
|
||||
|
||||
type ImageCanvasReferenceSlotProps = {
|
||||
buttonRef?: RefObject<HTMLButtonElement | null>;
|
||||
@@ -25,6 +26,8 @@ type ImageCanvasReferenceSlotProps = {
|
||||
objectKey?: string | null;
|
||||
disabled?: boolean;
|
||||
isAdd?: boolean;
|
||||
/** 选中态(用于模式开关类入口):置位时高亮边框并带 aria-pressed。 */
|
||||
pressed?: boolean;
|
||||
onClick?: () => void;
|
||||
onRemove?: () => void;
|
||||
removeLabel?: string;
|
||||
@@ -42,6 +45,7 @@ export function ImageCanvasReferenceSlot({
|
||||
objectKey,
|
||||
disabled = false,
|
||||
isAdd = false,
|
||||
pressed,
|
||||
onClick,
|
||||
onRemove,
|
||||
removeLabel,
|
||||
@@ -54,11 +58,11 @@ export function ImageCanvasReferenceSlot({
|
||||
? 'image-canvas-editor__reference-chip--has-image'
|
||||
: 'image-canvas-editor__reference-chip--icon-only',
|
||||
isAdd ? 'image-canvas-editor__reference-chip--upload' : null,
|
||||
pressed ? 'image-canvas-editor__reference-chip--active' : null,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const slotContent = (
|
||||
.join(' '); const slotContent = (
|
||||
<>
|
||||
<span className="image-canvas-editor__reference-chip-icon">
|
||||
{hasPreviewImage ? (
|
||||
@@ -89,6 +93,7 @@ export function ImageCanvasReferenceSlot({
|
||||
className={slotClassName}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel}
|
||||
aria-pressed={pressed === undefined ? undefined : pressed}
|
||||
title={title ?? ariaLabel}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
||||
@@ -7226,6 +7226,12 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-cluster--viewset {
|
||||
width: auto;
|
||||
max-width: 6.5rem;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.image-canvas-editor__option-popover-anchor {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
@@ -7719,6 +7725,22 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.image-canvas-editor__reference-chip--character-sheet
|
||||
.image-canvas-editor__reference-chip-icon {
|
||||
color: #db2777;
|
||||
}
|
||||
|
||||
.image-canvas-editor__reference-chip--active {
|
||||
border-color: #a5b4fc;
|
||||
background: #eef2ff;
|
||||
color: #3730a3;
|
||||
}
|
||||
|
||||
button.image-canvas-editor__reference-chip--active:hover:not(:disabled) {
|
||||
border-color: #a5b4fc;
|
||||
background: #e0e7ff;
|
||||
}
|
||||
|
||||
.image-canvas-editor__publication-composer {
|
||||
grid-template-rows: auto auto auto;
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ export type EditorAssetGenerationInputs = {
|
||||
| 'scene.generate'
|
||||
| 'spec.generate'
|
||||
| 'character.generate'
|
||||
| 'character-sheet.generate'
|
||||
| 'icon.generate'
|
||||
| 'ui-design.generate'
|
||||
| 'publication.generate'
|
||||
@@ -329,13 +330,19 @@ export type EditorImageGenerationInput = {
|
||||
prompt: string;
|
||||
size?: string;
|
||||
kind?:
|
||||
'spec' | 'character' | 'quick-edit' | 'ui-design' | 'publication-material';
|
||||
| 'spec'
|
||||
| 'character'
|
||||
| 'character-sheet'
|
||||
| 'quick-edit'
|
||||
| 'ui-design'
|
||||
| 'publication-material';
|
||||
model?: string;
|
||||
screenColor?: string;
|
||||
segModel?: string;
|
||||
style?: EditorImageGenerationStyle;
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
characterSheetViews?: 4 | 8;
|
||||
referenceImageSrcs?: string[];
|
||||
projectId?: string | null;
|
||||
assetKind?: string | null;
|
||||
@@ -1194,6 +1201,9 @@ export async function generateEditorImage(input: EditorImageGenerationInput) {
|
||||
...(input.style ? { style: input.style } : {}),
|
||||
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
|
||||
...(input.imageSize ? { imageSize: input.imageSize } : {}),
|
||||
...(input.characterSheetViews
|
||||
? { characterSheetViews: input.characterSheetViews }
|
||||
: {}),
|
||||
...(input.referenceImageSrcs?.length
|
||||
? { referenceImageSrcs: input.referenceImageSrcs }
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user