mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-15 06:18:03 -05:00
refactor(ui): canvas flow (wip)
This commit is contained in:
@@ -5,6 +5,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
|
||||
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
|
||||
import { withResult, withResultAsync } from 'common/util/result';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { canvasSessionStarted, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
|
||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
|
||||
@@ -115,6 +116,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
|
||||
try {
|
||||
await req.unwrap();
|
||||
if (!selectCanvasSessionType(state)) {
|
||||
dispatch(canvasSessionStarted({ sessionType: 'simple' }));
|
||||
}
|
||||
log.debug(parseify({ batchConfig: prepareBatchResult.value }), 'Enqueued batch');
|
||||
} catch (error) {
|
||||
log.error({ error: serializeError(error as Error) }, 'Failed to enqueue batch');
|
||||
|
||||
@@ -9,7 +9,7 @@ import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/contr
|
||||
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
|
||||
import {
|
||||
canvasStagingAreaPersistConfig,
|
||||
canvasStagingAreaSlice,
|
||||
canvasSessionSlice,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
|
||||
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
@@ -65,7 +65,7 @@ const allReducers = {
|
||||
[stylePresetSlice.name]: stylePresetSlice.reducer,
|
||||
[paramsSlice.name]: paramsSlice.reducer,
|
||||
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
|
||||
[canvasStagingAreaSlice.name]: canvasStagingAreaSlice.reducer,
|
||||
[canvasSessionSlice.name]: canvasSessionSlice.reducer,
|
||||
[lorasSlice.name]: lorasSlice.reducer,
|
||||
[workflowLibrarySlice.name]: workflowLibrarySlice.reducer,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Button, ContextMenu, Flex, IconButton, Image, Menu, MenuButton, MenuList, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
|
||||
@@ -18,17 +19,32 @@ import { StagingAreaToolbar } from 'features/controlLayers/components/StagingAre
|
||||
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
|
||||
import { Transform } from 'features/controlLayers/components/Transform/Transform';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { newCanvasSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import { canvasReset, newAdvancedCanvasSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import {
|
||||
selectCanvasSessionType,
|
||||
selectIsStaging,
|
||||
selectSelectedImage,
|
||||
selectStagedImageIndex,
|
||||
selectStagedImages,
|
||||
stagingAreaImageSelected,
|
||||
stagingAreaImageStaged,
|
||||
stagingAreaNextStagedImageSelected,
|
||||
stagingAreaPrevStagedImageSelected,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectIsCanvasEmpty, selectIsSessionStarted } from 'features/controlLayers/store/selectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { isImageField, type ProgressImage } from 'features/nodes/types/common';
|
||||
import { isCanvasOutputEvent } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import type { Atom } from 'nanostores';
|
||||
import { atom } from 'nanostores';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||
import { assert } from 'tsafe';
|
||||
import { getImageDTOSafe } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO, S } from 'services/api/types';
|
||||
import { $socket } from 'services/events/stores';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert, objectEntries } from 'tsafe';
|
||||
|
||||
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
|
||||
|
||||
@@ -50,22 +66,21 @@ const MenuContent = memo(() => {
|
||||
MenuContent.displayName = 'MenuContent';
|
||||
|
||||
export const CanvasMainPanelContent = memo(() => {
|
||||
const isCanvasEmpty = useAppSelector(selectIsCanvasEmpty);
|
||||
const isSessionStarted = useAppSelector(selectIsSessionStarted);
|
||||
const sessionType = useAppSelector(selectCanvasSessionType);
|
||||
|
||||
if (!isSessionStarted) {
|
||||
if (sessionType === null) {
|
||||
return <NoActiveSession />;
|
||||
}
|
||||
|
||||
if (isSessionStarted && isCanvasEmpty) {
|
||||
if (sessionType === 'simple') {
|
||||
return <SimpleActiveSession />;
|
||||
}
|
||||
|
||||
if (isSessionStarted && !isCanvasEmpty) {
|
||||
if (sessionType === 'advanced') {
|
||||
return <CanvasActiveSession />;
|
||||
}
|
||||
|
||||
assert(false);
|
||||
assert<Equals<never, typeof sessionType>>(false, 'Unexpected sessionType');
|
||||
});
|
||||
|
||||
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
|
||||
@@ -73,7 +88,7 @@ CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
|
||||
const NoActiveSession = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const newSesh = useCallback(() => {
|
||||
dispatch(newCanvasSessionRequested());
|
||||
dispatch(newAdvancedCanvasSessionRequested());
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
@@ -108,26 +123,189 @@ const NoActiveSession = memo(() => {
|
||||
);
|
||||
});
|
||||
NoActiveSession.displayName = 'NoActiveSession';
|
||||
|
||||
type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
|
||||
|
||||
const SimpleActiveSession = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const selectedImage = useAppSelector(selectSelectedImage);
|
||||
const stagedImages = useAppSelector(selectStagedImages);
|
||||
const socket = useStore($socket);
|
||||
const [$progressImage] = useState(() => atom<EphemeralProgressImage | null>(null));
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
const onInvocationProgress = (event: S['InvocationProgressEvent']) => {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
if (event.origin !== 'canvas') {
|
||||
return;
|
||||
}
|
||||
if (!event.image) {
|
||||
return;
|
||||
}
|
||||
$progressImage.set({ sessionId: event.session_id, image: event.image });
|
||||
};
|
||||
const onInvocationComplete = async (event: S['InvocationCompleteEvent']) => {
|
||||
const progressImage = $progressImage.get();
|
||||
if (!progressImage) {
|
||||
return;
|
||||
}
|
||||
if (progressImage.sessionId !== event.session_id) {
|
||||
return;
|
||||
}
|
||||
if (!isCanvasOutputEvent(event)) {
|
||||
return;
|
||||
}
|
||||
let imageDTO: ImageDTO | null = null;
|
||||
for (const [_name, value] of objectEntries(event.result)) {
|
||||
if (isImageField(value)) {
|
||||
imageDTO = await getImageDTOSafe(value.image_name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
flushSync(() => {
|
||||
dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
|
||||
});
|
||||
$progressImage.set(null);
|
||||
};
|
||||
|
||||
const onQueueItemStatusChanged = (event: S['QueueItemStatusChangedEvent']) => {
|
||||
const progressImage = $progressImage.get();
|
||||
if (!progressImage) {
|
||||
return;
|
||||
}
|
||||
if (progressImage.sessionId !== event.session_id) {
|
||||
return;
|
||||
}
|
||||
if (event.status !== 'canceled' && event.status !== 'failed') {
|
||||
return;
|
||||
}
|
||||
$progressImage.set(null);
|
||||
};
|
||||
console.log('SUB session preview image listeners');
|
||||
socket.on('invocation_progress', onInvocationProgress);
|
||||
socket.on('invocation_complete', onInvocationComplete);
|
||||
socket.on('queue_item_status_changed', onQueueItemStatusChanged);
|
||||
|
||||
return () => {
|
||||
console.log('UNSUB session preview image listeners');
|
||||
socket.off('invocation_progress', onInvocationProgress);
|
||||
socket.off('invocation_complete', onInvocationComplete);
|
||||
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
|
||||
};
|
||||
}, [$progressImage, dispatch, socket]);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
dispatch(canvasReset());
|
||||
}, [dispatch]);
|
||||
|
||||
const selectNext = useCallback(() => {
|
||||
dispatch(stagingAreaNextStagedImageSelected());
|
||||
}, [dispatch]);
|
||||
|
||||
useHotkeys(['right'], selectNext, { preventDefault: true }, [selectNext]);
|
||||
|
||||
const selectPrev = useCallback(() => {
|
||||
dispatch(stagingAreaPrevStagedImageSelected());
|
||||
}, [dispatch]);
|
||||
|
||||
useHotkeys(['left'], selectPrev, { preventDefault: true }, [selectPrev]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
Simple Session (staging view) {isStaging && 'STAGING'}
|
||||
</Text>
|
||||
{selectedImage && <Image src={selectedImage.imageDTO.image_url} />}
|
||||
<Flex gap={2} maxW="full" overflow="scroll">
|
||||
{stagedImages.map(({ imageDTO }) => (
|
||||
<Image key={imageDTO.image_name} maxW={108} src={imageDTO.thumbnail_url} />
|
||||
))}
|
||||
<Flex>
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
Simple Session (staging view) {isStaging && 'STAGING'}
|
||||
</Text>
|
||||
<Button onClick={onReset}>reset</Button>
|
||||
</Flex>
|
||||
<SelectedImage $progressImage={$progressImage} />
|
||||
<SessionImages />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
SimpleActiveSession.displayName = 'SimpleActiveSession';
|
||||
|
||||
const SelectedImage = memo(({ $progressImage }: { $progressImage: Atom<EphemeralProgressImage | null> }) => {
|
||||
const progressImage = useStore($progressImage);
|
||||
const selectedImage = useAppSelector(selectSelectedImage);
|
||||
|
||||
if (progressImage) {
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="center" minH={0} minW={0}>
|
||||
<Image
|
||||
objectFit="contain"
|
||||
maxH="full"
|
||||
maxW="full"
|
||||
src={progressImage.image.dataURL}
|
||||
width={progressImage.image.width}
|
||||
height={progressImage.image.height}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedImage) {
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="center" minH={0} minW={0}>
|
||||
<Image
|
||||
objectFit="contain"
|
||||
maxH="full"
|
||||
maxW="full"
|
||||
src={selectedImage.imageDTO.image_url}
|
||||
width={selectedImage.imageDTO.width}
|
||||
height={selectedImage.imageDTO.height}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text>No images</Text>;
|
||||
});
|
||||
SelectedImage.displayName = 'SelectedImage';
|
||||
|
||||
const SessionImages = memo(() => {
|
||||
const stagedImages = useAppSelector(selectStagedImages);
|
||||
return (
|
||||
<Flex gap={2} h={108} maxW="full" overflow="scroll">
|
||||
{stagedImages.map(({ imageDTO }, index) => (
|
||||
<SessionImage key={imageDTO.image_name} index={index} imageDTO={imageDTO} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
SessionImages.displayName = 'SessionImages';
|
||||
|
||||
const sx = {
|
||||
'&[data-is-selected="false"]': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
} satisfies SystemStyleObject;
|
||||
const SessionImage = memo(({ index, imageDTO }: { index: number; imageDTO: ImageDTO }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedImageIndex = useAppSelector(selectStagedImageIndex);
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(stagingAreaImageSelected({ index }));
|
||||
}, [dispatch, index]);
|
||||
return (
|
||||
<Image
|
||||
maxW={108}
|
||||
src={imageDTO.image_url}
|
||||
fallbackSrc={imageDTO.thumbnail_url}
|
||||
onClick={onClick}
|
||||
data-is-selected={selectedImageIndex === index}
|
||||
sx={sx}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SessionImage.displayName = 'SessionImage';
|
||||
|
||||
const CanvasActiveSession = memo(() => {
|
||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text }
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
|
||||
import { newAdvancedCanvasSessionRequested, newSimpleCanvasSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import {
|
||||
selectSystemShouldConfirmOnNewSession,
|
||||
shouldConfirmOnNewSessionToggled,
|
||||
@@ -20,7 +20,7 @@ export const useNewGallerySession = () => {
|
||||
const newSessionDialog = useNewGallerySessionDialog();
|
||||
|
||||
const newGallerySessionImmediate = useCallback(() => {
|
||||
dispatch(newGallerySessionRequested());
|
||||
dispatch(newSimpleCanvasSessionRequested());
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -41,7 +41,7 @@ export const useNewCanvasSession = () => {
|
||||
const newSessionDialog = useNewCanvasSessionDialog();
|
||||
|
||||
const newCanvasSessionImmediate = useCallback(() => {
|
||||
dispatch(newCanvasSessionRequested());
|
||||
dispatch(newAdvancedCanvasSessionRequested());
|
||||
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
const { x, y } = this.manager.stateApi.getBbox().rect;
|
||||
const shouldShowStagedImage = this.$shouldShowStagedImage.get();
|
||||
|
||||
this.selectedImage = stagingArea.stagedImages[stagingArea.selectedStagedImageIndex] ?? null;
|
||||
this.selectedImage = stagingArea.images[stagingArea.selectedImageIndex] ?? null;
|
||||
this.konva.group.position({ x, y });
|
||||
|
||||
if (this.selectedImage) {
|
||||
|
||||
@@ -2,6 +2,6 @@ import { createAction, isAnyOf } from '@reduxjs/toolkit';
|
||||
|
||||
// Needed to split this from canvasSlice.ts to avoid circular dependencies
|
||||
export const canvasReset = createAction('canvas/canvasReset');
|
||||
export const newGallerySessionRequested = createAction('canvas/newGallerySessionRequested');
|
||||
export const newCanvasSessionRequested = createAction('canvas/newCanvasSessionRequested');
|
||||
export const newSessionRequested = isAnyOf(newGallerySessionRequested, newCanvasSessionRequested);
|
||||
export const newSimpleCanvasSessionRequested = createAction('canvas/newSimpleCanvasSessionRequested');
|
||||
export const newAdvancedCanvasSessionRequested = createAction('canvas/newAdvancedCanvasSessionRequested');
|
||||
export const newSessionRequested = isAnyOf(newSimpleCanvasSessionRequested, newAdvancedCanvasSessionRequested);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
|
||||
import { newAdvancedCanvasSessionRequested, newSimpleCanvasSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
|
||||
type CanvasSettingsState = {
|
||||
@@ -158,10 +158,10 @@ export const canvasSettingsSlice = createSlice({
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(newGallerySessionRequested, (state) => {
|
||||
builder.addCase(newSimpleCanvasSessionRequested, (state) => {
|
||||
state.sendToCanvas = false;
|
||||
});
|
||||
builder.addCase(newCanvasSessionRequested, (state) => {
|
||||
builder.addCase(newAdvancedCanvasSessionRequested, (state) => {
|
||||
state.sendToCanvas = true;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -7,8 +7,8 @@ import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMul
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
canvasReset,
|
||||
newCanvasSessionRequested,
|
||||
newGallerySessionRequested,
|
||||
newAdvancedCanvasSessionRequested,
|
||||
newSimpleCanvasSessionRequested,
|
||||
} from 'features/controlLayers/store/actions';
|
||||
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
|
||||
import {
|
||||
@@ -1806,12 +1806,11 @@ export const canvasSlice = createSlice({
|
||||
syncScaledSize(state);
|
||||
}
|
||||
});
|
||||
builder.addCase(newGallerySessionRequested, (state) => {
|
||||
builder.addCase(newSimpleCanvasSessionRequested, (state) => {
|
||||
return resetState(state);
|
||||
});
|
||||
builder.addCase(newCanvasSessionRequested, (state) => {
|
||||
builder.addCase(newAdvancedCanvasSessionRequested, (state) => {
|
||||
const newState = resetState(state);
|
||||
newState.isSessionStarted = true;
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,51 +1,73 @@
|
||||
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import {
|
||||
canvasReset,
|
||||
newAdvancedCanvasSessionRequested,
|
||||
newSimpleCanvasSessionRequested,
|
||||
} from 'features/controlLayers/store/actions';
|
||||
import type { StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
|
||||
|
||||
import { newSessionRequested } from './actions';
|
||||
|
||||
type CanvasStagingAreaState = {
|
||||
stagedImages: StagingAreaImage[];
|
||||
selectedStagedImageIndex: number;
|
||||
sessionType: 'simple' | 'advanced' | null;
|
||||
images: StagingAreaImage[];
|
||||
selectedImageIndex: number;
|
||||
};
|
||||
|
||||
const initialState: CanvasStagingAreaState = {
|
||||
stagedImages: [],
|
||||
selectedStagedImageIndex: 0,
|
||||
sessionType: null,
|
||||
images: [],
|
||||
selectedImageIndex: 0,
|
||||
};
|
||||
|
||||
export const canvasStagingAreaSlice = createSlice({
|
||||
name: 'canvasStagingArea',
|
||||
export const canvasSessionSlice = createSlice({
|
||||
name: 'canvasSession',
|
||||
initialState,
|
||||
reducers: {
|
||||
stagingAreaImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
|
||||
const { stagingAreaImage } = action.payload;
|
||||
state.stagedImages.push(stagingAreaImage);
|
||||
state.selectedStagedImageIndex = state.stagedImages.length - 1;
|
||||
state.images.push(stagingAreaImage);
|
||||
state.selectedImageIndex = state.images.length - 1;
|
||||
},
|
||||
stagingAreaImageSelected: (state, action: PayloadAction<{ index: number }>) => {
|
||||
const { index } = action.payload;
|
||||
state.selectedImageIndex = index;
|
||||
},
|
||||
stagingAreaNextStagedImageSelected: (state) => {
|
||||
state.selectedStagedImageIndex = (state.selectedStagedImageIndex + 1) % state.stagedImages.length;
|
||||
state.selectedImageIndex = (state.selectedImageIndex + 1) % state.images.length;
|
||||
},
|
||||
stagingAreaPrevStagedImageSelected: (state) => {
|
||||
state.selectedStagedImageIndex =
|
||||
(state.selectedStagedImageIndex - 1 + state.stagedImages.length) % state.stagedImages.length;
|
||||
state.selectedImageIndex = (state.selectedImageIndex - 1 + state.images.length) % state.images.length;
|
||||
},
|
||||
stagingAreaStagedImageDiscarded: (state, action: PayloadAction<{ index: number }>) => {
|
||||
const { index } = action.payload;
|
||||
state.stagedImages.splice(index, 1);
|
||||
state.selectedStagedImageIndex = Math.min(state.selectedStagedImageIndex, state.stagedImages.length - 1);
|
||||
state.images.splice(index, 1);
|
||||
state.selectedImageIndex = Math.min(state.selectedImageIndex, state.images.length - 1);
|
||||
},
|
||||
stagingAreaReset: (state) => {
|
||||
state.stagedImages = [];
|
||||
state.selectedStagedImageIndex = 0;
|
||||
state.images = [];
|
||||
state.selectedImageIndex = 0;
|
||||
},
|
||||
canvasSessionStarted: (state, action: PayloadAction<{ sessionType: CanvasStagingAreaState['sessionType'] }>) => {
|
||||
const { sessionType } = action.payload;
|
||||
state.sessionType = sessionType;
|
||||
state.images = [];
|
||||
state.selectedImageIndex = 0;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(canvasReset, () => deepClone(initialState));
|
||||
builder.addMatcher(newSessionRequested, () => deepClone(initialState));
|
||||
builder.addCase(newSimpleCanvasSessionRequested, () => {
|
||||
const state = deepClone(initialState);
|
||||
state.sessionType === 'simple';
|
||||
return state;
|
||||
});
|
||||
builder.addCase(newAdvancedCanvasSessionRequested, () => {
|
||||
const state = deepClone(initialState);
|
||||
state.sessionType === 'advanced';
|
||||
return state;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,9 +75,11 @@ export const {
|
||||
stagingAreaImageStaged,
|
||||
stagingAreaStagedImageDiscarded,
|
||||
stagingAreaReset,
|
||||
stagingAreaImageSelected,
|
||||
stagingAreaNextStagedImageSelected,
|
||||
stagingAreaPrevStagedImageSelected,
|
||||
} = canvasStagingAreaSlice.actions;
|
||||
canvasSessionStarted,
|
||||
} = canvasSessionSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const migrate = (state: any): any => {
|
||||
@@ -63,13 +87,13 @@ const migrate = (state: any): any => {
|
||||
};
|
||||
|
||||
export const canvasStagingAreaPersistConfig: PersistConfig<CanvasStagingAreaState> = {
|
||||
name: canvasStagingAreaSlice.name,
|
||||
name: canvasSessionSlice.name,
|
||||
initialState,
|
||||
migrate,
|
||||
persistDenylist: [],
|
||||
};
|
||||
|
||||
export const selectCanvasStagingAreaSlice = (s: RootState) => s.canvasStagingArea;
|
||||
export const selectCanvasStagingAreaSlice = (s: RootState) => s[canvasSessionSlice.name];
|
||||
|
||||
/**
|
||||
* Selects if we should be staging images. This is true if:
|
||||
@@ -80,7 +104,7 @@ export const selectIsStaging = createSelector(
|
||||
selectCanvasQueueCounts,
|
||||
selectCanvasStagingAreaSlice,
|
||||
({ data }, staging) => {
|
||||
if (staging.stagedImages.length > 0) {
|
||||
if (staging.images.length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (!data) {
|
||||
@@ -91,17 +115,18 @@ export const selectIsStaging = createSelector(
|
||||
);
|
||||
export const selectStagedImageIndex = createSelector(
|
||||
selectCanvasStagingAreaSlice,
|
||||
(stagingArea) => stagingArea.selectedStagedImageIndex
|
||||
(stagingArea) => stagingArea.selectedImageIndex
|
||||
);
|
||||
export const selectSelectedImage = createSelector(
|
||||
[selectCanvasStagingAreaSlice, selectStagedImageIndex],
|
||||
(stagingArea, index) => stagingArea.stagedImages[index] ?? null
|
||||
);
|
||||
export const selectStagedImages = createSelector(
|
||||
selectCanvasStagingAreaSlice,
|
||||
(stagingArea) => stagingArea.stagedImages
|
||||
(stagingArea, index) => stagingArea.images[index] ?? null
|
||||
);
|
||||
export const selectStagedImages = createSelector(selectCanvasStagingAreaSlice, (stagingArea) => stagingArea.images);
|
||||
export const selectImageCount = createSelector(
|
||||
selectCanvasStagingAreaSlice,
|
||||
(stagingArea) => stagingArea.stagedImages.length
|
||||
(stagingArea) => stagingArea.images.length
|
||||
);
|
||||
export const selectCanvasSessionType = createSelector(
|
||||
selectCanvasStagingAreaSlice,
|
||||
(canvasSession) => canvasSession.sessionType
|
||||
);
|
||||
|
||||
@@ -409,7 +409,6 @@ export const selectCanvasMetadata = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
export const selectIsSessionStarted = createCanvasSelector(({ isSessionStarted }) => isSessionStarted);
|
||||
export const selectIsCanvasEmpty = createCanvasSelector(
|
||||
({ controlLayers, inpaintMasks, rasterLayers, regionalGuidance }) => {
|
||||
// Check it all manually - could use lodash isEqual, but this selector will be called very often!
|
||||
|
||||
@@ -561,7 +561,6 @@ const zReferenceImages = z.object({
|
||||
});
|
||||
const zCanvasState = z.object({
|
||||
_version: z.literal(3).default(3),
|
||||
isSessionStarted: z.boolean().default(false),
|
||||
selectedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
|
||||
bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
|
||||
inpaintMasks: zInpaintMasks.default({ isHidden: false, entities: [] }),
|
||||
|
||||
@@ -8,7 +8,7 @@ import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePreset
|
||||
import { selectStylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { pick } from 'lodash-es';
|
||||
import { selectListStylePresetsRequestState } from 'services/api/endpoints/stylePresets';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
import type { Invocation, S } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import type { MainModelLoaderNodes } from './types';
|
||||
@@ -134,3 +134,7 @@ export const isMainModelWithoutUnet = (modelLoader: Invocation<MainModelLoaderNo
|
||||
modelLoader.type === 'cogview4_model_loader'
|
||||
);
|
||||
};
|
||||
|
||||
export const isCanvasOutputEvent = (data: S['InvocationCompleteEvent']) => {
|
||||
return data.invocation_source_id.split(':')[0] === CANVAS_OUTPUT_PREFIX;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppDispatch, RootState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { stagingAreaImageStaged } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
import { isImageField, isImageFieldCollection } from 'features/nodes/types/common';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
import { CANVAS_OUTPUT_PREFIX } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import type { ApiTagDescription } from 'services/api';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
|
||||
@@ -19,10 +17,6 @@ import type { JsonObject } from 'type-fest';
|
||||
|
||||
const log = logger('events');
|
||||
|
||||
const isCanvasOutputNode = (data: S['InvocationCompleteEvent']) => {
|
||||
return data.invocation_source_id.split(':')[0] === CANVAS_OUTPUT_PREFIX;
|
||||
};
|
||||
|
||||
const nodeTypeDenylist = ['load_image', 'image'];
|
||||
|
||||
export const buildOnInvocationComplete = (getState: () => RootState, dispatch: AppDispatch) => {
|
||||
@@ -179,12 +173,12 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
|
||||
|
||||
if (data.destination === 'canvas') {
|
||||
// TODO(psyche): Can/should we let canvas handle this itself?
|
||||
if (isCanvasOutputNode(data)) {
|
||||
if (data.result.type === 'image_output') {
|
||||
dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
|
||||
}
|
||||
addImagesToGallery(data, [imageDTO]);
|
||||
}
|
||||
// if (isCanvasOutputEvent(data)) {
|
||||
// if (data.result.type === 'image_output') {
|
||||
// dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
|
||||
// }
|
||||
// addImagesToGallery(data, [imageDTO]);
|
||||
// }
|
||||
} else if (!imageDTO.is_intermediate) {
|
||||
// Desintaion is gallery
|
||||
addImagesToGallery(data, [imageDTO]);
|
||||
|
||||
@@ -9,6 +9,18 @@ export const $socketOptions = map<Partial<ManagerOptions & SocketOptions>>({});
|
||||
export const $isConnected = atom<boolean>(false);
|
||||
export const $lastProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
||||
export const $progressImage = computed($lastProgressEvent, (val) => val?.image ?? null);
|
||||
export const $canvasProgressImage = computed($lastProgressEvent, (event) => {
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
if (event.origin !== 'canvas') {
|
||||
return null;
|
||||
}
|
||||
if (!event.image) {
|
||||
return null;
|
||||
}
|
||||
return event.image;
|
||||
});
|
||||
export const $hasProgressImage = computed($lastProgressEvent, (val) => Boolean(val?.image));
|
||||
export const $isProgressFromCanvas = computed($lastProgressEvent, (val) => val?.destination === 'canvas');
|
||||
export const $invocationProgressMessage = computed($lastProgressEvent, (val) => {
|
||||
|
||||
Reference in New Issue
Block a user