完美像素刷新后的孤儿占位由归属标记收口
完美像素在 POST 前把 generating 占位落库后走同步 HTTP。刷新页面会丢弃服务端 handler,画布回填不再发生;新页面 hydrate 原样还原 generating,加载期没有任何 对账,占位永久转圈且 Esc 与关闭按钮都消不掉。 写入侧无解:validate_editor_pixel_art_snap_placeholder_exists 要求占位必须已 落库,否则 409,POST 前那句 flush 正是为此存在。改为读取侧收口。 给 dialog 增加 requiresLiveSession,加载时剥离置位且仍为 generating 的占位。 判据是结构性不变量而非时间阈值:这类占位只能由创建它的会话收口,活着的那份始终 在内存里不经过 hydrate,所以从服务端读回来的必然属于已死会话。队列型链路一律不 置位——去除背景恒返回 queueState、图片生成默认入队,它们的 job 在服务端继续跑, 误清会诱发重复提交。 剥离只用于项目首次加载的两个调用点,不下沉进 hydrate:会话内 applyQueuedEditorGenerationProject 也会重新 GET 并套用,那时占位对应的操作正在 进行。提示只指路不断言,因为服务端持久化非事务,前几步可能已成功。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5982,5 +5982,17 @@
|
||||
- 配套修复:对账成功分支补上 `hasCanvasGenerationDialogById` 检查——权威快照里占位消失有两种原因,服务端消费掉或用户在请求期间删除,后者契约要求不应用完成快照、不写历史,成功路径同一处早有这道检查而对账路径漏了。对账同时刷新素材库(新增可选 `refreshAssetLibrary` 贯穿 `ImageCanvasEditorView` → surface → workflow),否则只 GET 项目却让用户核对素材库,他看到的仍是旧列表,不满足契约的「项目 / 素材快照」。占位存活分支刻意不调 `applyProjectSnapshot`:传入的是 `applyGeneratedProjectSnapshot`,其 action 默认值为 `generate-image`,不传 action 会写一条类型错误且受撤销保护的历史,而权威快照此刻与本地一致,套用只会覆盖未保存编辑。
|
||||
- 验证:服务端 `assert_function_occurrence_count` 把标记钉为 4 处,并用顺序断言要求它只出现在 `persist_editor_generated_image_owned` 之后——漏标一处或误标在校验阶段都会失败。客户端新增用例覆盖「带标记的 409 触发对账并刷新素材库」「未带标记的 400 不对账、文案保持服务端原文」「用户删除占位则不应用快照不写历史」。
|
||||
- 文案边界:对账尾句只下指令、不断言素材库已刷新。`refreshAssetLibrary` 在 `canAccessProtectedData` 为 false 时直接 return,读取失败也只在鉴权错误时弹登录框、其余一律吞掉,返回 `Promise<void>` 不带成败信号,且它本身是可选 prop——三种情况下「已刷新」都是假话,会让用户对着旧列表判定「没有派生图,可以重试」,重新走回这条修复要避免的重复创建。刷新照旧调用(成功时用户白赚一份新列表),但文案在刷新失效时也必须成立。
|
||||
- 未覆盖:刷新页面后停在 `generating` 的占位仍无自动收口,需先给 dialog 增加「无 durable job 的 inline 链路」标记,单独立项。客户端 120 秒超时相对服务端 30 秒预算是四倍冗余,未调整。
|
||||
- 未覆盖:刷新页面后停在 `generating` 的占位仍无自动收口(已由 2026-08-03 的 `requiresLiveSession` 条目解决)。客户端 120 秒超时相对服务端 30 秒预算是四倍冗余,未调整。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
## 2026-08-03 完美像素刷新后的孤儿占位由归属标记收口
|
||||
|
||||
- 缺陷:完美像素在 POST 前 `flushProjectPersistence()` 把 `status: 'generating'` 的占位落库,随后走同步 HTTP。此时刷新页面,浏览器断连、handler future 被丢弃,服务端不会走完画布回填;新页面 hydrate 时 `status` 被原样还原(`isGenerationStatus` 认 `generating`),而加载期没有任何对账、轮询或重试 GET,占位就永久停在转圈状态。它还消不掉——Esc 被 `useImageCanvasKeyboardShortcuts` 的 `status === 'generating'` 挡,composer 关闭被 `closeCanvasGenerationComposer` 的同一判断挡。
|
||||
- 归因:序列化设施是存量,但这个组合是本分支首次出现。同类路径逐条比对——去除背景的 `EditorBackgroundRemovalResult` 的 `queueState` 是非可选字段,恒走 durable job,worker 会在服务端替换占位;拆分图集根本不创建占位;图片生成等提交类链路在默认 `ExternalGenerationMode::Queue` 下同样入队,只有显式设 `GENARRATIVE_EXTERNAL_GENERATION_MODE=inline` 的部署才同步执行。完美像素的 handler 353 行全同步,`tokio::spawn` / `enqueue_` 各 0 次,`EditorPixelArtSnapResult` 无 `queueState`,是唯一「持久化 generating + 无 durable job」的链路。
|
||||
- 方案取舍:不能从写入侧解决。`validate_editor_pixel_art_snap_placeholder_exists` 要求占位必须已落库,否则返回 `409`「完美像素画布占位不存在或尚未保存,请重试」——不持久化占位会让每一次完美像素都失败。POST 前那句 flush 正是为满足该门禁而存在。因此只能在读取侧收口,纯前端。
|
||||
- 决策:给 dialog 增加 `requiresLiveSession` 标记,加载时由 `dropDeadInlineGenerationPlaceholders` 剥离置位且仍为 `generating` 的占位。判据是结构性不变量而非时间阈值:这类占位的收口只能由创建它的会话完成,而活着的那一份始终在内存里、永远不经过快照 hydrate,所以凡是从服务端快照读回来的必然属于已死会话。因此不需要时间戳,也不必猜阈值。队列型占位一律不置位——它们的 job 在服务端继续跑,误清会让用户以为操作没发生而重复提交。hydrate 只认布尔 `true`,缺字段的历史占位按队列型处理,不会被误清。
|
||||
- 作用域:剥离只用在项目首次加载的两个调用点(会话缓存与权威快照都要,否则首屏会先闪一个永远转圈的占位)。**不能**下沉进 `hydrateCanvasGenerationDialog`——会话内 `applyQueuedEditorGenerationProject` 也会重新 GET 项目并套用,那时候占位对应的操作正在进行,套用剥离会把自己的活占位清掉。唯一置位点是完美像素占位的创建处。
|
||||
- 提示文案:服务端持久化顺序 `OSS PUT → asset object → project resource → editor asset → 画布回填` 是非事务的,加载时还看到 `generating` 只说明最后一步没做完,前面几步可能已成功。所以不能断言「什么都没发生」,只提示「画布占位已清理,请确认素材库是否已生成派生图」,与同链路的对账文案同一口径。计数用累计值而非布尔——同一会话可能连着切换多个项目,布尔只提示一次。
|
||||
- 已知残留:剥离是本地的,不主动回写。`applyProjectSnapshot` 会置 `skipNextProjectLayoutSaveRef`,加载后的第一次 effect 被消费掉,所以清理要等用户下一次布局变更才随防抖落库;在此之前重复打开会重复提示。刻意不强制回写:那会加剧多标签页问题——B 标签加载时会误判 A 标签正在跑的占位为孤儿,只在本地剥离时 A 的回填仍能成功,一旦立即回写就会让 A 撞上 `409` 占位不存在。多标签页同时编辑同一画布另有 CAS `expected_revision` 兜底,不由本次修复承担。
|
||||
- 验证:`dropDeadInlineGenerationPlaceholders` 四条单测覆盖「剥离已死 inline 占位」「保留队列型占位(缺字段与显式 false 两种)」「保留已终态的 inline 占位与普通图层」「标记经 hydrate 与序列化往返不丢失」——最后一条钉住白名单式 hydrate 漏字段会让标记在一次「加载→保存」后消失。工作流测试新增 `live-session-dialogs` 探针,正向断言完美像素占位置位、反向断言去除背景占位不置位。`vitest src/components/image-editor` 893 通过 / 72 文件,typecheck、eslint、check:encoding 通过。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type {
|
||||
EditorProjectLayerSnapshot,
|
||||
EditorProjectSnapshot,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import {
|
||||
CANVAS_WORLD_ORIGIN,
|
||||
canvasDisplayScaleToViewportScale,
|
||||
canvasDisplayViewportToViewport,
|
||||
createLayerFromAsset,
|
||||
DEFAULT_CANVAS_BACKGROUND_COLOR,
|
||||
dropDeadInlineGenerationPlaceholders,
|
||||
formatCanvasDisplayScalePercent,
|
||||
hydrateCanvasGenerationDialog,
|
||||
hydrateLayer,
|
||||
@@ -993,4 +998,101 @@ describe('ImageCanvasEditorModel', () => {
|
||||
horizontal: 160,
|
||||
});
|
||||
});
|
||||
|
||||
describe('dropDeadInlineGenerationPlaceholders', () => {
|
||||
const buildProject = (
|
||||
layers: EditorProjectLayerSnapshot[],
|
||||
): EditorProjectSnapshot => ({
|
||||
projectId: 'project-1',
|
||||
title: '画布',
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers,
|
||||
resources: [],
|
||||
updatedAt: '2026-08-03T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const buildDialogItem = (
|
||||
id: string,
|
||||
dialog: Record<string, unknown>,
|
||||
): EditorProjectLayerSnapshot =>
|
||||
({
|
||||
itemType: 'generation-dialog',
|
||||
layerId: `generation-dialog:${id}`,
|
||||
resourceId: `generation-dialog:${id}`,
|
||||
dialog: { id, mode: 'quick-edit', prompt: '完美像素', ...dialog },
|
||||
}) as unknown as EditorProjectLayerSnapshot;
|
||||
|
||||
it('drops generating placeholders whose only owner was a dead session', () => {
|
||||
const project = buildProject([
|
||||
buildDialogItem('dead', {
|
||||
status: 'generating',
|
||||
requiresLiveSession: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = dropDeadInlineGenerationPlaceholders(project);
|
||||
|
||||
expect(result.droppedCount).toBe(1);
|
||||
expect(result.project.layers).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps generating placeholders backed by a durable job', () => {
|
||||
// 中文注释:去除背景恒队列、图片生成默认队列,它们的 job 在服务端继续跑,worker 会
|
||||
// 替换占位。刷新后必须原样恢复,误清会让用户以为操作没发生而重复提交。
|
||||
const project = buildProject([
|
||||
buildDialogItem('queued', { status: 'generating' }),
|
||||
buildDialogItem('queued-explicit-false', {
|
||||
status: 'generating',
|
||||
requiresLiveSession: false,
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = dropDeadInlineGenerationPlaceholders(project);
|
||||
|
||||
expect(result.droppedCount).toBe(0);
|
||||
expect(result.project).toBe(project);
|
||||
});
|
||||
|
||||
it('keeps settled inline placeholders and unrelated layout items', () => {
|
||||
const settled = buildDialogItem('settled', {
|
||||
status: 'failed',
|
||||
requiresLiveSession: true,
|
||||
});
|
||||
const imageLayer = {
|
||||
itemType: 'layer',
|
||||
layerId: 'layer-1',
|
||||
resourceId: 'resource-1',
|
||||
} as unknown as EditorProjectLayerSnapshot;
|
||||
const project = buildProject([settled, imageLayer]);
|
||||
|
||||
const result = dropDeadInlineGenerationPlaceholders(project);
|
||||
|
||||
expect(result.droppedCount).toBe(0);
|
||||
expect(result.project.layers).toEqual([settled, imageLayer]);
|
||||
});
|
||||
|
||||
it('round-trips the marker through hydration so reloads can still detect it', () => {
|
||||
// 中文注释:hydrate 是白名单式的,漏掉这个字段会让标记在一次「加载→保存」后消失,
|
||||
// 孤儿占位重新变得不可识别。
|
||||
const hydrated = hydrateCanvasGenerationDialog({
|
||||
id: 'dialog-1',
|
||||
mode: 'quick-edit',
|
||||
prompt: '完美像素',
|
||||
status: 'generating',
|
||||
requiresLiveSession: true,
|
||||
});
|
||||
|
||||
expect(hydrated?.requiresLiveSession).toBe(true);
|
||||
|
||||
const [item] = serializeCanvasLayout({
|
||||
layers: [],
|
||||
canvasGenerationDialogs: [hydrated as CanvasGenerationDialogState],
|
||||
});
|
||||
|
||||
expect(
|
||||
(item as unknown as { dialog: { requiresLiveSession?: boolean } }).dialog
|
||||
.requiresLiveSession,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
EditorAssetLibrarySnapshot,
|
||||
EditorCharacterAnimationGenerationResult,
|
||||
EditorProjectLayerSnapshot,
|
||||
EditorProjectSnapshot,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import type {
|
||||
CanvasAssetKind,
|
||||
@@ -500,6 +501,39 @@ function isCanvasSettingsLayoutItem(
|
||||
return item.itemType === 'canvas-settings';
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文注释:剥离「只能由已死会话收口」的 generating 占位。
|
||||
*
|
||||
* 判据是一条结构性不变量,不是时间阈值:置位了 requiresLiveSession 的占位,其收口只能由
|
||||
* 创建它的页面会话完成;而活着的那一份始终在内存里,永远不经过快照 hydrate。因此凡是从
|
||||
* 服务端快照里读回来的这类 generating 占位,创建它的会话必然已经不在了。
|
||||
*
|
||||
* 只能用在项目首次加载。会话内 applyQueuedEditorGenerationProject 会重新 GET 项目并套用,
|
||||
* 那时候占位对应的操作正在进行,套用本函数会把自己的活占位清掉。
|
||||
*/
|
||||
export function dropDeadInlineGenerationPlaceholders(
|
||||
project: EditorProjectSnapshot,
|
||||
): { project: EditorProjectSnapshot; droppedCount: number } {
|
||||
const nextLayers = project.layers.filter((item) => {
|
||||
if (item.itemType !== 'generation-dialog') {
|
||||
return true;
|
||||
}
|
||||
const dialog =
|
||||
(item as { dialog?: unknown }).dialog &&
|
||||
typeof (item as { dialog?: unknown }).dialog === 'object'
|
||||
? ((item as { dialog?: unknown }).dialog as Record<string, unknown>)
|
||||
: null;
|
||||
return !(
|
||||
dialog?.requiresLiveSession === true && dialog?.status === 'generating'
|
||||
);
|
||||
});
|
||||
const droppedCount = project.layers.length - nextLayers.length;
|
||||
if (droppedCount === 0) {
|
||||
return { project, droppedCount: 0 };
|
||||
}
|
||||
return { project: { ...project, layers: nextLayers }, droppedCount };
|
||||
}
|
||||
|
||||
export function splitCanvasLayoutItems(
|
||||
items: EditorProjectLayerSnapshot[],
|
||||
resourcesById: Map<string, CanvasLayerResourceMetadata> = new Map(),
|
||||
@@ -569,6 +603,9 @@ export function hydrateCanvasGenerationDialog(
|
||||
mode: snapshot.mode,
|
||||
prompt,
|
||||
status: isGenerationStatus(snapshot.status) ? snapshot.status : 'idle',
|
||||
// 中文注释:只认布尔 true。缺字段的历史占位一律视为未置位,按队列型处理原样恢复,
|
||||
// 不会被 dropDeadInlineGenerationPlaceholders 误清。
|
||||
requiresLiveSession: snapshot.requiresLiveSession === true ? true : undefined,
|
||||
composerOpen:
|
||||
typeof snapshot.composerOpen === 'boolean'
|
||||
? snapshot.composerOpen
|
||||
|
||||
@@ -237,6 +237,11 @@ export type GenerateDialogState = {
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
errorMessage?: string;
|
||||
// 中文注释:标记该占位的收口只能由创建它的页面会话完成——链路是同步 HTTP、服务端没有
|
||||
// durable job,进程一死就再没有任何东西会把它推向终态。队列型占位(去除背景恒队列、
|
||||
// 图片生成默认队列)不得置位:它们的 job 在服务端继续跑,worker 会替换占位,刷新后
|
||||
// 必须原样恢复。
|
||||
requiresLiveSession?: boolean;
|
||||
generationStartedAt?: number;
|
||||
generationFinishedAt?: number;
|
||||
placeholder?: {
|
||||
|
||||
@@ -1184,6 +1184,7 @@ export function ImageCanvasEditorView({
|
||||
appendCanvasLayersWithResources,
|
||||
applyProjectSnapshot,
|
||||
flushProjectPersistence,
|
||||
deadInlinePlaceholderDropCount,
|
||||
} = useImageCanvasProjectPersistence({
|
||||
refs: projectPersistenceRefs,
|
||||
setters: projectPersistenceSetters,
|
||||
@@ -1497,6 +1498,17 @@ export function ImageCanvasEditorView({
|
||||
generationSurface.refreshTaskList();
|
||||
}, [generationSurface]);
|
||||
const showGenerationWarning = generationSurface.showGenerationWarning;
|
||||
useEffect(() => {
|
||||
if (deadInlinePlaceholderDropCount === 0) {
|
||||
return;
|
||||
}
|
||||
// 中文注释:服务端持久化顺序是 OSS PUT → asset object → project resource → editor asset
|
||||
// → 画布收口,非事务。加载时还看到 generating 只能说明最后一步没做完,前面几步可能已经
|
||||
// 成功。所以不能断言「什么都没发生」,只能指路让用户自己核对素材库。
|
||||
showGenerationWarning(
|
||||
'上次的完美像素处理未完成,画布占位已清理。请确认素材库是否已生成派生图。',
|
||||
);
|
||||
}, [deadInlinePlaceholderDropCount, showGenerationWarning]);
|
||||
const handleExternalGenerationTasksCompleted = useCallback(
|
||||
(tasks: ExternalGenerationTaskRecord[]) => {
|
||||
if (!projectId || tasks.length === 0) {
|
||||
|
||||
@@ -255,6 +255,12 @@ function GenerationWorkflowHarness({
|
||||
)
|
||||
.join('|') || '-'}
|
||||
</span>
|
||||
<span data-testid="live-session-dialogs">
|
||||
{dialogs.canvasGenerationDialogs
|
||||
.filter((dialog) => dialog.requiresLiveSession === true)
|
||||
.map((dialog) => dialog.id)
|
||||
.join('|') || '-'}
|
||||
</span>
|
||||
<span data-testid="generation-references">
|
||||
{activeDialog?.generationReferences
|
||||
?.map((reference) => reference.label)
|
||||
@@ -2141,6 +2147,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect(screen.getByTestId('generation-dialogs').textContent).toContain(
|
||||
'generation-dialog-1:quick-edit:closed',
|
||||
);
|
||||
// 中文注释:去除背景恒返回 queueState,job 在服务端继续跑,刷新后 worker 会替换占位。
|
||||
// 置位 requiresLiveSession 会让加载期把还在跑的占位清掉,因此必须保持未置位。
|
||||
expect(screen.getByTestId('live-session-dialogs').textContent).toBe('-');
|
||||
expect(removeImageBackgroundMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceImageSrc: 'resource-source',
|
||||
@@ -2240,6 +2249,12 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect(events).toEqual(['flush', 'post']);
|
||||
expect(flushProjectPersistence).toHaveBeenCalledTimes(1);
|
||||
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
|
||||
// 中文注释:完美像素是同步 HTTP、服务端无 durable job,占位的收口只能由本页会话完成。
|
||||
// 标记必须真的落在提交出去的那份占位上——它会随 flush 一起持久化,是刷新后唯一能把
|
||||
// 孤儿占位和队列型占位区分开的依据。漏置位就退回「刷新后永久转圈」。
|
||||
expect(screen.getByTestId('live-session-dialogs').textContent).toBe(
|
||||
'generation-dialog-1',
|
||||
);
|
||||
expect(snapImageToPerfectPixelsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceImageSrc: 'generated-images/editor/source.png',
|
||||
|
||||
@@ -1800,6 +1800,11 @@ export function useImageCanvasGenerationWorkflow({
|
||||
},
|
||||
}),
|
||||
composerOpen: false,
|
||||
// 中文注释:完美像素是同步 HTTP,服务端不建 durable job(见 snap_editor_image_to_pixel_art
|
||||
// 无 enqueue / spawn)。刷新掉本页会话后,没有任何东西会把这个占位推向终态,而
|
||||
// 服务端 409 门禁又要求占位必须先落库,所以只能在读取侧收口:置位后由项目首次
|
||||
// 加载时的 dropDeadInlineGenerationPlaceholders 清掉。
|
||||
requiresLiveSession: true,
|
||||
});
|
||||
perfectPixelDialogId = placement.dialogId;
|
||||
if (!placement.placeholder) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
canvasDisplayViewportToViewport,
|
||||
type CanvasLayerResourceMetadata,
|
||||
DEFAULT_CANVAS_BACKGROUND_COLOR,
|
||||
dropDeadInlineGenerationPlaceholders,
|
||||
hydrateLayer,
|
||||
isInlineEditorMediaSource,
|
||||
serializeCanvasLayout,
|
||||
@@ -430,6 +431,10 @@ export function useImageCanvasProjectPersistence({
|
||||
const coverSnapshotViewportSizeRef = useRef(canvasSize);
|
||||
const [projectId, setProjectId] = useState<string | null>(null);
|
||||
const [isProjectReady, setIsProjectReady] = useState(false);
|
||||
// 中文注释:累计而不是布尔——同一次会话里可能连着切换多个项目,每个都可能留有孤儿占位,
|
||||
// 布尔只会提示一次。调用方以计数变化为触发条件。
|
||||
const [deadInlinePlaceholderDropCount, setDeadInlinePlaceholderDropCount] =
|
||||
useState(0);
|
||||
const {
|
||||
setProjectTitle,
|
||||
setProjectRenameValue,
|
||||
@@ -1193,17 +1198,31 @@ export function useImageCanvasProjectPersistence({
|
||||
currentUserId,
|
||||
);
|
||||
if (cachedProject) {
|
||||
applyProjectSnapshot(cachedProject.project, { authoritative: false });
|
||||
// 中文注释:会话缓存写于占位落库之后,同样可能带着已死会话的 generating 占位,
|
||||
// 必须和权威快照走同一道剥离,否则首屏会先闪一个永远转圈的占位。
|
||||
applyProjectSnapshot(
|
||||
dropDeadInlineGenerationPlaceholders(cachedProject.project).project,
|
||||
{ authoritative: false },
|
||||
);
|
||||
}
|
||||
const loadProject = projectIdFromQuery
|
||||
? loadEditorProject(projectIdFromQuery)
|
||||
: loadOrCreateRecentEditorProject();
|
||||
|
||||
loadProject
|
||||
.then((project) => {
|
||||
.then((loadedProject) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
// 中文注释:只在这里剥离。会话内 applyQueuedEditorGenerationProject 也会重新 GET
|
||||
// 项目并套用,那时候占位对应的操作正在进行,套用剥离会把自己的活占位清掉。
|
||||
const { project, droppedCount } =
|
||||
dropDeadInlineGenerationPlaceholders(loadedProject);
|
||||
if (droppedCount > 0) {
|
||||
setDeadInlinePlaceholderDropCount(
|
||||
(currentCount) => currentCount + droppedCount,
|
||||
);
|
||||
}
|
||||
const projectIsAuthoritative = applyProjectSnapshot(project, {
|
||||
allowProjectSwitch: true,
|
||||
});
|
||||
@@ -1327,5 +1346,6 @@ export function useImageCanvasProjectPersistence({
|
||||
appendCanvasLayersWithResources,
|
||||
applyProjectSnapshot,
|
||||
flushProjectPersistence,
|
||||
deadInlinePlaceholderDropCount,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user