Skip to content

Commit fb4b4ee

Browse files
refactor(ui): canvas autoswitch logic
Simplify the canvas auto-switch logic to not rely on the preview images loading. This fixes an issue where offscreen preview images didn't get auto-switched to. Images are now loaded directly.
1 parent 8d15686 commit fb4b4ee

File tree

4 files changed

+64
-141
lines changed

4 files changed

+64
-141
lines changed

invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,6 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, index }: Props) =>
6464
}
6565
}, [autoSwitch, dispatch]);
6666

67-
const onLoad = useCallback(() => {
68-
ctx.onImageLoad(item.item_id);
69-
}, [ctx, item.item_id]);
70-
7167
return (
7268
<Flex
7369
id={getQueueItemElementId(index)}
@@ -77,7 +73,7 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, index }: Props) =>
7773
onDoubleClick={onDoubleClick}
7874
>
7975
<QueueItemStatusLabel item={item} position="absolute" margin="auto" />
80-
{imageDTO && <DndImage imageDTO={imageDTO} onLoad={onLoad} asThumbnail position="absolute" />}
76+
{imageDTO && <DndImage imageDTO={imageDTO} asThumbnail position="absolute" />}
8177
{!imageLoaded && <QueueItemProgressImage itemId={item.item_id} position="absolute" />}
8278
<QueueItemNumber number={index + 1} position="absolute" top={0} left={1} />
8379
<QueueItemCircularProgress itemId={item.item_id} status={item.status} position="absolute" top={1} right={2} />

invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx

Lines changed: 63 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useStore } from '@nanostores/react';
22
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
33
import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
4+
import { loadImage } from 'features/controlLayers/konva/util';
45
import { selectStagingAreaAutoSwitch } from 'features/controlLayers/store/canvasSettingsSlice';
56
import {
67
buildSelectCanvasQueueItems,
@@ -99,7 +100,6 @@ type CanvasSessionContextValue = {
99100
selectPrev: () => void;
100101
selectFirst: () => void;
101102
selectLast: () => void;
102-
onImageLoad: (itemId: number) => void;
103103
discard: (itemId: number) => void;
104104
discardAll: () => void;
105105
};
@@ -127,23 +127,12 @@ export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildre
127127
*/
128128
const $lastCompletedItemId = useState(() => atom<number | null>(null))[0];
129129

130-
/**
131-
* Track the last started item. Used to implement autoswitch.
132-
*/
133-
const $lastStartedItemId = useState(() => atom<number | null>(null))[0];
134-
135130
/**
136131
* Manually-synced atom containing queue items for the current session. This is populated from the RTK Query cache
137132
* and kept in sync with it via a redux subscription.
138133
*/
139134
const $items = useState(() => atom<S['SessionQueueItem'][]>([]))[0];
140135

141-
/**
142-
* An internal flag used to work around race conditions with auto-switch switching to queue items before their
143-
* output images have fully loaded.
144-
*/
145-
const $lastLoadedItemId = useState(() => atom<number | null>(null))[0];
146-
147136
/**
148137
* An ephemeral store of progress events and images for all items in the current session.
149138
*/
@@ -282,30 +271,6 @@ export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildre
282271
$selectedItemId.set(last.item_id);
283272
}, [$items, $selectedItemId]);
284273

285-
const onImageLoad = useCallback(
286-
(itemId: number) => {
287-
const progressData = $progressData.get();
288-
const current = progressData[itemId];
289-
if (current) {
290-
const next = { ...current, imageLoaded: true };
291-
$progressData.setKey(itemId, next);
292-
} else {
293-
$progressData.setKey(itemId, {
294-
...getInitialProgressData(itemId),
295-
imageLoaded: true,
296-
});
297-
}
298-
if (
299-
$lastCompletedItemId.get() === itemId &&
300-
selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish'
301-
) {
302-
$selectedItemId.set(itemId);
303-
$lastCompletedItemId.set(null);
304-
}
305-
},
306-
[$lastCompletedItemId, $progressData, $selectedItemId, store]
307-
);
308-
309274
// Set up socket listeners
310275
useEffect(() => {
311276
if (!socket) {
@@ -324,10 +289,19 @@ export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildre
324289
return;
325290
}
326291
if (data.status === 'completed') {
292+
/**
293+
* There is an unpleasant bit of indirection here. When an item is completed, and auto-switch is set to
294+
* switch_on_finish, we want to load the image and switch to it. In this socket handler, we don't have
295+
* access to the full queue item, which we need to get the output image and load it. We get the full
296+
* queue items as part of the list query, so it's rather inefficient to fetch it again here.
297+
*
298+
* To reduce the number of extra network requests, we instead store this item as the last completed item.
299+
* Then in the progress data sync effect, we process the queue item load its image.
300+
*/
327301
$lastCompletedItemId.set(data.item_id);
328302
}
329-
if (data.status === 'in_progress') {
330-
$lastStartedItemId.set(data.item_id);
303+
if (data.status === 'in_progress' && selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_start') {
304+
$selectedItemId.set(data.item_id);
331305
}
332306
};
333307

@@ -338,7 +312,7 @@ export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildre
338312
socket.off('invocation_progress', onProgress);
339313
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
340314
};
341-
}, [$lastCompletedItemId, $lastStartedItemId, $progressData, $selectedItemId, sessionId, socket]);
315+
}, [$progressData, $selectedItemId, sessionId, socket, $lastCompletedItemId, store]);
342316

