修复画布权威快照合并回归
统一生成完成态与本地布局的生命周期合并 保留同项目权威刷新中的有效画布选择 同步生成集成夹具与真实后端完成契约 补齐画布持久化回归测试和项目记忆
This commit is contained in:
@@ -196,6 +196,8 @@
|
||||
- Windows 私有 ACL 检查复用 `Get-Item` 对象的 `GetAccessControl()`,避免从 PowerShell 7 启动时继承的模块路径让 Windows PowerShell 5.1 的 `Get-Acl` 加载不兼容模块;静态配置门禁禁止重新引入该命令。
|
||||
- 编辑器持久化的 `prompt` 统一表示规范化用户意图;provider `actual_prompt` 只保留在 resource / asset 审计字段,系统 prompt 不进入跨资源检索字段。角色和图标的透明图、切片继承源用户 prompt;本次只修新写入,不迁移历史记录,不修改 SpacetimeDB schema。
|
||||
- 画布收到生成完成等较新权威快照时,必须把同项目待保存或在途的本地布局重放到新 revision:后端资源与生成终态优先,本地布局编辑优先;后端新增项合入,后端删除项和用户本地删除项均不得复活,合并后立即进入既有串行 CAS 保存队列。
|
||||
- 生成器合并必须把 `status / composerOpen / generatedLayerId / errorMessage / generation timestamps / characterAnimationResult` 视为后端生命周期事实;生成完成快照继续保持 `composerOpen=false`,不得被本地在途快照重新展开。提示词、参数和占位位置等本地布局编辑继续保留。
|
||||
- 同项目权威快照刷新不得无条件选择第一张图层:当前仍有效的单选、多选和生成占位选择保持,已删除的选择过滤,本来未选择时保持空选;只有首次载入或切换到另一项目时才默认选择第一张可用图层。生成完成结果需要用户显式点击后才进入选中态,后台完成回包不能偷走用户当前焦点。
|
||||
|
||||
## 2026-07-30 Provider 503 等待与耗尽状态使用严格字段派生的安全摘要
|
||||
|
||||
|
||||
@@ -4035,7 +4035,8 @@
|
||||
- 现象:用户拖动、缩放、改层序、背景色或 viewport 后,生成完成回包立即覆盖画布;450ms 防抖尚未触发或布局保存仍在途时,编辑静默丢失,undo 也可能被生成保护项阻断。
|
||||
- 原因:服务端 revision 只能排序已提交事实,本地未落库布局没有 revision;直接清空 pending save 并整体应用权威快照等同于把“服务端更新更晚”误判成“服务端知道本地编辑”。
|
||||
- 处理:保留同项目最新本地 dirty snapshot,权威回包先更新资源和生成终态,再按稳定 item ID 合并本地布局字段并基于新 revision 保存。旧权威项在新快照缺失表示后端删除,不能从 pending 或在途旧输入复活;新权威项必须合入,本地删除的旧项不能从权威回包复活。
|
||||
- 验证:分别覆盖防抖 pending、真实在途成功与 409、后端新增、后端删除、本地删除、viewport、背景色和生成面板完成态。
|
||||
- 生成器边界:`composerOpen` 与 `status / generatedLayerId / errorMessage` 一样属于后端生命周期事实;生成完成快照要求保持面板关闭时,不得被本地在途快照重新展开。提示词、参数和占位位置等本地布局编辑继续保留。集成测试夹具必须模拟后端真实完成快照:既有布局保持原位,完成结果层追加到末尾。同项目权威刷新还必须保留仍有效的单选、多选、生成占位选择或空选,只过滤已删除目标,不得无条件降成第一张图层的单选;首次载入 / 项目切换才设置默认选择。不要只跑 persistence Hook 单测,必须同时运行图片画布生成集成测试,覆盖完成后面板关闭、显式选择结果、背景清选和合并后 CAS 保存。
|
||||
- 验证:分别覆盖防抖 pending、真实在途成功与 409、后端新增、后端删除、本地删除、viewport、背景色和生成面板完成态;运行 `npm run test -- src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`。
|
||||
|
||||
## Provider schema 能力不能从统一工具标记直接推断(2026-08-03)
|
||||
|
||||
|
||||
@@ -428,6 +428,24 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
}),
|
||||
],
|
||||
layers: [
|
||||
...projectLayers,
|
||||
{
|
||||
itemType: 'generation-dialog',
|
||||
layerId: `generation-dialog:${canvasCompletion.dialogId}`,
|
||||
resourceId: `generation-dialog:${canvasCompletion.dialogId}`,
|
||||
dialog: {
|
||||
id: canvasCompletion.dialogId,
|
||||
mode: dialogMode,
|
||||
prompt,
|
||||
status: 'idle',
|
||||
composerOpen: false,
|
||||
generatedLayerId: layerId,
|
||||
placeholder,
|
||||
imageModel: model,
|
||||
aspectRatio: input.aspectRatio,
|
||||
imageSize: input.imageSize,
|
||||
},
|
||||
},
|
||||
{
|
||||
layerId,
|
||||
resourceId,
|
||||
@@ -450,24 +468,6 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
assetKind: input.assetKind ?? undefined,
|
||||
generationInputs: input.generationInputs,
|
||||
},
|
||||
{
|
||||
itemType: 'generation-dialog',
|
||||
layerId: `generation-dialog:${canvasCompletion.dialogId}`,
|
||||
resourceId: `generation-dialog:${canvasCompletion.dialogId}`,
|
||||
dialog: {
|
||||
id: canvasCompletion.dialogId,
|
||||
mode: dialogMode,
|
||||
prompt,
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
generatedLayerId: layerId,
|
||||
placeholder,
|
||||
imageModel: model,
|
||||
aspectRatio: input.aspectRatio,
|
||||
imageSize: input.imageSize,
|
||||
},
|
||||
},
|
||||
...projectLayers,
|
||||
],
|
||||
updatedAt: '2026-06-19T00:00:00.000Z',
|
||||
},
|
||||
@@ -887,10 +887,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
.getByAltText(/画布图片:生成图片/)
|
||||
.closest('button')!;
|
||||
expect(generatedLayer).toBeTruthy();
|
||||
const anchoredGenerateDialog = screen.getByRole('dialog', {
|
||||
name: '生成图片',
|
||||
});
|
||||
expect(anchoredGenerateDialog).toBeTruthy();
|
||||
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
expect(
|
||||
Number.isFinite(
|
||||
Number.parseFloat((generatedLayer as HTMLElement).style.top),
|
||||
@@ -978,10 +975,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
const generatedLayer = screen
|
||||
.getByAltText(/画布图片:生成图片/)
|
||||
.closest('button')!;
|
||||
const anchoredGenerateDialog = screen.getByRole('dialog', {
|
||||
name: '生成图片',
|
||||
});
|
||||
expect(anchoredGenerateDialog).toBeTruthy();
|
||||
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
expect(screen.queryByLabelText('图像生成占位图')).toBeNull();
|
||||
expect(
|
||||
Number.parseFloat((generatedLayer as HTMLElement).style.left) +
|
||||
@@ -1522,7 +1516,28 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect(screen.getByAltText(/画布图片:角色规范/)).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByText('规范')).toBeTruthy();
|
||||
expect(saveEditorProjectLayoutMock).not.toHaveBeenCalled();
|
||||
expect(saveEditorProjectLayoutMock).toHaveBeenCalledWith(
|
||||
'editor-project-default',
|
||||
expect.objectContaining({
|
||||
expectedRevision: 1,
|
||||
layers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
itemType: 'generation-dialog',
|
||||
dialog: expect.objectContaining({
|
||||
mode: 'spec',
|
||||
status: 'idle',
|
||||
generatedLayerId: 'layer-editor-spec-role-1',
|
||||
specValues: expect.objectContaining({
|
||||
playSetting: '平台跳跃玩法',
|
||||
artStyle: '低多边形卡通',
|
||||
bodyRatio: '4',
|
||||
characterView: '左向三分之二侧身站姿',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows visible titles for character spec, icon spec, and icon spritesheet generation fields', async () => {
|
||||
@@ -2094,10 +2109,11 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
|
||||
const generatedImage = await screen.findByAltText(/画布图片:生成图片/u);
|
||||
const generatedLayerButton = generatedImage.closest('button')!;
|
||||
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
fireEvent.click(generatedLayerButton);
|
||||
expect(generatedLayerButton.className).toContain(
|
||||
'image-canvas-editor__layer--selected',
|
||||
);
|
||||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||||
|
||||
fireEvent.pointerDown(screen.getByLabelText('画布工作区'), {
|
||||
button: 0,
|
||||
@@ -3652,7 +3668,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
.closest('button') as HTMLElement;
|
||||
expect(Number.parseFloat(generatedLayer.style.width)).toBe(1024);
|
||||
expect(Number.parseFloat(generatedLayer.style.height)).toBe(1024);
|
||||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||||
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
|
||||
const metadataCornerButton = screen.getAllByRole('button', {
|
||||
name: /查看生成图片 .*图片信息/,
|
||||
|
||||
@@ -1152,6 +1152,8 @@ export function ImageCanvasEditorView({
|
||||
viewportRef,
|
||||
canvasGenerationDialogsRef,
|
||||
canvasBackgroundColorRef,
|
||||
selectedLayerIdRef,
|
||||
selectedLayerIdsRef,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
@@ -1161,6 +1163,8 @@ export function ImageCanvasEditorView({
|
||||
setProjectRenameValue,
|
||||
setViewport,
|
||||
setLayers,
|
||||
setSelectedLayerId,
|
||||
setSelectedLayerIds,
|
||||
selectSingleLayer,
|
||||
setLayerCounter: (value: number) => {
|
||||
layerCounterRef.current = value;
|
||||
@@ -1170,6 +1174,8 @@ export function ImageCanvasEditorView({
|
||||
}),
|
||||
[
|
||||
applyCanvasBackgroundColor,
|
||||
setSelectedLayerId,
|
||||
setSelectedLayerIds,
|
||||
restoreCanvasGenerationDialogs,
|
||||
selectSingleLayer,
|
||||
setLayers,
|
||||
|
||||
@@ -108,6 +108,27 @@ function createCompletedEditorProjectSnapshot(
|
||||
},
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [
|
||||
{
|
||||
itemType: 'generation-dialog',
|
||||
layerId: 'generation-dialog:generation-dialog-1',
|
||||
resourceId: 'generation-dialog:generation-dialog-1',
|
||||
dialog: {
|
||||
id: 'generation-dialog-1',
|
||||
mode: 'generate',
|
||||
prompt: '后端完成生成器',
|
||||
status: 'idle',
|
||||
composerOpen: false,
|
||||
generatedLayerId: 'layer-generated',
|
||||
placeholder: {
|
||||
x: 42,
|
||||
y: 56,
|
||||
width: 420,
|
||||
height: 420,
|
||||
originalWidth: 420,
|
||||
originalHeight: 420,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
layerId: 'layer-generated',
|
||||
resourceId: 'resource-generated',
|
||||
@@ -121,27 +142,6 @@ function createCompletedEditorProjectSnapshot(
|
||||
zIndex: 1,
|
||||
sourceType: 'generated',
|
||||
},
|
||||
{
|
||||
itemType: 'generation-dialog',
|
||||
layerId: 'generation-dialog:generation-dialog-1',
|
||||
resourceId: 'generation-dialog:generation-dialog-1',
|
||||
dialog: {
|
||||
id: 'generation-dialog-1',
|
||||
mode: 'generate',
|
||||
prompt: '后端完成生成器',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
generatedLayerId: 'layer-generated',
|
||||
placeholder: {
|
||||
x: 42,
|
||||
y: 56,
|
||||
width: 420,
|
||||
height: 420,
|
||||
originalWidth: 420,
|
||||
originalHeight: 420,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
resources: [
|
||||
{
|
||||
@@ -314,7 +314,7 @@ it('merges pending geometry and dialog edits while retaining backend additions a
|
||||
dialog: expect.objectContaining({
|
||||
prompt: '请求在途期间的新提示词',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
composerOpen: false,
|
||||
generatedLayerId: 'layer-generated-by-backend',
|
||||
placeholder: expect.objectContaining({ x: 88, y: 99 }),
|
||||
}),
|
||||
@@ -359,11 +359,14 @@ function ProjectPersistenceHarness({
|
||||
const [projectTitle, setProjectTitle] = useState('');
|
||||
const [projectRenameValue, setProjectRenameValue] = useState('');
|
||||
const [flushCompleted, setFlushCompleted] = useState(false);
|
||||
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
||||
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
|
||||
const layersRef = useRef(layers);
|
||||
const viewportRef = useRef(viewport);
|
||||
const canvasGenerationDialogsRef = useRef(generationDialogs);
|
||||
const canvasBackgroundColorRef = useRef(canvasBackgroundColor);
|
||||
const selectedLayerRef = useRef<string | null>(null);
|
||||
const selectedLayerRef = useRef<string | null>(selectedLayerId);
|
||||
const selectedLayerIdsRef = useRef<string[]>(selectedLayerIds);
|
||||
const layerCounterRef = useRef(0);
|
||||
const openEditorLoginModalRef = useRef(vi.fn());
|
||||
|
||||
@@ -371,8 +374,11 @@ function ProjectPersistenceHarness({
|
||||
viewportRef.current = viewport;
|
||||
canvasGenerationDialogsRef.current = generationDialogs;
|
||||
canvasBackgroundColorRef.current = canvasBackgroundColor;
|
||||
selectedLayerRef.current = selectedLayerId;
|
||||
selectedLayerIdsRef.current = selectedLayerIds;
|
||||
const selectSingleLayer = useCallback((layerId: string | null) => {
|
||||
selectedLayerRef.current = layerId;
|
||||
setSelectedLayerId(layerId);
|
||||
setSelectedLayerIds(layerId ? [layerId] : []);
|
||||
}, []);
|
||||
const setLayerCounter = useCallback((value: number) => {
|
||||
layerCounterRef.current = value;
|
||||
@@ -391,6 +397,8 @@ function ProjectPersistenceHarness({
|
||||
viewportRef,
|
||||
canvasGenerationDialogsRef,
|
||||
canvasBackgroundColorRef,
|
||||
selectedLayerIdRef: selectedLayerRef,
|
||||
selectedLayerIdsRef,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
@@ -400,6 +408,8 @@ function ProjectPersistenceHarness({
|
||||
setProjectRenameValue,
|
||||
setViewport,
|
||||
setLayers,
|
||||
setSelectedLayerId,
|
||||
setSelectedLayerIds,
|
||||
selectSingleLayer,
|
||||
setLayerCounter,
|
||||
restoreCanvasGenerationDialogs: setGenerationDialogs,
|
||||
@@ -439,6 +449,9 @@ function ProjectPersistenceHarness({
|
||||
.join(',')}
|
||||
</span>
|
||||
<span data-testid="selected">{selectedLayerRef.current ?? '-'}</span>
|
||||
<span data-testid="selected-ids">
|
||||
{selectedLayerIdsRef.current.join(',') || '-'}
|
||||
</span>
|
||||
<span data-testid="counter">{layerCounterRef.current}</span>
|
||||
<span data-testid="viewport">
|
||||
{viewport.x},{viewport.y},{viewport.scale}
|
||||
@@ -651,6 +664,28 @@ function ProjectPersistenceHarness({
|
||||
>
|
||||
apply completed project
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedLayerId('layer-generated');
|
||||
setSelectedLayerIds([
|
||||
'layer-generated',
|
||||
'generation-dialog:generation-dialog-1',
|
||||
]);
|
||||
}}
|
||||
>
|
||||
select completed objects
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
persistence.applyProjectSnapshot(
|
||||
createCompletedEditorProjectSnapshot(2),
|
||||
);
|
||||
}}
|
||||
>
|
||||
reapply completed project
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -1183,6 +1218,45 @@ describe('useImageCanvasProjectPersistence', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not steal an empty selection when a same-project completion snapshot arrives', async () => {
|
||||
render(<ProjectPersistenceHarness />);
|
||||
expect(await screen.findByText('editor-project-default')).toBeTruthy();
|
||||
expect(screen.getByTestId('selected').textContent).toBe('-');
|
||||
|
||||
act(() => {
|
||||
screen.getByRole('button', { name: 'apply completed project' }).click();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-generated:resource-generated',
|
||||
);
|
||||
expect(screen.getByTestId('selected').textContent).toBe('-');
|
||||
});
|
||||
|
||||
it('preserves valid layer and generation selections across a same-project snapshot', async () => {
|
||||
render(<ProjectPersistenceHarness />);
|
||||
expect(await screen.findByText('editor-project-default')).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
screen.getByRole('button', { name: 'apply completed project' }).click();
|
||||
});
|
||||
act(() => {
|
||||
screen.getByRole('button', { name: 'select completed objects' }).click();
|
||||
});
|
||||
expect(screen.getByTestId('selected-ids').textContent).toBe(
|
||||
'layer-generated,generation-dialog:generation-dialog-1',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
screen.getByRole('button', { name: 'reapply completed project' }).click();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('selected').textContent).toBe('layer-generated');
|
||||
expect(screen.getByTestId('selected-ids').textContent).toBe(
|
||||
'layer-generated,generation-dialog:generation-dialog-1',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an asynchronous project callback after authority is revoked', async () => {
|
||||
const { rerender } = render(<ProjectPersistenceHarness />);
|
||||
expect(await screen.findByText('editor-project-default')).toBeTruthy();
|
||||
|
||||
@@ -43,6 +43,10 @@ import {
|
||||
type ProjectCoverSnapshotViewportSize,
|
||||
} from './ImageCanvasProjectCoverSnapshotModel';
|
||||
import { createProjectCoverSnapshotBlob } from './ImageCanvasProjectCoverSnapshotRenderer';
|
||||
import {
|
||||
firstSelectedLayerId,
|
||||
normalizeCanvasSelectionIds,
|
||||
} from './ImageCanvasSelectionModel';
|
||||
|
||||
type ProjectResourceOptions = {
|
||||
onCreated?: (resourceId: string) => void;
|
||||
@@ -127,6 +131,7 @@ function mergePendingCanvasLayerLayout(
|
||||
generationFinishedAt: authoritativeDialog.generationFinishedAt,
|
||||
characterAnimationResult:
|
||||
authoritativeDialog.characterAnimationResult,
|
||||
composerOpen: authoritativeDialog.composerOpen,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -195,6 +200,8 @@ type ImageCanvasProjectPersistenceRefs = {
|
||||
viewportRef: RefObject<CanvasViewport>;
|
||||
canvasGenerationDialogsRef: RefObject<CanvasGenerationDialogState[]>;
|
||||
canvasBackgroundColorRef: RefObject<string>;
|
||||
selectedLayerIdRef: RefObject<string | null>;
|
||||
selectedLayerIdsRef: RefObject<string[]>;
|
||||
};
|
||||
|
||||
type ImageCanvasProjectPersistenceSetters = {
|
||||
@@ -202,6 +209,8 @@ type ImageCanvasProjectPersistenceSetters = {
|
||||
setProjectRenameValue: (title: string) => void;
|
||||
setViewport: (viewport: CanvasViewport) => void;
|
||||
setLayers: (layers: CanvasLayer[]) => void;
|
||||
setSelectedLayerId: (layerId: string | null) => void;
|
||||
setSelectedLayerIds: (layerIds: string[]) => void;
|
||||
selectSingleLayer: (layerId: string | null) => void;
|
||||
setLayerCounter: (value: number) => void;
|
||||
restoreCanvasGenerationDialogs: (
|
||||
@@ -541,6 +550,8 @@ export function useImageCanvasProjectPersistence({
|
||||
setProjectRenameValue,
|
||||
setViewport,
|
||||
setLayers,
|
||||
setSelectedLayerId,
|
||||
setSelectedLayerIds,
|
||||
selectSingleLayer,
|
||||
setLayerCounter,
|
||||
restoreCanvasGenerationDialogs,
|
||||
@@ -1183,6 +1194,8 @@ export function useImageCanvasProjectPersistence({
|
||||
: null;
|
||||
const previousAuthoritativeItemIds =
|
||||
authoritativeLayoutItemIdsRef.current;
|
||||
const applyingToCurrentProject =
|
||||
projectIdRef.current === project.projectId;
|
||||
clearPendingProjectLayoutSave();
|
||||
skipNextProjectLayoutSaveRef.current = true;
|
||||
if (projectIdRef.current !== project.projectId) {
|
||||
@@ -1268,7 +1281,30 @@ export function useImageCanvasProjectPersistence({
|
||||
setLayerCounter(hydratedLayers.length);
|
||||
refs.layersRef.current = hydratedLayers;
|
||||
setLayers(hydratedLayers);
|
||||
selectSingleLayer(hydratedLayers[0]?.id ?? null);
|
||||
if (applyingToCurrentProject) {
|
||||
const currentSelectionIds = refs.selectedLayerIdsRef.current.length
|
||||
? refs.selectedLayerIdsRef.current
|
||||
: refs.selectedLayerIdRef.current
|
||||
? [refs.selectedLayerIdRef.current]
|
||||
: [];
|
||||
const nextSelectionIds = normalizeCanvasSelectionIds({
|
||||
selectionIds: currentSelectionIds,
|
||||
layers: hydratedLayers,
|
||||
generationDialogs,
|
||||
});
|
||||
const nextSelectedLayerId = firstSelectedLayerId(nextSelectionIds);
|
||||
refs.selectedLayerIdRef.current = nextSelectedLayerId;
|
||||
refs.selectedLayerIdsRef.current = nextSelectionIds;
|
||||
setSelectedLayerId(nextSelectedLayerId);
|
||||
setSelectedLayerIds(nextSelectionIds);
|
||||
} else {
|
||||
const nextSelectedLayerId = hydratedLayers[0]?.id ?? null;
|
||||
refs.selectedLayerIdRef.current = nextSelectedLayerId;
|
||||
refs.selectedLayerIdsRef.current = nextSelectedLayerId
|
||||
? [nextSelectedLayerId]
|
||||
: [];
|
||||
selectSingleLayer(nextSelectedLayerId);
|
||||
}
|
||||
refs.canvasGenerationDialogsRef.current = generationDialogs;
|
||||
restoreCanvasGenerationDialogs(generationDialogs);
|
||||
applyCanvasBackgroundColor(
|
||||
@@ -1309,6 +1345,8 @@ export function useImageCanvasProjectPersistence({
|
||||
selectSingleLayer,
|
||||
setLayerCounter,
|
||||
setLayers,
|
||||
setSelectedLayerId,
|
||||
setSelectedLayerIds,
|
||||
setProjectRenameValue,
|
||||
setProjectTitle,
|
||||
setViewport,
|
||||
|
||||
Reference in New Issue
Block a user