合并最新主分支并保留策划退役清理
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m30s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m44s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m37s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m6s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m9s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m26s
Project CI / Native shell tests (pull_request) Successful in 19m40s
Project CI / Frontend tests (pull_request) Successful in 21m8s
Project CI / Repository checks (pull_request) Successful in 21m29s
Project CI / Backend tests (pull_request) Successful in 27m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 19m50s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m30s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m44s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m37s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m6s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m9s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m26s
Project CI / Native shell tests (pull_request) Successful in 19m40s
Project CI / Frontend tests (pull_request) Successful in 21m8s
Project CI / Repository checks (pull_request) Successful in 21m29s
Project CI / Backend tests (pull_request) Successful in 27m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 19m50s
合并 origin/master 的最新功能与测试变更 保留已删除的策划 Agent V1/V2 运行时和旧 GDD 入口 仅解决资源预览类型冲突,不恢复退役策划调用
This commit is contained in:
@@ -155,6 +155,26 @@ function SuspendAfterLandingRender({ active }: { active: boolean }) {
|
||||
describe('CreationLandingView', () => {
|
||||
beforeEach(() => {
|
||||
clearSignedAssetReadUrlCache();
|
||||
// 每次换签返回独立响应,筛选等用例也不能触发真实网络请求。
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = new URL(String(input), 'http://localhost');
|
||||
if (url.pathname !== '/api/assets/read-url') {
|
||||
throw new Error(`Unexpected test request: ${url.pathname}`);
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
read: {
|
||||
objectKey: url.searchParams.get('objectKey') ?? '',
|
||||
signedUrl: 'data:image/png;base64,dGVzdA==',
|
||||
expiresAt: '2099-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { 'content-type': 'application/json' } },
|
||||
);
|
||||
}),
|
||||
);
|
||||
listPublicEditorProjectResourcesMock.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
@@ -1304,17 +1324,18 @@ describe('CreationLandingView', () => {
|
||||
const campaignObjectKey =
|
||||
'generated-character-drafts/editor/showcase-campaign/current/campaign.png';
|
||||
const signedCampaignUrl = 'data:image/png;base64,Y2FtcGFpZ24=';
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
read: {
|
||||
objectKey: campaignObjectKey,
|
||||
signedUrl: signedCampaignUrl,
|
||||
expiresAt: '2099-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { 'content-type': 'application/json' } },
|
||||
),
|
||||
const fetchMock = vi.fn().mockImplementation(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
read: {
|
||||
objectKey: campaignObjectKey,
|
||||
signedUrl: signedCampaignUrl,
|
||||
expiresAt: '2099-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { 'content-type': 'application/json' } },
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
listEditorProjectsMock.mockResolvedValue(projectItems);
|
||||
|
||||
@@ -2749,6 +2749,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect.objectContaining({
|
||||
referenceId: 'resource-icon-spec',
|
||||
iconDescriptions: ['返回按钮\n设置按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
@@ -3874,9 +3875,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
if (!metadataCornerButton) {
|
||||
throw new Error('metadata corner button should exist');
|
||||
}
|
||||
expect(metadataCornerButton.className).toContain(
|
||||
'image-canvas-editor__metadata-corner',
|
||||
);
|
||||
expect(metadataCornerButton.className).toContain('shared-canvas-card-info');
|
||||
fireEvent.click(metadataCornerButton);
|
||||
|
||||
const metadataDialog = screen.getByRole('dialog', { name: '图片信息' });
|
||||
|
||||
@@ -1387,9 +1387,7 @@ describe('ImageCanvasEditorView', () => {
|
||||
const infoButton = screen.getByRole('button', {
|
||||
name: '查看拼图素材图片信息',
|
||||
});
|
||||
expect(infoButton.className).toContain(
|
||||
'image-canvas-editor__metadata-corner',
|
||||
);
|
||||
expect(infoButton.className).toContain('shared-canvas-card-info');
|
||||
fireEvent.click(infoButton);
|
||||
|
||||
const infoPanel = screen.getByRole('dialog', { name: '图片信息' });
|
||||
|
||||
@@ -1080,6 +1080,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
referenceId: 'resource-icon-spec',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
iconDescriptions: ['返回按钮\n\n设置按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: 'auto',
|
||||
segModel: 'birefnet',
|
||||
|
||||
@@ -1136,6 +1136,9 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
}
|
||||
: {}),
|
||||
iconDescriptions,
|
||||
// 切分模式没有默认值:画板链路按自由排布生成图标表,因此显式声明连通域切分;
|
||||
// 等分网格或固定槽位需求必须由调用方显式传 grid + gridX/gridY。
|
||||
sliceMode: 'connected-components',
|
||||
model: rememberImageModel,
|
||||
screenColor,
|
||||
segModel,
|
||||
|
||||
@@ -70,6 +70,50 @@ function renderSelectedToolbar(
|
||||
}
|
||||
|
||||
describe('ImageCanvasSelectedLayerToolbarView', () => {
|
||||
it('AGC 收纳包含宿主末组,换选后关闭旧资源更多菜单', () => {
|
||||
const props = renderSelectedToolbar({
|
||||
maxVisibleActions: 5,
|
||||
supportedActions: new Set([
|
||||
'quick-edit',
|
||||
'character-animation',
|
||||
'download',
|
||||
]),
|
||||
downloadLabel: '导出',
|
||||
extraActions: (
|
||||
<>
|
||||
<button>引用</button>
|
||||
<button>编辑标签</button>
|
||||
<button>重命名</button>
|
||||
</>
|
||||
),
|
||||
endActions: <button>删除素材</button>,
|
||||
});
|
||||
const toolbar = screen.getByRole('toolbar');
|
||||
expect(
|
||||
within(toolbar)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.textContent),
|
||||
).toEqual(['快速编辑', '生成动画', '引用', '编辑标签', '重命名', '更多 ▴']);
|
||||
fireEvent.mouseEnter(screen.getByRole('button', { name: '更多' }));
|
||||
const group = screen.getByRole('group', { name: '更多操作' });
|
||||
expect(
|
||||
within(group)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.textContent),
|
||||
).toEqual(['导出', '删除素材']);
|
||||
fireEvent.click(within(group).getByRole('button', { name: '导出' }));
|
||||
expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer);
|
||||
cleanup();
|
||||
const view = render(<ImageCanvasSelectedLayerToolbarView {...props} />);
|
||||
fireEvent.mouseEnter(screen.getByRole('button', { name: '更多' }));
|
||||
view.rerender(
|
||||
<ImageCanvasSelectedLayerToolbarView
|
||||
{...props}
|
||||
selectedLayer={createLayer({ id: 'other' })}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByRole('group', { name: '更多操作' })).toBeNull();
|
||||
});
|
||||
it('常规动作与末组分别分隔,导出紧邻删除且位于删除之前', () => {
|
||||
const props = renderSelectedToolbar({
|
||||
supportedActions: new Set(['quick-edit', 'download']),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CanvasChromeButton } from '@genarrative/image-canvas-react';
|
||||
import { OverflowActions } from '@genarrative/shared/components';
|
||||
import {
|
||||
Crop,
|
||||
Download,
|
||||
@@ -38,6 +39,7 @@ export type ImageCanvasSelectedToolbarAction =
|
||||
| 'download';
|
||||
|
||||
type ImageCanvasSelectedLayerToolbarViewProps = {
|
||||
maxVisibleActions?: number;
|
||||
/**
|
||||
* 宿主显式声明的可用动作集合。
|
||||
*
|
||||
@@ -78,6 +80,7 @@ function hasToolbarActions(actions: ReactNode): boolean {
|
||||
}
|
||||
|
||||
export function ImageCanvasSelectedLayerToolbarView({
|
||||
maxVisibleActions,
|
||||
supportedActions = null,
|
||||
extraActions,
|
||||
endActions,
|
||||
@@ -148,22 +151,24 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
aria-label="素材工具栏"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
{canRedraw ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="改造"
|
||||
title="改造"
|
||||
icon={<WandSparkles className="h-4 w-4" />}
|
||||
onClick={() => onOpenRedrawPanel(selectedLayer)}
|
||||
>
|
||||
<span>改造</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{canRedraw && hasExtraActions ? divider : null}
|
||||
{extraActions}
|
||||
{(canRedraw || hasExtraActions) && hasEndActions ? divider : null}
|
||||
{downloadAction}
|
||||
{endActions}
|
||||
<OverflowActions key={selectedLayer.id} maxVisible={maxVisibleActions}>
|
||||
{canRedraw ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="改造"
|
||||
title="改造"
|
||||
icon={<WandSparkles className="h-4 w-4" />}
|
||||
onClick={() => onOpenRedrawPanel(selectedLayer)}
|
||||
>
|
||||
<span>改造</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{canRedraw && hasExtraActions ? divider : null}
|
||||
{extraActions}
|
||||
{(canRedraw || hasExtraActions) && hasEndActions ? divider : null}
|
||||
{downloadAction}
|
||||
{endActions}
|
||||
</OverflowActions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -208,170 +213,172 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
aria-label="图片工具栏"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
{showQuickEdit ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="快速编辑"
|
||||
title="快速编辑"
|
||||
icon={<Sparkles className="h-4 w-4" />}
|
||||
onClick={() => onOpenQuickEditPanel(selectedLayer)}
|
||||
>
|
||||
<span>快速编辑</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{showQuickEdit && hasEditingActions ? divider : null}
|
||||
{showCropExpand ? (
|
||||
<EditorIconButton
|
||||
label="裁扩按钮"
|
||||
title="裁扩按钮"
|
||||
icon={Crop}
|
||||
onClick={() => onOpenCropExpandPanel(selectedLayer)}
|
||||
/>
|
||||
) : null}
|
||||
{showRemoveBackground ? (
|
||||
<EditorIconButton
|
||||
label="去除背景按钮"
|
||||
title="去除背景按钮"
|
||||
icon={ImageOff}
|
||||
onClick={() => onRemoveBackground(selectedLayer)}
|
||||
/>
|
||||
) : null}
|
||||
{showPerfectPixel ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
// 中文注释:保存态名称不能直接用「素材类型保存中」——icon-spritesheet 图层上
|
||||
// 拆分图集按钮同时显示该文案,两个控件会撞同一个无障碍名称。
|
||||
label={
|
||||
isPersistingAssetKind
|
||||
? '完美像素等待素材类型保存'
|
||||
: isPerfectPixelProcessing
|
||||
? '完美像素处理中'
|
||||
: isPerfectPixelPendingConfirmation
|
||||
? '完美像素结果待确认'
|
||||
: '完美像素'
|
||||
}
|
||||
title={
|
||||
isPersistingAssetKind
|
||||
? '完美像素等待素材类型保存'
|
||||
: isPerfectPixelProcessing
|
||||
? '完美像素处理中'
|
||||
: isPerfectPixelPendingConfirmation
|
||||
? '结果待确认,双击原占位继续核对或重试'
|
||||
: '自动识别并规整像素网格'
|
||||
}
|
||||
icon={
|
||||
isPersistingAssetKind || isPerfectPixelProcessing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Grid2X2 className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
// 中文注释:素材类型保存在途时必须一并禁用。请求同时带 assetKind 和
|
||||
// sourceResourceId,本地类型已改但资源尚未落库时两者不一致,后端
|
||||
// resolve_editor_pixel_art_snap_asset_kind 会直接 400,只留下失败占位。
|
||||
// 与相邻的拆分图集按钮保持同一套门禁。
|
||||
disabled={
|
||||
isPersistingAssetKind ||
|
||||
isPerfectPixelProcessing ||
|
||||
isPerfectPixelPendingConfirmation
|
||||
}
|
||||
aria-busy={isPersistingAssetKind || isPerfectPixelProcessing}
|
||||
onClick={() => onPerfectPixel(selectedLayer)}
|
||||
>
|
||||
<span>
|
||||
{isPersistingAssetKind
|
||||
? '保存中'
|
||||
: isPerfectPixelProcessing
|
||||
? '处理中'
|
||||
: isPerfectPixelPendingConfirmation
|
||||
? '待确认'
|
||||
: '完美像素'}
|
||||
</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{showSplitIconSpritesheet ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label={
|
||||
isPersistingAssetKind
|
||||
? '素材类型保存中'
|
||||
: isSplittingIconSpritesheet
|
||||
? '拆图中'
|
||||
: '拆分图集'
|
||||
}
|
||||
title={
|
||||
isPersistingAssetKind
|
||||
? '素材类型保存中'
|
||||
: isSplittingIconSpritesheet
|
||||
? '拆图中'
|
||||
: '拆分图集'
|
||||
}
|
||||
icon={
|
||||
isPersistingAssetKind || isSplittingIconSpritesheet ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Scissors className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
disabled={isPersistingAssetKind || isSplittingIconSpritesheet}
|
||||
aria-busy={isPersistingAssetKind || isSplittingIconSpritesheet}
|
||||
onClick={() => onSplitIconSpritesheet(selectedLayer)}
|
||||
>
|
||||
<span>
|
||||
{isPersistingAssetKind
|
||||
? '保存中'
|
||||
: isSplittingIconSpritesheet
|
||||
? '拆图中'
|
||||
: '拆分图集'}
|
||||
</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{showExtractUiDesign ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="提取素材"
|
||||
title="提取素材"
|
||||
icon={<Scissors className="h-4 w-4" />}
|
||||
onClick={() => onExtractUiDesignAssets(selectedLayer)}
|
||||
>
|
||||
<span>提取素材</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{showCharacterAnimation ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="生成动画"
|
||||
title="生成动画"
|
||||
icon={<PersonStanding className="h-4 w-4" />}
|
||||
onClick={() => onOpenCharacterAnimationPanel(selectedLayer)}
|
||||
>
|
||||
<span>生成动画</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{canRedraw ? (
|
||||
<>
|
||||
{showQuickEdit || hasEditingActions ? divider : null}
|
||||
<OverflowActions key={selectedLayer.id} maxVisible={maxVisibleActions}>
|
||||
{showQuickEdit ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="改造"
|
||||
title="改造"
|
||||
icon={<WandSparkles className="h-4 w-4" />}
|
||||
onClick={() => onOpenRedrawPanel(selectedLayer)}
|
||||
label="快速编辑"
|
||||
title="快速编辑"
|
||||
icon={<Sparkles className="h-4 w-4" />}
|
||||
onClick={() => onOpenQuickEditPanel(selectedLayer)}
|
||||
>
|
||||
<span>改造</span>
|
||||
<span>快速编辑</span>
|
||||
</CanvasChromeButton>
|
||||
</>
|
||||
) : null}
|
||||
{(showQuickEdit || hasEditingActions || canRedraw) && hasExtraActions
|
||||
? divider
|
||||
: null}
|
||||
{extraActions}
|
||||
{(showQuickEdit || hasEditingActions || canRedraw || hasExtraActions) &&
|
||||
hasEndActions
|
||||
? divider
|
||||
: null}
|
||||
{downloadAction}
|
||||
{endActions}
|
||||
) : null}
|
||||
{showQuickEdit && hasEditingActions ? divider : null}
|
||||
{showCropExpand ? (
|
||||
<EditorIconButton
|
||||
label="裁扩按钮"
|
||||
title="裁扩按钮"
|
||||
icon={Crop}
|
||||
onClick={() => onOpenCropExpandPanel(selectedLayer)}
|
||||
/>
|
||||
) : null}
|
||||
{showRemoveBackground ? (
|
||||
<EditorIconButton
|
||||
label="去除背景按钮"
|
||||
title="去除背景按钮"
|
||||
icon={ImageOff}
|
||||
onClick={() => onRemoveBackground(selectedLayer)}
|
||||
/>
|
||||
) : null}
|
||||
{showPerfectPixel ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
// 中文注释:保存态名称不能直接用「素材类型保存中」——icon-spritesheet 图层上
|
||||
// 拆分图集按钮同时显示该文案,两个控件会撞同一个无障碍名称。
|
||||
label={
|
||||
isPersistingAssetKind
|
||||
? '完美像素等待素材类型保存'
|
||||
: isPerfectPixelProcessing
|
||||
? '完美像素处理中'
|
||||
: isPerfectPixelPendingConfirmation
|
||||
? '完美像素结果待确认'
|
||||
: '完美像素'
|
||||
}
|
||||
title={
|
||||
isPersistingAssetKind
|
||||
? '完美像素等待素材类型保存'
|
||||
: isPerfectPixelProcessing
|
||||
? '完美像素处理中'
|
||||
: isPerfectPixelPendingConfirmation
|
||||
? '结果待确认,双击原占位继续核对或重试'
|
||||
: '自动识别并规整像素网格'
|
||||
}
|
||||
icon={
|
||||
isPersistingAssetKind || isPerfectPixelProcessing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Grid2X2 className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
// 中文注释:素材类型保存在途时必须一并禁用。请求同时带 assetKind 和
|
||||
// sourceResourceId,本地类型已改但资源尚未落库时两者不一致,后端
|
||||
// resolve_editor_pixel_art_snap_asset_kind 会直接 400,只留下失败占位。
|
||||
// 与相邻的拆分图集按钮保持同一套门禁。
|
||||
disabled={
|
||||
isPersistingAssetKind ||
|
||||
isPerfectPixelProcessing ||
|
||||
isPerfectPixelPendingConfirmation
|
||||
}
|
||||
aria-busy={isPersistingAssetKind || isPerfectPixelProcessing}
|
||||
onClick={() => onPerfectPixel(selectedLayer)}
|
||||
>
|
||||
<span>
|
||||
{isPersistingAssetKind
|
||||
? '保存中'
|
||||
: isPerfectPixelProcessing
|
||||
? '处理中'
|
||||
: isPerfectPixelPendingConfirmation
|
||||
? '待确认'
|
||||
: '完美像素'}
|
||||
</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{showSplitIconSpritesheet ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label={
|
||||
isPersistingAssetKind
|
||||
? '素材类型保存中'
|
||||
: isSplittingIconSpritesheet
|
||||
? '拆图中'
|
||||
: '拆分图集'
|
||||
}
|
||||
title={
|
||||
isPersistingAssetKind
|
||||
? '素材类型保存中'
|
||||
: isSplittingIconSpritesheet
|
||||
? '拆图中'
|
||||
: '拆分图集'
|
||||
}
|
||||
icon={
|
||||
isPersistingAssetKind || isSplittingIconSpritesheet ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Scissors className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
disabled={isPersistingAssetKind || isSplittingIconSpritesheet}
|
||||
aria-busy={isPersistingAssetKind || isSplittingIconSpritesheet}
|
||||
onClick={() => onSplitIconSpritesheet(selectedLayer)}
|
||||
>
|
||||
<span>
|
||||
{isPersistingAssetKind
|
||||
? '保存中'
|
||||
: isSplittingIconSpritesheet
|
||||
? '拆图中'
|
||||
: '拆分图集'}
|
||||
</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{showExtractUiDesign ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="提取素材"
|
||||
title="提取素材"
|
||||
icon={<Scissors className="h-4 w-4" />}
|
||||
onClick={() => onExtractUiDesignAssets(selectedLayer)}
|
||||
>
|
||||
<span>提取素材</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{showCharacterAnimation ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="生成动画"
|
||||
title="生成动画"
|
||||
icon={<PersonStanding className="h-4 w-4" />}
|
||||
onClick={() => onOpenCharacterAnimationPanel(selectedLayer)}
|
||||
>
|
||||
<span>生成动画</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{canRedraw ? (
|
||||
<>
|
||||
{showQuickEdit || hasEditingActions ? divider : null}
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="改造"
|
||||
title="改造"
|
||||
icon={<WandSparkles className="h-4 w-4" />}
|
||||
onClick={() => onOpenRedrawPanel(selectedLayer)}
|
||||
>
|
||||
<span>改造</span>
|
||||
</CanvasChromeButton>
|
||||
</>
|
||||
) : null}
|
||||
{(showQuickEdit || hasEditingActions || canRedraw) && hasExtraActions
|
||||
? divider
|
||||
: null}
|
||||
{extraActions}
|
||||
{(showQuickEdit || hasEditingActions || canRedraw || hasExtraActions) &&
|
||||
hasEndActions
|
||||
? divider
|
||||
: null}
|
||||
{downloadAction}
|
||||
{endActions}
|
||||
</OverflowActions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -814,7 +814,7 @@ describe('ImageCanvasWorldView', () => {
|
||||
|
||||
expect(
|
||||
(
|
||||
within(layerButton).getByText('角色') as HTMLElement
|
||||
within(layerButton).getByText('角色').parentElement as HTMLElement
|
||||
).style.getPropertyValue('--image-canvas-editor-inverse-scale'),
|
||||
).toBe(inverseScale);
|
||||
expect(
|
||||
@@ -825,7 +825,9 @@ describe('ImageCanvasWorldView', () => {
|
||||
expect(
|
||||
within(layerButton)
|
||||
.getByRole('button', { name: '查看角色主图图片信息' })
|
||||
.style.getPropertyValue('--image-canvas-editor-inverse-scale'),
|
||||
.parentElement!.style.getPropertyValue(
|
||||
'--image-canvas-editor-inverse-scale',
|
||||
),
|
||||
).toBe(inverseScale);
|
||||
expect(
|
||||
(
|
||||
@@ -1118,12 +1120,8 @@ describe('ImageCanvasWorldView', () => {
|
||||
name: '查看角色主图图片信息',
|
||||
});
|
||||
|
||||
expect(badge.className).toContain(
|
||||
'image-canvas-editor__kind-badge--beside-info',
|
||||
);
|
||||
expect(metadataButton.className).not.toContain(
|
||||
'image-canvas-editor__metadata-corner--beside-kind',
|
||||
);
|
||||
expect(badge.parentElement?.className).toBe('shared-canvas-card-corners');
|
||||
expect(badge.nextElementSibling).toBe(metadataButton);
|
||||
});
|
||||
|
||||
it('keeps the layer type menu scrollable and closes it when clicking outside', () => {
|
||||
|
||||
@@ -3,13 +3,13 @@ import {
|
||||
LayerRenderer,
|
||||
SelectionOverlay,
|
||||
} from '@genarrative/image-canvas-react';
|
||||
import { CanvasCardCornerActions } from '@genarrative/shared/components';
|
||||
import {
|
||||
AppWindow,
|
||||
Clapperboard,
|
||||
ClipboardList,
|
||||
Grid2X2,
|
||||
ImageIcon,
|
||||
Info,
|
||||
Megaphone,
|
||||
Mountain,
|
||||
Music,
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
PlatformFloatingMenu,
|
||||
PlatformFloatingMenuItem,
|
||||
} from '../common/PlatformFloatingMenu';
|
||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import {
|
||||
@@ -367,17 +366,6 @@ function handleLayerKeyboardActivation(event: ReactKeyboardEvent<HTMLElement>) {
|
||||
event.currentTarget.click();
|
||||
}
|
||||
|
||||
function handleLayerTagKeyboardActivation(
|
||||
event: ReactKeyboardEvent<HTMLElement>,
|
||||
) {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.click();
|
||||
}
|
||||
|
||||
function stopMediaControlKeyPropagation(
|
||||
event: ReactKeyboardEvent<HTMLElement>,
|
||||
) {
|
||||
@@ -1012,38 +1000,19 @@ const MemoizedCanvasLayerNode = memo(function CanvasLayerNode({
|
||||
mediaTransform={mediaTransform}
|
||||
/>
|
||||
)}
|
||||
{kindLabel ? (
|
||||
<span
|
||||
ref={isKindMenuOpen ? openLayerKindTriggerRef : undefined}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`素材标签:${kindLabel}`}
|
||||
className={`image-canvas-editor__kind-badge image-canvas-editor__kind-badge--${kindTagMeta.className} image-canvas-editor__kind-badge--beside-info image-canvas-editor__kind-badge--editable`}
|
||||
style={inverseScaleStyle}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handlers.setOpenLayerKindMenuId((currentId) =>
|
||||
currentId === layer.id ? null : layer.id,
|
||||
);
|
||||
}}
|
||||
onKeyDown={handleLayerTagKeyboardActivation}
|
||||
>
|
||||
{kindLabel}
|
||||
</span>
|
||||
) : null}
|
||||
<PlatformIconButton
|
||||
asChild="spanButton"
|
||||
variant="darkMini"
|
||||
className="image-canvas-editor__metadata-corner"
|
||||
label={`查看${layer.title}${metadataMediaLabel}信息`}
|
||||
icon={<Info className="h-3 w-3" />}
|
||||
<CanvasCardCornerActions
|
||||
kindLabel={kindLabel}
|
||||
kindAriaLabel={`素材标签:${kindLabel}`}
|
||||
kindRef={isKindMenuOpen ? openLayerKindTriggerRef : undefined}
|
||||
kindClassName={`image-canvas-editor__kind-badge--${kindTagMeta.className}`}
|
||||
infoLabel={`查看${layer.title}${metadataMediaLabel}信息`}
|
||||
style={inverseScaleStyle}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handlers.onOpenLayerMetadata(layer);
|
||||
}}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onKindClick={() =>
|
||||
handlers.setOpenLayerKindMenuId((currentId) =>
|
||||
currentId === layer.id ? null : layer.id,
|
||||
)
|
||||
}
|
||||
onInfoClick={() => handlers.onOpenLayerMetadata(layer)}
|
||||
/>
|
||||
{isHovered && layer.mediaType !== 'audio' ? (
|
||||
<PlatformPillBadge
|
||||
@@ -1344,7 +1313,7 @@ export function ImageCanvasWorldView({
|
||||
{canvasGenerationDialogs.map((dialog) => {
|
||||
const hasGeneratedLayer = Boolean(
|
||||
dialog.generatedLayerId &&
|
||||
layers.some((layer) => layer.id === dialog.generatedLayerId),
|
||||
layers.some((layer) => layer.id === dialog.generatedLayerId),
|
||||
);
|
||||
return dialog.placeholder && !hasGeneratedLayer
|
||||
? (() => {
|
||||
|
||||
@@ -4249,6 +4249,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
'generated-character-drafts/editor/refs/icon-style.png',
|
||||
],
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { type Dispatch, type SetStateAction, useRef, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
@@ -258,6 +258,10 @@ function KeyboardShortcutsHarness({
|
||||
}
|
||||
|
||||
describe('useImageCanvasKeyboardShortcuts', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('routes undo and redo while ignoring editable inputs', () => {
|
||||
const undoCanvasChange = vi.fn();
|
||||
const redoCanvasChange = vi.fn();
|
||||
|
||||
@@ -954,6 +954,7 @@ describe('useImageCanvasProjectPersistence', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
window.history.replaceState(null, '', '/editor/canvas');
|
||||
try {
|
||||
globalThis.sessionStorage?.clear();
|
||||
} catch {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { RUNTIME_RESOURCE_PENDING_SELECTOR } from '../components/common/RuntimeResourcePendingMarker';
|
||||
import { ResolvedAssetImage } from '../components/ResolvedAssetImage';
|
||||
import {
|
||||
clearStoredAccessToken,
|
||||
@@ -215,6 +216,7 @@ describe('useResolvedAssetReadUrl', () => {
|
||||
});
|
||||
|
||||
test('refreshKey changes force a fresh signed url request without mutating OSS signature query', async () => {
|
||||
let requestCount = 0;
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(
|
||||
async () =>
|
||||
new Response(
|
||||
@@ -224,7 +226,7 @@ describe('useResolvedAssetReadUrl', () => {
|
||||
read: {
|
||||
objectKey:
|
||||
'generated-puzzle-assets/puzzle-session-1/candidate-1/asset-1/image.png',
|
||||
signedUrl: 'https://signed.example.com/puzzle.png',
|
||||
signedUrl: `https://signed.example.com/puzzle.png?x-oss-signature=version-${++requestCount}`,
|
||||
expiresAt: '2099-01-01T00:10:00Z',
|
||||
},
|
||||
},
|
||||
@@ -255,7 +257,7 @@ describe('useResolvedAssetReadUrl', () => {
|
||||
|
||||
const firstImage = await screen.findByRole('img', { name: '候选图' });
|
||||
expect(firstImage.getAttribute('src')).toBe(
|
||||
'https://signed.example.com/puzzle.png',
|
||||
'https://signed.example.com/puzzle.png?x-oss-signature=version-1',
|
||||
);
|
||||
|
||||
rerender(
|
||||
@@ -267,11 +269,11 @@ describe('useResolvedAssetReadUrl', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
screen.getByRole('img', { name: '候选图' }).getAttribute('src'),
|
||||
).toBe('https://signed.example.com/puzzle.png?x-oss-signature=version-2');
|
||||
});
|
||||
expect(
|
||||
screen.getByRole('img', { name: '候选图' }).getAttribute('src'),
|
||||
).toBe('https://signed.example.com/puzzle.png');
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('generated 私有资源签名失败时保持空图像而不是回退裸路径', async () => {
|
||||
@@ -300,16 +302,22 @@ describe('useResolvedAssetReadUrl', () => {
|
||||
),
|
||||
);
|
||||
|
||||
render(
|
||||
const { container } = render(
|
||||
<ResolvedAssetImage
|
||||
src="/generated-puzzle-assets/puzzle-session-1/candidate-1/asset-1/image.png"
|
||||
alt="候选图"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
container.querySelector(RUNTIME_RESOURCE_PENDING_SELECTOR),
|
||||
).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
container.querySelector(RUNTIME_RESOURCE_PENDING_SELECTOR),
|
||||
).toBeNull();
|
||||
});
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByRole('img', { name: '候选图' })).toBeNull();
|
||||
});
|
||||
|
||||
@@ -339,7 +347,7 @@ describe('useResolvedAssetReadUrl', () => {
|
||||
),
|
||||
);
|
||||
|
||||
render(
|
||||
const { container } = render(
|
||||
<ResolvedAssetImage
|
||||
src="/generated-puzzle-assets/puzzle-session-1/candidate-1/asset-1/image.png"
|
||||
fallbackSrc="/creation-type-references/puzzle.webp"
|
||||
@@ -350,9 +358,15 @@ describe('useResolvedAssetReadUrl', () => {
|
||||
expect(
|
||||
screen.getByRole('img', { name: '候选图' }).getAttribute('src'),
|
||||
).toBe('/creation-type-references/puzzle.webp');
|
||||
expect(
|
||||
container.querySelector(RUNTIME_RESOURCE_PENDING_SELECTOR),
|
||||
).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
container.querySelector(RUNTIME_RESOURCE_PENDING_SELECTOR),
|
||||
).toBeNull();
|
||||
});
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
screen.getByRole('img', { name: '候选图' }).getAttribute('src'),
|
||||
).toBe('/creation-type-references/puzzle.webp');
|
||||
|
||||
@@ -211,8 +211,7 @@ describe('editorProjectClient', () => {
|
||||
{ deadlineAt: expect.any(Number) },
|
||||
);
|
||||
const requestOptions = requestJsonMock.mock.calls[0]?.[3] as
|
||||
| { deadlineAt?: number }
|
||||
| undefined;
|
||||
{ deadlineAt?: number } | undefined;
|
||||
expect(requestOptions?.deadlineAt).toBeGreaterThanOrEqual(
|
||||
startedAt + 60_000,
|
||||
);
|
||||
@@ -1112,6 +1111,7 @@ describe('editorProjectClient', () => {
|
||||
referenceId: 'editor-resource-spec',
|
||||
referenceImageSrcs: references.slice(0, 5),
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
model: 'gpt-image-2',
|
||||
}),
|
||||
).rejects.toThrow('图标素材参考图最多允许 4 张');
|
||||
@@ -1162,6 +1162,29 @@ describe('editorProjectClient', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('requires an explicit slice declaration and rejects contradictory grid dimensions', async () => {
|
||||
requestJsonMock.mockResolvedValue({ queueState: { status: 'queued' } });
|
||||
|
||||
await expect(
|
||||
generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-icon-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'grid',
|
||||
}),
|
||||
).rejects.toThrow('sliceMode=grid 必须同时提供 gridX 与 gridY');
|
||||
await expect(
|
||||
generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-icon-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
gridX: 2,
|
||||
gridY: 2,
|
||||
}),
|
||||
).rejects.toThrow('sliceMode=connected-components 不接受 gridX/gridY');
|
||||
|
||||
expect(requestJsonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects oversized stable video reference fields before submission', async () => {
|
||||
await expect(
|
||||
generateEditorVideo({
|
||||
@@ -1331,6 +1354,9 @@ describe('editorProjectClient', () => {
|
||||
referenceId: 'editor-resource-spec',
|
||||
referenceImageSrcs: ['/generated-images/editor/icon-ref.png'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
sliceMode: 'grid',
|
||||
gridX: 3,
|
||||
gridY: 2,
|
||||
assetLabel: '冒险游戏图标',
|
||||
});
|
||||
|
||||
@@ -1348,6 +1374,9 @@ describe('editorProjectClient', () => {
|
||||
referenceId: 'editor-resource-spec',
|
||||
referenceImageSrcs: ['/generated-images/editor/icon-ref.png'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
sliceMode: 'grid',
|
||||
gridX: 3,
|
||||
gridY: 2,
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
assetLabel: '冒险游戏图标',
|
||||
}),
|
||||
@@ -1365,6 +1394,7 @@ describe('editorProjectClient', () => {
|
||||
generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: ['图'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS + 1)],
|
||||
sliceMode: 'connected-components',
|
||||
}),
|
||||
).rejects.toThrow(`不能超过 ${EDITOR_ICON_DESCRIPTION_MAX_CHARS} 个字符`);
|
||||
|
||||
@@ -1377,6 +1407,7 @@ describe('editorProjectClient', () => {
|
||||
'图'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS),
|
||||
),
|
||||
],
|
||||
sliceMode: 'connected-components',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`合计不能超过 ${EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS} 个字符`,
|
||||
@@ -1388,6 +1419,7 @@ describe('editorProjectClient', () => {
|
||||
iconDescriptions: Array.from({ length: 8 }, () =>
|
||||
'😀'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS),
|
||||
),
|
||||
sliceMode: 'connected-components',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`合计不能超过 ${EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES} 个 UTF-8 字节`,
|
||||
@@ -1412,6 +1444,7 @@ describe('editorProjectClient', () => {
|
||||
await generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
segModel: 'anime-seg',
|
||||
@@ -1434,6 +1467,7 @@ describe('editorProjectClient', () => {
|
||||
body: JSON.stringify({
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
segModel: 'anime-seg',
|
||||
|
||||
@@ -219,9 +219,7 @@ export type EditorAssetGenerationInputs = {
|
||||
};
|
||||
|
||||
export type EditorProjectResourceSourceType =
|
||||
| 'uploaded'
|
||||
| 'generated'
|
||||
| 'mock_generated';
|
||||
'uploaded' | 'generated' | 'mock_generated';
|
||||
|
||||
export type EditorProjectResourceSnapshot = {
|
||||
resourceId: string;
|
||||
@@ -330,11 +328,7 @@ export type EditorImageGenerationInput = {
|
||||
prompt: string;
|
||||
size?: string;
|
||||
kind?:
|
||||
| 'spec'
|
||||
| 'character'
|
||||
| 'quick-edit'
|
||||
| 'ui-design'
|
||||
| 'publication-material';
|
||||
'spec' | 'character' | 'quick-edit' | 'ui-design' | 'publication-material';
|
||||
model?: string;
|
||||
screenColor?: string;
|
||||
segModel?: string;
|
||||
@@ -380,7 +374,8 @@ export type EditorIconSpritesheetGenerationInput = {
|
||||
referenceId: string;
|
||||
referenceImageSrcs?: string[];
|
||||
iconDescriptions: string[];
|
||||
sliceMode?: 'connected-components' | 'grid';
|
||||
/** 必填且没有默认值:必须显式声明连通域或网格切分。 */
|
||||
sliceMode: 'connected-components' | 'grid';
|
||||
gridX?: number;
|
||||
gridY?: number;
|
||||
model?: string;
|
||||
@@ -543,12 +538,7 @@ export type EditorIconSpritesheetSliceResult = {
|
||||
export type EditorCharacterAnimationResolution = '480p' | '720p';
|
||||
|
||||
export type EditorCharacterAnimationRatio =
|
||||
| 'same'
|
||||
| '1:1'
|
||||
| '4:3'
|
||||
| '16:9'
|
||||
| '9:16'
|
||||
| '3:4';
|
||||
'same' | '1:1' | '4:3' | '16:9' | '9:16' | '3:4';
|
||||
|
||||
export type EditorCharacterAnimationFrameCount = 32 | 40 | 48;
|
||||
|
||||
@@ -610,12 +600,7 @@ export type EditorVideoModel =
|
||||
export type EditorVideoResolution = '480p' | '720p' | '1080p';
|
||||
export type EditorVideoSoundMode = 'on' | 'off';
|
||||
export type EditorVideoAspectRatio =
|
||||
| '16:9'
|
||||
| '9:16'
|
||||
| '1:1'
|
||||
| '4:3'
|
||||
| '3:4'
|
||||
| '21:9';
|
||||
'16:9' | '9:16' | '1:1' | '4:3' | '3:4' | '21:9';
|
||||
|
||||
export type EditorVideoGenerationInput = {
|
||||
prompt: string;
|
||||
@@ -1300,6 +1285,16 @@ export async function generateEditorIconSpritesheet(
|
||||
input.iconDescriptions,
|
||||
);
|
||||
const model = input.model?.trim() || EDITOR_IMAGE_MODEL_NANOBANANA2;
|
||||
// 切分模式没有默认值:声明必须自洽,网格尺寸只能与 grid 同时提交。
|
||||
if (input.sliceMode === 'grid') {
|
||||
if (input.gridX === undefined || input.gridY === undefined) {
|
||||
throw new Error('sliceMode=grid 必须同时提供 gridX 与 gridY');
|
||||
}
|
||||
} else if (input.gridX !== undefined || input.gridY !== undefined) {
|
||||
throw new Error(
|
||||
'sliceMode=connected-components 不接受 gridX/gridY:网格尺寸只能与 grid 同时提交',
|
||||
);
|
||||
}
|
||||
assertStableEditorMediaReferences(input.referenceImageSrcs, '图标素材参考图');
|
||||
assertEditorReferenceLimit(
|
||||
input.referenceImageSrcs,
|
||||
@@ -1317,9 +1312,13 @@ export async function generateEditorIconSpritesheet(
|
||||
? { referenceImageSrcs: input.referenceImageSrcs }
|
||||
: {}),
|
||||
iconDescriptions,
|
||||
...(input.sliceMode ? { sliceMode: input.sliceMode } : {}),
|
||||
...(input.gridX !== undefined ? { gridX: input.gridX } : {}),
|
||||
...(input.gridY !== undefined ? { gridY: input.gridY } : {}),
|
||||
sliceMode: input.sliceMode,
|
||||
...(input.sliceMode === 'grid' && input.gridX !== undefined
|
||||
? { gridX: input.gridX }
|
||||
: {}),
|
||||
...(input.sliceMode === 'grid' && input.gridY !== undefined
|
||||
? { gridY: input.gridY }
|
||||
: {}),
|
||||
model,
|
||||
...(input.screenColor ? { screenColor: input.screenColor } : {}),
|
||||
...(input.segModel ? { segModel: input.segModel } : {}),
|
||||
|
||||
Reference in New Issue
Block a user