343317
// Set up state subscriptions and effects
344318
useEffect(() => {
@@ -357,41 +331,32 @@ export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildre
357331
});
358332

359333
// Handle cases that could result in a nonexistent queue item being selected.
360-
const unsubEnsureSelectedItemIdExists = effect(
361-
[$items, $selectedItemId, $lastStartedItemId],
362-
(items, selectedItemId, lastStartedItemId) => {
363-
if (items.length === 0) {
364-
// If there are no items, cannot have a selected item.
365-
$selectedItemId.set(null);
366-
} else if (selectedItemId === null && items.length > 0) {
367-
// If there is no selected item but there are items, select the first one.
368-
$selectedItemId.set(items[0]?.item_id ?? null);
369-
return;
370-
} else if (
371-
selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_start' &&
372-
items.findIndex(({ item_id }) => item_id === lastStartedItemId) !== -1
373-
) {
374-
$selectedItemId.set(lastStartedItemId);
375-
$lastStartedItemId.set(null);
376-
} else if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
377-
// If an item is selected and it is not in the list of items, un-set it. This effect will run again and we'll
378-
// the above case, selecting the first item if there are any.
379-
let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId);
380-
if (prevIndex >= items.length) {
381-
prevIndex = items.length - 1;
382-
}
383-
const nextItem = items[prevIndex];
384-
$selectedItemId.set(nextItem?.item_id ?? null);
334+
const unsubEnsureSelectedItemIdExists = effect([$items, $selectedItemId], (items, selectedItemId) => {
335+
if (items.length === 0) {
336+
// If there are no items, cannot have a selected item.
337+
$selectedItemId.set(null);
338+
} else if (selectedItemId === null && items.length > 0) {
339+
// If there is no selected item but there are items, select the first one.
340+
$selectedItemId.set(items[0]?.item_id ?? null);
341+
return;
342+
} else if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
343+
// If an item is selected and it is not in the list of items, un-set it. This effect will run again and we'll
344+
// the above case, selecting the first item if there are any.
345+
let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId);
346+
if (prevIndex >= items.length) {
347+
prevIndex = items.length - 1;
385348
}
349+
const nextItem = items[prevIndex];
350+
$selectedItemId.set(nextItem?.item_id ?? null);
351+
}
386352

387-
if (items !== _prevItems) {
388-
_prevItems = items;
389-
}
353+
if (items !== _prevItems) {
354+
_prevItems = items;
390355
}
391-
);
356+
});
392357

