恢复完美像素占位的用户删除权

低层删除不再对未收口 operation 抗命,随源图层与多选删除同等对待

删除确认判据收敛为 requiresGenerationDeleteConfirmation,免费的完美像素不再弹泥点确认

标记 operation 失效时一并丢弃快照,消除重试消失但占位仍在的矛盾态

翻转钉死删除保护的用例,改为覆盖删除后结果仍落库并走 asset-only 提示

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 04:50:37 +00:00
parent 291dd75a8c
commit 5925b396d0
8 changed files with 190 additions and 135 deletions
@@ -6370,3 +6370,14 @@
- 遗留:历史资源行的 `model` 列仍存有 2026-07-30 之前写入的内部处理模型,读边界继续脱敏它。把该列回填为源生图模型、原值移入 `generationInputs.mattingModel`,并据此删掉两侧的脱敏与推导逻辑,另行排期,不在本次范围。
- 验证方式:前端覆盖 `serializeLayer` 产物不含 `model` / `provider`,以及「序列化后去掉 sourceType → hydrate → 再序列化」仍为 `generated` 的往返不变式;api-server 覆盖读边界按 `resourceId` 回填 `sourceType`、且缺资源行的 legacy 本地序列保持自带值;spacetime-module 覆盖资源行存内部处理模型而图层带推导值时不再报错。运行 `npx vitest run src/components/image-editor``cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_project::``cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml --all-targets``npm run typecheck``npm run check:encoding``npm run check:rustfmt`。spacetime-module 的单测二进制在 Windows 本机链接失败(缺 SpacetimeDB 宿主符号),本机只能做到 `cargo check --all-targets`
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`
## 2026-08-05 完美像素占位恢复为可删除,删除保护条款作废
- 背景:2026-08-04「完美像素第二批:object-only 上传与 GET-only unknown 收口」把「未收口的完美像素 operation」定为不可删除,理由是结果 unknown 时要保留 identity 供原样重试;同日「confirm 后 strict 保存失败保留原 operation」又把 POST 尚未发出的 `failed` 一并纳入,理由是从源图重开会重新执行 ticket / PUT / confirm,让上一次已 confirm 的源图对象失去引用。两条叠加后,右键删除、Delete 快捷键、随源图层删除、多选删除全部豁免该占位,到期清理也不覆盖它,用户画布上出现了删不掉的元素。
- 缺陷判定:删除占位**不撤销任何在途请求**——完美像素没有取消接口,结果照常落库并进素材库;服务端 completion 发现 dialog 已不在会返回 `DialogMissing`,客户端本就有对应提示。封锁买到的只是「结果自动回填画布」这一便利,代价却是用户文档不可编辑。至于孤儿源图对象,「confirm 后 strict 保存失败保留原 operation」自己的「剩余边界」一节已把同类残留定性为「影响限于不可达的源图存储与垃圾记录累积……不作为本 PR 的合并阻断项」;为避免同一种残留而禁止用户删除自己画布上的元素,权衡不自洽。
- 决策:本条取代上述两条中关于**删除**的条款。完美像素占位在 `generating` / `pending-confirmation` / `failed` 任何状态都可删,且不弹确认。低层 `removeCanvasGenerationDialogById` 恢复为无条件删除——低层对上层抗命正是「占位未删却写出伪历史」的根因;随源图层删除不再豁免;Delete 快捷键与多选删除不再过滤该目标。删除确认的判据收敛为具名的 `requiresGenerationDeleteConfirmation`:现成弹窗讲的是「已消耗的泥点不会返还」,只对计费生成成立,而完美像素 `generation_cost_mud_points = 0`。运行时标记 `perfectPixelOperationInvalid` 时必须同时丢弃 `perfectPixelOperation`,与 `hydrateCanvasGenerationDialog` 口径一致,不再留下「重试按钮因 invalid 消失、快照却还挂着」的矛盾态。
- 保留不变:保留 operation identity 供「在原占位原样重试」的能力不变,重试仍复用同一 request、source objectKey 与 dialog / operation / task identity。从源图重新发起仍被 `existingOperation` 闸拦住,另行处理。带 operation 的占位继续豁免 inline 占位 TTL 到期清理——用户主动删除与系统替用户删除是两回事。
- 已知后果:删掉 `pending-confirmation` 占位后,刷新恢复不再对账这条 operation;结果若已生成只会出现在素材库,不回填画布。这是用户主动放弃的结果,不是回归,不得据此判定为缺陷。
- 影响范围:`src/components/image-editor/useCanvasGenerationDialogs.ts`(删除 `isUnsettledPerfectPixelOperationDialog`,新增 `requiresGenerationDeleteConfirmation`)、`ImageCanvasEditorView.tsx``requestRemoveCanvasGenerationDialog``useImageCanvasLayerCommands.ts``deleteSelectedLayer``useImageCanvasGenerationWorkflow.ts` 的恢复失效分支。不修改服务端、契约或数据。
- 验证方式:覆盖三种状态下按 id 删除与随源图层删除均真正移除、完美像素占位任何状态都不要求确认而普通 `generating` 占位仍要求、快捷键与混合选择删除会写入历史并触发副作用、在途删除后已知失败退回全局提示、删除后 applied verdict 走 asset-only 提示且不回填画布、标记失效时快照被丢弃。运行 `npx vitest run src/components/image-editor``npx vitest run src/components/platform-entry``npm run typecheck``npm run check:encoding`
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`
@@ -79,7 +79,7 @@ import {
} from './ImageCanvasSelectionModel';
import { ImageCanvasShortcutDialogView } from './ImageCanvasShortcutDialogView';
import {
isUnsettledPerfectPixelOperationDialog,
requiresGenerationDeleteConfirmation,
useCanvasGenerationDialogs,
} from './useCanvasGenerationDialogs';
import { useCanvasHistory } from './useCanvasHistory';
@@ -1719,31 +1719,14 @@ export function ImageCanvasEditorView({
);
const requestRemoveCanvasGenerationDialog = useCallback(
(dialog: CanvasGenerationDialogState) => {
if (isUnsettledPerfectPixelOperationDialog(dialog)) {
const previousSelectedLayerId = selectedLayerIdRef.current;
const previousSelectedLayerIds = [...selectedLayerIdsRef.current];
activateCanvasGenerationDialog(dialog);
setSelectedLayerId(previousSelectedLayerId);
setSelectedLayerIds(previousSelectedLayerIds);
showGenerationWarning(
'完美像素结果尚未确认,当前占位用于保留原操作身份;请继续核对或按原操作重试。',
);
return;
}
if (dialog.status === 'generating') {
if (requiresGenerationDeleteConfirmation(dialog)) {
activateCanvasGenerationDialog(dialog);
setPendingGenerationDeleteDialog(dialog);
return;
}
removeCanvasGenerationDialog(dialog.id);
},
[
activateCanvasGenerationDialog,
removeCanvasGenerationDialog,
setSelectedLayerId,
setSelectedLayerIds,
showGenerationWarning,
],
[activateCanvasGenerationDialog, removeCanvasGenerationDialog],
);
const contextMenuLayer =
contextMenu?.kind === 'layer'
@@ -4,7 +4,10 @@ import { act, renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { CanvasGenerationDialogState } from './ImageCanvasEditorTypes';
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
import {
requiresGenerationDeleteConfirmation,
useCanvasGenerationDialogs,
} from './useCanvasGenerationDialogs';
function createDialog(
mode: CanvasGenerationDialogState['mode'],
@@ -26,6 +29,45 @@ function createDialog(
};
}
function durablePerfectPixelDialog(
dialogId: string,
status: 'generating' | 'pending-confirmation' | 'failed',
): CanvasGenerationDialogState {
const submittedAt = 1_700_000_000_000;
return {
id: dialogId,
mode: 'quick-edit',
prompt: '完美像素',
status,
composerOpen: true,
sourceLayerId: 'layer-source',
perfectPixelOperation: {
version: 1,
kind: 'perfect-pixel',
operationId: dialogId,
taskId: `pixel-art-snap-${dialogId}`,
request: {
sourceImageSrc: 'generated-images/editor/source.png',
projectId: 'project-1',
canvasCompletion: {
dialogId,
title: '源图 · 完美像素',
placeholder: {
x: 0,
y: 0,
width: 320,
height: 240,
originalWidth: 320,
originalHeight: 240,
},
},
},
submittedAt,
reconcileUntil: submittedAt + 75_000,
},
};
}
describe('useCanvasGenerationDialogs', () => {
it('archives, activates, updates, and removes canvas generation dialogs', () => {
const onActivate = vi.fn();
@@ -306,69 +348,73 @@ describe('useCanvasGenerationDialogs', () => {
]);
});
it('preserves unsettled durable perfect-pixel operations across ordinary delete paths', () => {
// 中文注释:占位是用户文档的一部分,删除它不撤销任何在途请求——完美像素没有取消接口,
// 结果照常落库并进素材库。低层删除因此不得对未收口 operation 抗命,否则上层会写出
// 「历史记了一笔、占位还在」的伪历史。
it.each(['generating', 'pending-confirmation', 'failed'] as const)(
'deletes an unsettled durable perfect-pixel operation by id while %s',
(status) => {
const { result } = renderHook(() => useCanvasGenerationDialogs());
const dialogId = 'perfect-pixel-durable';
act(() => {
result.current.restoreCanvasGenerationDialogs([
durablePerfectPixelDialog(dialogId, status),
]);
});
act(() => {
result.current.removeCanvasGenerationDialogById(dialogId);
});
expect(result.current.activeCanvasGenerationDialog).toBeNull();
expect(result.current.canvasGenerationDialogs).toEqual([]);
},
);
// 中文注释:确认弹窗讲的是「已消耗的泥点不会返还」,只对计费生成成立;完美像素免费且删除
// 占位不撤销在途请求,所以它在任何状态都直接删。
it.each(['generating', 'pending-confirmation', 'failed'] as const)(
'never asks for delete confirmation on a perfect-pixel placeholder while %s',
(status) => {
expect(
requiresGenerationDeleteConfirmation(
durablePerfectPixelDialog('perfect-pixel-confirm', status),
),
).toBe(false);
},
);
it('still asks for delete confirmation on an ordinary generating placeholder', () => {
expect(
requiresGenerationDeleteConfirmation({
id: 'generation-dialog-1',
...createDialog('generate', '一只猫'),
status: 'generating',
}),
).toBe(true);
expect(
requiresGenerationDeleteConfirmation({
id: 'generation-dialog-2',
...createDialog('generate', '一只猫'),
status: 'failed',
}),
).toBe(false);
});
it('drops unsettled durable perfect-pixel operations together with their source layer', () => {
const { result } = renderHook(() => useCanvasGenerationDialogs());
const dialogId = 'perfect-pixel-durable';
const submittedAt = 1_700_000_000_000;
act(() => {
result.current.restoreCanvasGenerationDialogs([
{
id: dialogId,
mode: 'quick-edit',
prompt: '完美像素',
status: 'failed',
composerOpen: true,
sourceLayerId: 'layer-source',
perfectPixelOperation: {
version: 1,
kind: 'perfect-pixel',
operationId: dialogId,
taskId: `pixel-art-snap-${dialogId}`,
request: {
sourceImageSrc: 'generated-images/editor/source.png',
projectId: 'project-1',
canvasCompletion: {
dialogId,
title: '源图 · 完美像素',
placeholder: {
x: 0,
y: 0,
width: 320,
height: 240,
originalWidth: 320,
originalHeight: 240,
},
},
},
submittedAt,
reconcileUntil: submittedAt + 75_000,
},
},
durablePerfectPixelDialog(dialogId, 'pending-confirmation'),
]);
});
act(() => {
result.current.removeCanvasGenerationDialogById(dialogId);
result.current.removeCanvasGenerationDialogsByLayerId('layer-source');
});
expect(result.current.activeCanvasGenerationDialog).toEqual(
expect.objectContaining({
id: dialogId,
status: 'failed',
perfectPixelOperation: expect.objectContaining({
operationId: dialogId,
}),
}),
);
act(() => {
result.current.updateCanvasGenerationDialogById(dialogId, (dialog) => ({
...dialog,
status: 'idle',
}));
result.current.removeCanvasGenerationDialogById(dialogId);
});
expect(result.current.activeCanvasGenerationDialog).toBeNull();
expect(result.current.canvasGenerationDialogs).toEqual([]);
});
});
@@ -24,15 +24,18 @@ export type CanvasGenerationDialogDraft = Omit<
id?: string;
};
export function isUnsettledPerfectPixelOperationDialog(
/**
* 中文注释:删除生成占位前是否需要二次确认。
*
* 确认弹窗讲的是「已消耗的泥点不会返还」,只对计费生成成立。完美像素
* `generation_cost_mud_points = 0`,删除占位也不撤销任何在途请求——它没有取消接口,结果照常
* 落库并进素材库,服务端 completion 发现 dialog 已不在会返回 DialogMissing 并由客户端提示。
* 所以完美像素占位在任何状态都直接删,不额外解释。
*/
export function requiresGenerationDeleteConfirmation(
dialog: CanvasGenerationDialogState,
) {
return (
Boolean(dialog.perfectPixelOperation) &&
(dialog.status === 'generating' ||
dialog.status === 'pending-confirmation' ||
dialog.status === 'failed')
);
return dialog.status === 'generating' && !dialog.perfectPixelOperation;
}
function withGenerationTimestamps<T extends GenerateDialogState | null>(
@@ -227,11 +230,13 @@ export function useCanvasGenerationDialogs({
[],
);
// 中文注释:低层删除不再对未收口的完美像素 operation 抗命。删除占位不撤销任何在途请求
// ——完美像素没有取消接口,结果照常落库并进素材库,服务端 completion 发现 dialog 已不在
// 会返回 DialogMissing,客户端有对应提示。封锁换来的只是「结果自动回填画布」这一便利,
// 代价却是用户画布上出现删不掉的对象;低层偷偷保留还会让上层写出伪历史。
const removeCanvasGenerationDialogById = useCallback(
(dialogId: string) => {
updateCanvasGenerationDialogById(dialogId, (dialog) =>
isUnsettledPerfectPixelOperationDialog(dialog) ? dialog : null,
);
updateCanvasGenerationDialogById(dialogId, () => null);
},
[updateCanvasGenerationDialogById],
);
@@ -312,9 +317,8 @@ export function useCanvasGenerationDialogs({
const removeCanvasGenerationDialogsByLayerId = useCallback(
(targetLayerId: string) => {
const keepDialog = (dialog: CanvasGenerationDialogState) =>
isUnsettledPerfectPixelOperationDialog(dialog) ||
(dialog.sourceLayerId !== targetLayerId &&
dialog.generatedLayerId !== targetLayerId);
dialog.sourceLayerId !== targetLayerId &&
dialog.generatedLayerId !== targetLayerId;
const currentDialog = generateDialogRef.current;
const nextActiveDialog =
isCanvasGenerationDialog(currentDialog) && !keepDialog(currentDialog)
@@ -3373,7 +3373,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
expect(screen.getByTestId('dialog').textContent).toBe('-');
});
it('keeps the durable operation when deletion is attempted and applies a later GET result', async () => {
// 中文注释:删除占位不撤销在途请求。结果照常落库,但既然用户已经把占位拿掉,就不再回填画布,
// 只提示结果已进素材库。
it('deletes the durable operation and reports a later applied GET result as asset-only', async () => {
const applyProjectSnapshot = vi.fn();
let operationId = '';
let rejectPerfectPixel: ((error: unknown) => void) | undefined;
@@ -3408,22 +3410,17 @@ describe('useImageCanvasGenerationWorkflow', () => {
);
});
fireEvent.click(screen.getByRole('button', { name: '删除处理占位' }));
expect(screen.getByTestId('generation-dialogs').textContent).not.toBe('-');
expect(screen.getByTestId('generation-dialogs').textContent).toBe('-');
rejectPerfectPixel?.(new Error('网络中断'));
await waitFor(() => {
expect(applyProjectSnapshot).toHaveBeenCalledWith(
expect.objectContaining({
projectId: 'project-1',
}),
{
type: 'perfect-pixel',
count: 1,
},
expect(screen.getByTestId('reference-pick-warning').textContent).toBe(
'完美像素结果已生成并保存到素材库。画布占位已被删除,结果未落入当前画布,重新加载后可见。',
);
});
expect(screen.getByTestId('reference-pick-warning').textContent).toBe('-');
expect(applyProjectSnapshot).not.toHaveBeenCalled();
expect(screen.getByTestId('generation-dialogs').textContent).toBe('-');
});
it('does not let a never-settling asset refresh block a terminal reconciliation verdict', async () => {
@@ -3794,7 +3791,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
).toBe('空闲');
});
it('refuses to delete a durable in-flight operation and keeps a later known failure visible', async () => {
// 中文注释:占位属于用户文档,在途也允许删除——删除不撤销任何请求。占位没了之后,已知失败
// 没有地方可挂,必须退回全局提示,不能静默。
it('deletes a durable in-flight operation and reports the later known failure globally', async () => {
let rejectPerfectPixel: ((error: unknown) => void) | undefined;
snapImageToPerfectPixelsMock.mockImplementationOnce(
() =>
@@ -3824,7 +3823,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
});
fireEvent.click(screen.getByRole('button', { name: '删除处理占位' }));
expect(screen.getByTestId('generation-dialogs').textContent).not.toBe('-');
expect(screen.getByTestId('generation-dialogs').textContent).toBe('-');
// 中文注释:用 ApiClientError(已知结果)避免走对账分支,把用例聚焦在「占位没了还要不要说话」。
rejectPerfectPixel?.(
@@ -3836,12 +3835,12 @@ describe('useImageCanvasGenerationWorkflow', () => {
);
await waitFor(() => {
expect(screen.getByTestId('dialog-error').textContent).toBe(
expect(screen.getByTestId('reference-pick-warning').textContent).toBe(
'完美像素画布占位不存在或尚未保存,请重试。',
);
});
expect(screen.getByTestId('dialog').textContent).toContain('failed');
expect(screen.getByTestId('reference-pick-warning').textContent).toBe('-');
expect(screen.getByTestId('generation-dialogs').textContent).toBe('-');
expect(loadEditorProjectMock).not.toHaveBeenCalled();
expect(
screen.getByRole('status', { name: '完美像素状态' }).textContent,
).toBe('空闲');
@@ -3888,8 +3887,6 @@ describe('useImageCanvasGenerationWorkflow', () => {
await waitFor(() => {
expect(snapImageToPerfectPixelsMock).toHaveBeenCalled();
});
fireEvent.click(screen.getByRole('button', { name: '删除处理占位' }));
expect(screen.getByTestId('generation-dialogs').textContent).not.toBe('-');
resolvePerfectPixel?.(
createPerfectPixelResult(
@@ -4142,6 +4139,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
expect(
screen.getByTestId('perfect-pixel-operation-invalid').textContent,
).toBe('invalid');
// 中文注释:标记失效必须同时丢弃快照,与 hydrateCanvasGenerationDialog 一致。两者并存会
// 造出「重试按钮因 invalid 消失、快照却还挂着」的矛盾态。
expect(screen.getByTestId('perfect-pixel-operation').textContent).toBe('-');
expect(screen.getByTestId('dialog-error').textContent).toBe(
'完美像素操作快照与当前项目不匹配,禁止自动恢复或重试。',
);
@@ -2781,17 +2781,27 @@ export function useImageCanvasGenerationWorkflow({
perfectPixelRecoveryControllersRef.current.set(recoveryKey, controller);
void (async () => {
if (operation.request.projectId !== normalizedProjectId) {
updateCanvasGenerationDialogById(dialog.id, (current) =>
isSameUnsettledPerfectPixelOperation(current, operation.operationId)
? {
...current,
status: 'failed' as const,
perfectPixelOperationInvalid: true,
errorMessage:
'完美像素操作快照与当前项目不匹配,禁止自动恢复或重试。',
}
: current,
);
updateCanvasGenerationDialogById(dialog.id, (current) => {
if (
!isSameUnsettledPerfectPixelOperation(
current,
operation.operationId,
)
) {
return current;
}
// 中文注释:标记快照失效时必须同时丢弃 operation,与 hydrateCanvasGenerationDialog
// 的口径一致。两者并存会造出「重试按钮因 invalid 而消失、快照却还挂着」的矛盾态,
// 用户既重试不了也看不懂占位为什么还在。
const { perfectPixelOperation: _discarded, ...rest } = current;
return {
...rest,
status: 'failed' as const,
perfectPixelOperationInvalid: true,
errorMessage:
'完美像素操作快照与当前项目不匹配,禁止自动恢复或重试。',
};
});
return;
}
const verdict = await reconcilePerfectPixelProject(
@@ -756,8 +756,11 @@ describe('useImageCanvasLayerCommands', () => {
);
});
// 中文注释:未收口的完美像素 operation 不再豁免删除。删除占位不撤销任何在途请求,结果照常
// 落库并进素材库;封锁换来的只是「结果自动回填画布」这一便利,代价是用户画布上出现删不掉
// 的对象。快捷键删除因此与其它占位同等对待。
it.each(['generating', 'pending-confirmation', 'failed'] as const)(
'keeps a selected durable perfect-pixel dialog as a complete no-op while %s',
'deletes a selected durable perfect-pixel dialog while %s',
(status) => {
const dialog = createDurablePerfectPixelDialog(
`perfect-pixel-${status}`,
@@ -782,15 +785,18 @@ describe('useImageCanvasLayerCommands', () => {
fireEvent.click(screen.getByRole('button', { name: '删除选中' }));
expect(screen.getByTestId('history').textContent).toBe('0');
expect(screen.getByTestId('selection').textContent).toBe(
`${selectionId}:${selectionId}`,
);
expect(screen.getByTestId('context').textContent).toBe('open');
expect(screen.getByTestId('image-context-closed').textContent).toBe('0');
expect(onCaptureCanvasHistory).not.toHaveBeenCalled();
expect(screen.getByTestId('history').textContent).toBe('1');
expect(onCaptureCanvasHistory).toHaveBeenCalledWith({
type: 'delete-generation-result',
count: 1,
});
expect(screen.getByTestId('selection').textContent).toBe('-:');
expect(screen.getByTestId('context').textContent).toBe('closed');
expect(screen.getByTestId('image-context-closed').textContent).toBe('1');
expect(onDeleteLayerSideEffects).not.toHaveBeenCalled();
expect(onDeleteGenerationDialogSideEffects).not.toHaveBeenCalled();
expect(onDeleteGenerationDialogSideEffects).toHaveBeenCalledWith(
dialog.id,
);
},
);
@@ -836,7 +842,7 @@ describe('useImageCanvasLayerCommands', () => {
expect(onDeleteGenerationDialogSideEffects).not.toHaveBeenCalled();
});
it('deletes and counts only deletable targets in a mixed durable-dialog selection', () => {
it('deletes both layer and durable-dialog targets in a mixed selection', () => {
const dialog = createDurablePerfectPixelDialog(
'perfect-pixel-pending',
'pending-confirmation',
@@ -861,7 +867,8 @@ describe('useImageCanvasLayerCommands', () => {
fireEvent.click(screen.getByRole('button', { name: '删除选中' }));
expect(screen.getByTestId('layers').textContent).not.toContain('first');
expect(screen.getByTestId('selection').textContent).toBe('third:third');
// 中文注释:混合选择里存在占位目标时统一清空选择,计数沿用既有口径——只数图层。
expect(screen.getByTestId('selection').textContent).toBe('-:');
expect(screen.getByTestId('history').textContent).toBe('1');
expect(onCaptureCanvasHistory).toHaveBeenCalledWith({
type: 'delete-image',
@@ -869,6 +876,7 @@ describe('useImageCanvasLayerCommands', () => {
});
expect(onDeleteLayerSideEffects).toHaveBeenCalledTimes(1);
expect(onDeleteLayerSideEffects).toHaveBeenCalledWith('first');
expect(onDeleteGenerationDialogSideEffects).not.toHaveBeenCalled();
expect(onDeleteGenerationDialogSideEffects).toHaveBeenCalledTimes(1);
expect(onDeleteGenerationDialogSideEffects).toHaveBeenCalledWith(dialog.id);
});
});
@@ -37,9 +37,6 @@ import {
getCanvasGenerationDialogIdFromSelectionId,
getSelectedLayerIds,
} from './ImageCanvasSelectionModel';
import {
isUnsettledPerfectPixelOperationDialog,
} from './useCanvasGenerationDialogs';
import type { ImageSequenceExportMode } from './useImageCanvasAssetExportWorkflow';
type LayerCommandsOptions = {
@@ -703,11 +700,7 @@ export function useImageCanvasLayerCommands({
.map(getCanvasGenerationDialogIdFromSelectionId)
.filter((dialogId): dialogId is string => Boolean(dialogId))
.filter((dialogId) =>
canvasGenerationDialogs.some(
(dialog) =>
dialog.id === dialogId &&
!isUnsettledPerfectPixelOperationDialog(dialog),
),
canvasGenerationDialogs.some((dialog) => dialog.id === dialogId),
);
if (!targetLayerIds.length && !targetDialogIds.length) {
return;