393-
// Clean up the progress data when a queue item is discarded.
394-
const unsubCleanUpProgressData = $items.subscribe(async (items) => {
358+
// Sync progress data - remove canceled/failed items, update progress data with new images, and load images
359+
const unsubSyncProgressData = $items.subscribe(async (items) => {
395360
const progressData = $progressData.get();
396361

397362
const toDelete: number[] = [];
@@ -418,36 +383,34 @@ export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildre
418383
for (const item of items) {
419384
const datum = progressData[item.item_id];
420385

421-
if (datum) {
422-
if (datum.imageDTO) {
423-
continue;
424-
}
425-
const outputImageName = getOutputImageName(item);
426-
if (!outputImageName) {
427-
continue;
428-
}
429-
const imageDTO = await getImageDTOSafe(outputImageName);
430-
if (!imageDTO) {
431-
continue;
432-
}
433-
toUpdate.push({
434-
...datum,
435-
imageDTO,
436-
});
437-
} else {
438-
const outputImageName = getOutputImageName(item);
439-
if (!outputImageName) {
440-
continue;
441-
}
442-
const imageDTO = await getImageDTOSafe(outputImageName);
443-
if (!imageDTO) {
444-
continue;
445-
}
446-
toUpdate.push({
447-
...getInitialProgressData(item.item_id),
448-
imageDTO,
386+
if (datum?.imageDTO) {
387+
continue;
388+
}
389+
const outputImageName = getOutputImageName(item);
390+
if (!outputImageName) {
391+
continue;
392+
}
393+
const imageDTO = await getImageDTOSafe(outputImageName);
394+
if (!imageDTO) {
395+
continue;
396+
}
397+
398+
// This is the load logic mentioned in the comment in the QueueItemStatusChangedEvent handler above.
399+
if (
400+
$lastCompletedItemId.get() === item.item_id &&
401+
selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish'
402+
) {
403+
loadImage(imageDTO.image_url, true).then(() => {
404+
$selectedItemId.set(item.item_id);
405+
$lastCompletedItemId.set(null);
449406
});
450407
}
408+
409+
toUpdate.push({
410+
...getInitialProgressData(item.item_id),
411+
...datum,
412+
imageDTO,
413+
});
451414
}
452415

453416
for (const itemId of toDelete) {
@@ -459,24 +422,6 @@ export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildre
459422
}
460423
});
461424

462-
// We only want to auto-switch to completed queue items once their images have fully loaded to prevent flashes
463-
// of fallback content and/or progress images. The only surefire way to determine when images have fully loaded
464-
// is via the image elements' `onLoad` callback. Images set `$lastLoadedItemId` to their queue item ID in their
465-
// `onLoad` handler, and we listen for that here. If auto-switch is enabled, we then switch the to the item.
466-
//
467-
// TODO: This isn't perfect... we set $lastLoadedItemId in the mini preview component, but the full view
468-
// component still needs to retrieve the image from the browser cache... can result in a flash of the progress
469-
// image...
470-
const unsubHandleAutoSwitch = $lastLoadedItemId.listen((lastLoadedItemId) => {
471-
if (lastLoadedItemId === null) {
472-
return;
473-
}
474-
if (selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish') {
475-
$selectedItemId.set(lastLoadedItemId);
476-
}
477-
$lastLoadedItemId.set(null);
478-
});
479-
480425
// Create an RTK Query subscription. Without this, the query cache selector will never return anything bc RTK
481426
// doesn't know we care about it.
482427
const { unsubscribe: unsubQueueItemsQuery } = store.dispatch(
@@ -485,25 +430,15 @@ export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildre
485430

486431
// Clean up all subscriptions and top-level (i.e. non-computed/derived state)
487432
return () => {
488-
unsubHandleAutoSwitch();
489433
unsubQueueItemsQuery();
490434
unsubReduxSyncToItemsAtom();
491435
unsubEnsureSelectedItemIdExists();
492-
unsubCleanUpProgressData();
436+
unsubSyncProgressData();
493437
$items.set([]);
494438
$progressData.set({});
495439
$selectedItemId.set(null);
496440
};
497-
}, [
498-
$items,
499-
$lastLoadedItemId,
500-
$lastStartedItemId,
501-
$progressData,
502-
$selectedItemId,
503-
selectQueueItems,
504-
sessionId,
505-
store,
506-
]);
441+
}, [$items, $progressData, $selectedItemId, selectQueueItems, sessionId, store, $lastCompletedItemId]);
507442

508443
const value = useMemo<CanvasSessionContextValue>(
509444
() => ({
@@ -520,7 +455,6 @@ export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildre
520455
selectPrev,
521456
selectFirst,
522457
selectLast,
523-
onImageLoad,
524458
discard,
525459
discardAll,
526460
}),
@@ -538,7 +472,6 @@ export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildre
538472
selectPrev,
539473
selectFirst,
540474
selectLast,
541-
onImageLoad,
542475
discard,
543476
discardAll,
544477
]

invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
256256
}
257257

258258
this.konva.group.visible(shouldShowStagedImage && this.$isStaging.get());
259-
console.log({ isPending, isStaging: this.$isStaging.get(), shouldShowStagedImage, imageSrc });
260259
} finally {
261260
release();
262261
}

invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,6 @@ export const buildSelectCanvasQueueItems = (sessionId: string) =>
8787
);
8888
}
8989
);
90-
export const useCanvasQueueItems = () => {
91-
const sessionId = useAppSelector(selectCanvasSessionId);
92-
const selector = useMemo(() => buildSelectCanvasQueueItems(sessionId), [sessionId]);
93-
return useAppSelector(selector);
94-
};
9590

9691
export const buildSelectIsStaging = (sessionId: string) =>
9792
createSelector([buildSelectCanvasQueueItems(sessionId)], (queueItems) => {

0 commit comments

Comments
 (0)