From 58c656224f50b9aac7107333f7f4b1e348d4c17e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:38:00 +1000 Subject: [PATCH] refactor(ui): canvas v2 (wip) --- .../listeners/boardAndImagesDeleted.ts | 4 +- .../listeners/controlAdapterPreprocessor.ts | 6 +- .../listeners/imageDeletionListeners.ts | 2 +- .../listeners/modelsLoaded.ts | 6 +- invokeai/frontend/web/src/app/store/store.ts | 3 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 6 +- .../components/AddPromptButtons.tsx | 2 +- .../components/BrushColorPicker.tsx | 2 +- .../controlLayers/components/BrushSize.tsx | 2 +- .../components/CALayer/CALayer.tsx | 2 +- .../CALayer/CALayerControlAdapterWrapper.tsx | 2 +- .../components/ControlLayersPanelContent.tsx | 2 +- .../components/DeleteAllLayersButton.tsx | 2 +- .../components/GlobalMaskLayerOpacity.tsx | 2 +- .../components/HeadsUpDisplay.tsx | 4 +- .../components/IILayer/IILayer.tsx | 2 +- .../components/IPALayer/IPALayer.tsx | 2 +- .../IPALayer/IPALayerIPAdapterWrapper.tsx | 2 +- .../LayerCommon/LayerMenuArrangeActions.tsx | 6 +- .../LayerCommon/LayerMenuRGActions.tsx | 2 +- .../components/LayerCommon/LayerOpacity.tsx | 2 +- .../components/RGLayer/RGLayer.tsx | 4 +- .../RGLayer/RGLayerAutoNegativeCheckbox.tsx | 2 +- .../components/RGLayer/RGLayerColorPicker.tsx | 2 +- .../RGLayer/RGLayerIPAdapterList.tsx | 2 +- .../RGLayer/RGLayerIPAdapterWrapper.tsx | 2 +- .../components/RasterLayer/RasterLayer.tsx | 2 +- .../components/StageComponent.tsx | 311 +++++++++++------- .../controlLayers/components/ToolChooser.tsx | 4 +- .../controlLayers/hooks/addLayerHooks.ts | 2 +- .../controlLayers/hooks/layerStateHooks.ts | 10 +- .../features/controlLayers/konva/events.ts | 252 +++++++------- .../controlLayers/konva/renderers/caLayer.ts | 61 ++-- .../controlLayers/konva/renderers/layers.ts | 59 ++-- .../controlLayers/konva/renderers/objects.ts | 14 +- .../konva/renderers/previewLayer.ts | 30 +- .../konva/renderers/rasterLayer.ts | 55 ++-- .../controlLayers/konva/renderers/rgLayer.ts | 61 ++-- .../controlLayers/store/controlLayersSlice.ts | 36 +- .../controlLayers/store/layersSlice.ts | 33 +- .../store/regionalGuidanceSlice.ts | 39 +-- .../src/features/controlLayers/store/types.ts | 34 +- .../components/DeleteImageModal.tsx | 2 +- .../deleteImageModal/store/selectors.ts | 2 +- .../components/Boards/DeleteBoardModal.tsx | 2 +- .../util/graph/buildLinearBatchConfig.ts | 2 +- .../util/graph/generation/addControlLayers.ts | 10 +- .../nodes/util/graph/generation/addHRF.ts | 2 +- .../nodes/util/graph/graphBuilderUtils.ts | 2 +- .../components/Core/ParamNegativePrompt.tsx | 2 +- .../components/Core/ParamPositivePrompt.tsx | 2 +- .../queue/components/QueueButtonTooltip.tsx | 2 +- .../ParamSDXLNegativeStylePrompt.tsx | 2 +- .../ParamSDXLPositiveStylePrompt.tsx | 2 +- .../SDXLPrompts/SDXLConcatButton.tsx | 2 +- .../ImageSettingsAccordion.tsx | 2 +- .../ImageSizeLinear.tsx | 6 +- .../hooks/usePresetModifiedPrompts.ts | 2 +- .../ParametersPanelTextToImage.tsx | 2 +- 59 files changed, 605 insertions(+), 519 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index 244e0cdf8a..fb4a23912a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -19,9 +19,9 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS let wereControlAdaptersReset = false; let wereControlLayersReset = false; - const { canvas, nodes, controlAdapters, controlLayers } = getState(); + const { canvas, nodes, controlAdapters, canvasV2 } = getState(); deleted_images.forEach((image_name) => { - const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, controlLayers.present, image_name); + const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, canvasV2, image_name); if (imageUsage.isCanvasImage && !wasCanvasReset) { dispatch(resetCanvas()); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index 3c7d626e77..bdd72764f2 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -65,14 +65,14 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // Delay before starting actual work await delay(DEBOUNCE_MS); - const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); + const layer = state.canvasV2.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); if (!layer) { return; } // We should only process if the processor settings or image have changed - const originalLayer = originalState.controlLayers.present.layers + const originalLayer = originalState.canvasV2.layers .filter(isControlAdapterLayer) .find((l) => l.id === layerId); const originalImage = originalLayer?.controlAdapter.image; @@ -161,7 +161,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni if (signal.aborted) { // The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now). const pendingBatchId = getState() - .controlLayers.present.layers.filter(isControlAdapterLayer) + .canvasV2.layers.filter(isControlAdapterLayer) .find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId; if (pendingBatchId) { cancelProcessorBatch(dispatch, layerId, pendingBatchId); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 61df8846f0..218b0be8ee 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -70,7 +70,7 @@ const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, ima }; const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.controlLayers.present.layers.forEach((l) => { + state.canvasV2.layers.forEach((l) => { if (isRegionalGuidanceLayer(l)) { if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) { dispatch(layerDeleted(l.id)); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 9d02fcbfa5..30f558086e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -79,15 +79,15 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { const optimalDimension = getOptimalDimension(defaultModelInList); if ( getIsSizeOptimal( - state.controlLayers.present.size.width, - state.controlLayers.present.size.height, + state.canvasV2.size.width, + state.canvasV2.size.height, optimalDimension ) ) { return; } const { width, height } = calculateNewSize( - state.controlLayers.present.size.aspectRatio.value, + state.canvasV2.size.aspectRatio.value, optimalDimension * optimalDimension ); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 0fcb842db3..45c0d020e8 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -4,7 +4,7 @@ import { logger } from 'app/logging/logger'; import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver'; import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; import type { JSONObject } from 'common/types'; -import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice'; +import { canvasPersistConfig } from 'features/canvas/store/canvasSlice'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { controlAdaptersV2PersistConfig, @@ -52,7 +52,6 @@ import { listenerMiddleware } from './middleware/listenerMiddleware'; const allReducers = { [api.reducerPath]: api.reducer, - [canvasSlice.name]: canvasSlice.reducer, [gallerySlice.name]: gallerySlice.reducer, [generationSlice.name]: generationSlice.reducer, [nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig), diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index ac47d9005d..97042124b1 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -59,8 +59,8 @@ const createSelector = (templates: Templates) => config ) => { const { model } = generation; - const { size } = controlLayers.present; - const { positivePrompt } = controlLayers.present; + const { size } = canvasV2; + const { positivePrompt } = canvasV2; const { isConnected } = system; @@ -126,7 +126,7 @@ const createSelector = (templates: Templates) => if (activeTabName === 'generation') { // Handling for generation tab - controlLayers.present.layers + canvasV2.layers .filter((l) => l.isEnabled) .forEach((l, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 073a188871..71b1ca5f1e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -23,7 +23,7 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { canAddPositivePrompt: layer.positivePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx index 4c218a87fd..ec8e535b40 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; export const BrushColorPicker = memo(() => { const { t } = useTranslation(); - const brushColor = useAppSelector((s) => s.controlLayers.present.brushColor); + const brushColor = useAppSelector((s) => s.canvasV2.brushColor); const dispatch = useAppDispatch(); const onChange = useCallback( (color: RgbaColor) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx index 731f9a1c77..4c102ea0ff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx @@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`; export const BrushSize = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const brushSize = useAppSelector((s) => s.controlLayers.present.brushSize); + const brushSize = useAppSelector((s) => s.canvasV2.brushSize); const onChange = useCallback( (v: number) => { dispatch(brushSizeChanged(Math.round(v))); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index 868693e58c..9d543446e9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -19,7 +19,7 @@ type Props = { export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector( - (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isControlAdapterLayer).isSelected + (s) => selectLayerOrThrow(s.canvasV2, layerId, isControlAdapterLayer).isSelected ); const onClick = useCallback(() => { dispatch(layerSelected(layerId)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx index 7b0e3aa332..f5ccd49cb2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx @@ -28,7 +28,7 @@ type Props = { export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const controlAdapter = useAppSelector( - (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isControlAdapterLayer).controlAdapter + (s) => selectLayerOrThrow(s.canvasV2, layerId, isControlAdapterLayer).controlAdapter ); const onChangeBeginEndStepPct = useCallback( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 90f21a7253..27ef318c1a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -19,7 +19,7 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; const selectLayerIdTypePairs = createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer); + const [renderableLayers, ipAdapterLayers] = partition(canvasV2.layers, isRenderableLayer); return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse(); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index 00487fcc43..e69b83fa79 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -8,7 +8,7 @@ import { PiTrashSimpleBold } from 'react-icons/pi'; export const DeleteAllLayersButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isDisabled = useAppSelector((s) => s.controlLayers.present.layers.length === 0); + const isDisabled = useAppSelector((s) => s.canvasV2.layers.length === 0); const onClick = useCallback(() => { dispatch(allLayersDeleted()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx index 40985499db..23720f4c22 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx @@ -14,7 +14,7 @@ export const GlobalMaskLayerOpacity = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const globalMaskLayerOpacity = useAppSelector((s) => - Math.round(s.controlLayers.present.globalMaskLayerOpacity * 100) + Math.round(s.canvasV2.globalMaskLayerOpacity * 100) ); const onChange = useCallback( (v: number) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 834903c0f7..2aea3a422c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -7,8 +7,8 @@ import { memo } from 'react'; export const HeadsUpDisplay = memo(() => { const stageAttrs = useStore($stageAttrs); - const layerCount = useAppSelector((s) => s.controlLayers.present.layers.length); - const bbox = useAppSelector((s) => s.controlLayers.present.bbox); + const layerCount = useAppSelector((s) => s.canvasV2.layers.length); + const bbox = useAppSelector((s) => s.canvasV2.bbox); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx index 43857b6fc3..202b830e47 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -25,7 +25,7 @@ type Props = { export const IILayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const layer = useAppSelector((s) => selectLayerOrThrow(s.controlLayers.present, layerId, isInitialImageLayer)); + const layer = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, layerId, isInitialImageLayer)); const onClick = useCallback(() => { dispatch(layerSelected(layerId)); }, [dispatch, layerId]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx index e4d3dd9e4f..d227f73045 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -16,7 +16,7 @@ type Props = { export const IPALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector( - (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isIPAdapterLayer).isSelected + (s) => selectLayerOrThrow(s.canvasV2, layerId, isIPAdapterLayer).isSelected ); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const onClick = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx index 08737e6e60..7d7ba7e1b5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx @@ -22,7 +22,7 @@ type Props = { export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const ipAdapter = useAppSelector( - (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isIPAdapterLayer).ipAdapter + (s) => selectLayerOrThrow(s.canvasV2, layerId, isIPAdapterLayer).ipAdapter ); const onChangeBeginEndStepPct = useCallback( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx index 67cf856c81..4c86a98c36 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx @@ -22,10 +22,10 @@ export const LayerMenuArrangeActions = memo(({ layerId }: Props) => { const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRenderableLayer(layer), `Layer ${layerId} not found or not an RP layer`); - const layerIndex = controlLayers.present.layers.findIndex((l) => l.id === layerId); - const layerCount = controlLayers.present.layers.length; + const layerIndex = canvasV2.layers.findIndex((l) => l.id === layerId); + const layerCount = canvasV2.layers.length; return { canMoveForward: layerIndex < layerCount - 1, canMoveBackward: layerIndex > 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx index 7addbf45eb..d04da5e4a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx @@ -22,7 +22,7 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { canAddPositivePrompt: layer.positivePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx index 481a6597a8..b92ccc41f9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx @@ -37,7 +37,7 @@ export const LayerOpacity = memo(({ layerId }: Props) => { const selectOpacity = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = selectLayerOrThrow(controlLayers.present, layerId, isLayerWithOpacity); + const layer = selectLayerOrThrow(canvasV2, layerId, isLayerWithOpacity); return Math.round(layer.opacity * 100); }), [layerId] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index 54e6b502e6..4df5670f0e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -30,14 +30,14 @@ export const RGLayer = memo(({ layerId }: Props) => { const selector = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { color: rgbColorToString(layer.previewColor), hasPositivePrompt: layer.positivePrompt !== null, hasNegativePrompt: layer.negativePrompt !== null, hasIPAdapters: layer.ipAdapters.length > 0, - isSelected: layerId === controlLayers.present.selectedLayerId, + isSelected: layerId === canvasV2.selectedLayerId, autoNegative: layer.autoNegative, }; }), diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx index ec52062062..998682d4cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx @@ -16,7 +16,7 @@ const useAutoNegative = (layerId: string) => { const selectAutoNegative = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return layer.autoNegative; }), diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx index 40660a1ac2..7ce002817a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx @@ -20,7 +20,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => { const selectColor = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an vector mask layer`); return layer.previewColor; }), diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx index 9a38123d4b..5b6be683d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx @@ -15,7 +15,7 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { const selectIPAdapterIds = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); + const layer = canvasV2.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.ipAdapters; }), diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx index f0879f07cf..802ad2bb3d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx @@ -28,7 +28,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu const onDeleteIPAdapter = useCallback(() => { dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId })); }, [dispatch, ipAdapterId, layerId]); - const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.controlLayers.present, layerId, ipAdapterId)); + const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.canvasV2, layerId, ipAdapterId)); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index e1dd5c3b78..921122fb33 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -19,7 +19,7 @@ type Props = { export const RasterLayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector( - (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isRasterLayer).isSelected + (s) => selectLayerOrThrow(s.canvasV2, layerId, isRasterLayer).isSelected ); const onClick = useCallback(() => { dispatch(layerSelected(layerId)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index f285624fe6..a0b65d3a4e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -5,51 +5,59 @@ import { logger } from 'app/logging/logger'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; -import { - BRUSH_SPACING_PCT, - MAX_BRUSH_SPACING_PX, - MIN_BRUSH_SPACING_PX, - TRANSPARENCY_CHECKER_PATTERN, -} from 'features/controlLayers/konva/constants'; +import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; +import { caBboxChanged, caTranslated } from 'features/controlLayers/store/controlAdaptersSlice'; import { $bbox, - $brushSpacingPx, - $brushWidth, - $fill, - $invertScroll, + $currentFill, $isDrawing, $isMouseDown, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, - $selectedLayer, + $selectedEntity, $spaceKey, $stageAttrs, - $tool, - $toolBuffer, + $toolState, bboxChanged, - brushLineAdded, - brushSizeChanged, - eraserLineAdded, - layerBboxChanged, - layerTranslated, - linePointsAdded, - rectAdded, + brushWidthChanged, + eraserWidthChanged, selectCanvasV2Slice, + toolBufferChanged, + toolChanged, } from 'features/controlLayers/store/controlLayersSlice'; -import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; -import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { + layerBboxChanged, + layerBrushLineAdded, + layerEraserLineAdded, + layerLinePointAdded, + layerRectAdded, + layerTranslated, + selectLayersSlice, +} from 'features/controlLayers/store/layersSlice'; +import { + rgBboxChanged, + rgBrushLineAdded, + rgEraserLineAdded, + rgLinePointAdded, + rgRectAdded, + rgTranslated, + selectRegionalGuidanceSlice, +} from 'features/controlLayers/store/regionalGuidanceSlice'; import type { - AddBrushLineArg, - AddEraserLineArg, - AddPointToLineArg, - AddRectShapeArg, + BboxChangedArg, + BrushLineAddedArg, + CanvasEntity, + EraserLineAddedArg, + PointAddedToLineArg, + PosChangedArg, + RectShapeAddedArg, + Tool, } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; -import { clamp } from 'lodash-es'; import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getImageDTO } from 'services/api/endpoints/images'; @@ -61,12 +69,12 @@ Konva.showWarnings = false; const log = logger('controlLayers'); -const selectBrushColor = createSelector( +const selectBrushFill = createSelector( selectCanvasV2Slice, selectLayersSlice, selectRegionalGuidanceSlice, (canvas, layers, regionalGuidance) => { - const rg = regionalGuidance.regions.find((i) => i.id === canvas.lastSelectedItem?.id); + const rg = regionalGuidance.regions.find((i) => i.id === canvas.selectedEntityIdentifier?.id); if (rg) { return rgbaColorToString({ ...rg.fill, a: regionalGuidance.opacity }); @@ -76,89 +84,121 @@ const selectBrushColor = createSelector( } ); -const selectSelectedLayer = createSelector(selectCanvasV2Slice, (controlLayers) => { - return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null; -}); - -const selectLayerCount = createSelector(selectCanvasV2Slice, (controlLayers) => controlLayers.present.layers.length); - const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => { const dispatch = useAppDispatch(); - const state = useAppSelector((s) => s.controlLayers.present); - const tool = useStore($tool); + const canvasV2State = useAppSelector(selectCanvasV2Slice); + const layersState = useAppSelector((s) => s.layers); + const controlAdaptersState = useAppSelector((s) => s.controlAdaptersV2); + const ipAdaptersState = useAppSelector((s) => s.ipAdapters); + const regionalGuidanceState = useAppSelector((s) => s.regionalGuidance); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); const isMouseDown = useStore($isMouseDown); const isDrawing = useStore($isDrawing); - const brushColor = useAppSelector(selectBrushColor); - const selectedLayer = useAppSelector(selectSelectedLayer); - const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); - const dpr = useDevicePixelRatio({ round: false }); - const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); - const brushSpacingPx = useMemo( - () => clamp(state.brushSize / BRUSH_SPACING_PCT, MIN_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX), - [state.brushSize] - ); - - useLayoutEffect(() => { - $fill.set(brushColor); - $brushWidth.set(state.brushSize); - $brushSpacingPx.set(brushSpacingPx); - $selectedLayer.set(selectedLayer); - $invertScroll.set(shouldInvertBrushSizeScrollDirection); - $bbox.set(state.bbox); + const brushColor = useAppSelector(selectBrushFill); + const selectedEntity = useMemo(() => { + const identifier = canvasV2State.selectedEntityIdentifier; + if (!identifier) { + return null; + } else if (identifier.type === 'layer') { + return layersState.layers.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'control_adapter') { + return controlAdaptersState.controlAdapters.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'ip_adapter') { + return ipAdaptersState.ipAdapters.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'regional_guidance') { + return regionalGuidanceState.regions.find((i) => i.id === identifier.id) ?? null; + } else { + return null; + } }, [ - brushSpacingPx, - brushColor, - selectedLayer, - shouldInvertBrushSizeScrollDirection, - state.brushSize, - state.selectedLayerId, - state.brushColor, - state.bbox, + canvasV2State.selectedEntityIdentifier, + controlAdaptersState.controlAdapters, + ipAdaptersState.ipAdapters, + layersState.layers, + regionalGuidanceState.regions, ]); - const onLayerPosChanged = useCallback( - (layerId: string, x: number, y: number) => { - dispatch(layerTranslated({ layerId, x, y })); + const currentFill = useMemo(() => { + if (selectedEntity && selectedEntity.type === 'regional_guidance') { + return { ...selectedEntity.fill, a: regionalGuidanceState.opacity }; + } + return canvasV2State.tool.fill; + }, [canvasV2State.tool.fill, regionalGuidanceState.opacity, selectedEntity]); + + const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); + const dpr = useDevicePixelRatio({ round: false }); + + useLayoutEffect(() => { + $toolState.set(canvasV2State.tool); + $selectedEntity.set(selectedEntity); + $bbox.set(canvasV2State.bbox); + $currentFill.set(currentFill); + }, [selectedEntity, canvasV2State.tool, canvasV2State.bbox, currentFill]); + + const onPosChanged = useCallback( + (arg: PosChangedArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerTranslated(arg)); + } else if (entityType === 'control_adapter') { + dispatch(caTranslated(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgTranslated(arg)); + } }, [dispatch] ); const onBboxChanged = useCallback( - (layerId: string, bbox: IRect | null) => { - dispatch(layerBboxChanged({ layerId, bbox })); + (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerBboxChanged(arg)); + } else if (entityType === 'control_adapter') { + dispatch(caBboxChanged(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgBboxChanged(arg)); + } }, [dispatch] ); const onBrushLineAdded = useCallback( - (arg: AddBrushLineArg) => { - dispatch(brushLineAdded(arg)); + (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerBrushLineAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgBrushLineAdded(arg)); + } }, [dispatch] ); const onEraserLineAdded = useCallback( - (arg: AddEraserLineArg) => { - dispatch(eraserLineAdded(arg)); + (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerEraserLineAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgEraserLineAdded(arg)); + } }, [dispatch] ); const onPointAddedToLine = useCallback( - (arg: AddPointToLineArg) => { - dispatch(linePointsAdded(arg)); + (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerLinePointAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgLinePointAdded(arg)); + } }, [dispatch] ); const onRectShapeAdded = useCallback( - (arg: AddRectShapeArg) => { - dispatch(rectAdded(arg)); - }, - [dispatch] - ); - const onBrushSizeChanged = useCallback( - (size: number) => { - dispatch(brushSizeChanged(size)); + (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerRectAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgRectAdded(arg)); + } }, [dispatch] ); @@ -168,6 +208,30 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, }, [dispatch] ); + const onBrushWidthChanged = useCallback( + (width: number) => { + dispatch(brushWidthChanged(width)); + }, + [dispatch] + ); + const onEraserWidthChanged = useCallback( + (width: number) => { + dispatch(eraserWidthChanged(width)); + }, + [dispatch] + ); + const setTool = useCallback( + (tool: Tool) => { + dispatch(toolChanged(tool)); + }, + [dispatch] + ); + const setToolBuffer = useCallback( + (toolBuffer: Tool | null) => { + dispatch(toolBufferChanged(toolBuffer)); + }, + [dispatch] + ); useLayoutEffect(() => { log.trace('Initializing stage'); @@ -189,32 +253,29 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const cleanup = setStageEventHandlers({ stage, - getTool: $tool.get, - setTool: $tool.set, - getToolBuffer: $toolBuffer.get, - setToolBuffer: $toolBuffer.set, + getToolState: $toolState.get, + setTool, + setToolBuffer, getIsDrawing: $isDrawing.get, setIsDrawing: $isDrawing.set, getIsMouseDown: $isMouseDown.get, setIsMouseDown: $isMouseDown.set, - getBrushColor: $fill.get, - getBrushSize: $brushWidth.get, - getBrushSpacingPx: $brushSpacingPx.get, - getSelectedLayer: $selectedLayer.get, + getSelectedEntity: $selectedEntity.get, getLastAddedPoint: $lastAddedPoint.get, setLastAddedPoint: $lastAddedPoint.set, getLastCursorPos: $lastCursorPos.get, setLastCursorPos: $lastCursorPos.set, getLastMouseDownPos: $lastMouseDownPos.get, setLastMouseDownPos: $lastMouseDownPos.set, - getShouldInvert: $invertScroll.get, getSpaceKey: $spaceKey.get, setStageAttrs: $stageAttrs.set, - onBrushSizeChanged, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, + onBrushWidthChanged, + onEraserWidthChanged, + getCurrentFill: $currentFill.get, }); return () => { @@ -224,12 +285,15 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, }, [ asPreview, onBrushLineAdded, - onBrushSizeChanged, + onBrushWidthChanged, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, stage, container, + onEraserWidthChanged, + setTool, + setToolBuffer, ]); useLayoutEffect(() => { @@ -267,29 +331,26 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, log.trace('Rendering tool preview'); renderers.renderToolPreview( stage, - tool, - brushColor, - selectedLayer?.type ?? null, - state.globalMaskLayerOpacity, + canvasV2State.tool, + currentFill, + selectedEntity, lastCursorPos, lastMouseDownPos, - state.brushSize, isDrawing, isMouseDown ); }, [ asPreview, brushColor, + canvasV2State.tool, + currentFill, isDrawing, isMouseDown, lastCursorPos, lastMouseDownPos, renderers, - selectedLayer?.type, + selectedEntity, stage, - state.brushSize, - state.globalMaskLayerOpacity, - tool, ]); useLayoutEffect(() => { @@ -300,8 +361,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, log.trace('Rendering bbox preview'); renderers.renderBboxPreview( stage, - state.bbox, - tool, + canvasV2State.bbox, + canvasV2State.tool.selected, $bbox.get, onBboxTransformed, $shift.get, @@ -309,21 +370,41 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, $meta.get, $alt.get ); - }, [asPreview, onBboxTransformed, renderers, stage, state.bbox, tool]); + }, [asPreview, canvasV2State.bbox, canvasV2State.tool.selected, onBboxTransformed, renderers, stage]); useLayoutEffect(() => { log.trace('Rendering layers'); - renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, getImageDTO, onLayerPosChanged); - }, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers]); + renderers.renderLayers( + stage, + layersState.layers, + controlAdaptersState.controlAdapters, + regionalGuidanceState.regions, + regionalGuidanceState.opacity, + canvasV2State.tool.selected, + selectedEntity, + getImageDTO, + onPosChanged + ); + }, [ + stage, + renderers, + layersState.layers, + controlAdaptersState.controlAdapters, + regionalGuidanceState.regions, + regionalGuidanceState.opacity, + onPosChanged, + canvasV2State.tool.selected, + selectedEntity, + ]); - useLayoutEffect(() => { - if (asPreview) { - // Preview should not check for transparency - return; - } - log.trace('Updating bboxes'); - debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged); - }, [stage, asPreview, state.layers, onBboxChanged]); + // useLayoutEffect(() => { + // if (asPreview) { + // // Preview should not check for transparency + // return; + // } + // log.trace('Updating bboxes'); + // debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged); + // }, [stage, asPreview, state.layers, onBboxChanged]); useLayoutEffect(() => { Konva.pixelRatio = dpr; @@ -395,7 +476,7 @@ StageComponent.displayName = 'StageComponent'; const NoLayersFallback = () => { const { t } = useTranslation(); - const layerCount = useAppSelector(selectLayerCount); + const layerCount = useAppSelector((s) => s.layers.layers.length); if (layerCount) { return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index c1daa11df4..d9eadd1a31 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -21,7 +21,7 @@ import { } from 'react-icons/pi'; const selectIsDisabled = createSelector(selectCanvasV2Slice, (controlLayers) => { - const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); + const selectedLayer = canvasV2.layers.find((l) => l.id === canvasV2.selectedLayerId); return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer'; }); @@ -29,7 +29,7 @@ export const ToolChooser: React.FC = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isDisabled = useAppSelector(selectIsDisabled); - const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId); + const selectedLayerId = useAppSelector((s) => s.canvasV2.selectedLayerId); const tool = useStore($tool); const setToolToBrush = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 5e0976ed59..5b9e799063 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -102,7 +102,7 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => { export const useAddIILayer = () => { const dispatch = useAppDispatch(); - const isDisabled = useAppSelector((s) => Boolean(s.controlLayers.present.layers.find(isInitialImageLayer))); + const isDisabled = useAppSelector((s) => Boolean(s.canvasV2.layers.find(isInitialImageLayer))); const addIILayer = useCallback(() => { dispatch(iiLayerAdded(null)); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts index c9dfeb4e12..03f47bcdda 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts @@ -10,7 +10,7 @@ export const useLayerPositivePrompt = (layerId: string) => { const selectLayer = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`); return layer.positivePrompt; @@ -25,7 +25,7 @@ export const useLayerNegativePrompt = (layerId: string) => { const selectLayer = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`); return layer.negativePrompt; @@ -40,7 +40,7 @@ export const useLayerIsEnabled = (layerId: string) => { const selectLayer = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.isEnabled; }), @@ -54,7 +54,7 @@ export const useLayerType = (layerId: string) => { const selectLayer = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.type; }), @@ -68,7 +68,7 @@ export const useCALayerOpacity = (layerId: string) => { const selectLayer = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); + const layer = canvasV2.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled }; }), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 1f1e99f316..bc8f94d31e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -2,27 +2,27 @@ import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { - AddBrushLineArg, - AddEraserLineArg, - AddPointToLineArg, - AddRectShapeArg, - LayerData, + BrushLineAddedArg, + CanvasEntity, + CanvasV2State, + EraserLineAddedArg, + PointAddedToLineArg, + RectShapeAddedArg, + RgbaColor, StageAttrs, Tool, } from 'features/controlLayers/store/types'; -import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; -import type { RgbaColor } from 'react-colorful'; import { PREVIEW_TOOL_GROUP_ID } from './naming'; type Arg = { stage: Konva.Stage; - getTool: () => Tool; + getToolState: () => CanvasV2State['tool']; + getCurrentFill: () => RgbaColor; setTool: (tool: Tool) => void; - getToolBuffer: () => Tool | null; setToolBuffer: (tool: Tool | null) => void; getIsDrawing: () => boolean; setIsDrawing: (isDrawing: boolean) => void; @@ -35,17 +35,14 @@ type Arg = { getLastAddedPoint: () => Vector2d | null; setLastAddedPoint: (pos: Vector2d | null) => void; setStageAttrs: (attrs: StageAttrs) => void; - getBrushColor: () => RgbaColor; - getBrushSize: () => number; - getBrushSpacingPx: () => number; - getSelectedLayer: () => LayerData | null; - getShouldInvert: () => boolean; + getSelectedEntity: () => CanvasEntity | null; getSpaceKey: () => boolean; - onBrushLineAdded: (arg: AddBrushLineArg) => void; - onEraserLineAdded: (arg: AddEraserLineArg) => void; - onPointAddedToLine: (arg: AddPointToLineArg) => void; - onRectShapeAdded: (arg: AddRectShapeArg) => void; - onBrushSizeChanged: (size: number) => void; + onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; + onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void; + onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void; + onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => void; + onBrushWidthChanged: (size: number) => void; + onEraserWidthChanged: (size: number) => void; }; /** @@ -72,30 +69,41 @@ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastC * @param onPointAddedToLine The callback to add a point to a line */ const maybeAddNextPoint = ( - selectedLayer: LayerData, + selectedEntity: CanvasEntity, currentPos: Vector2d, + getToolState: Arg['getToolState'], getLastAddedPoint: Arg['getLastAddedPoint'], setLastAddedPoint: Arg['setLastAddedPoint'], - getBrushSpacingPx: Arg['getBrushSpacingPx'], onPointAddedToLine: Arg['onPointAddedToLine'] ) => { + if (selectedEntity.type !== 'layer' && selectedEntity.type !== 'regional_guidance') { + return; + } // Continue the last line const lastAddedPoint = getLastAddedPoint(); + const toolState = getToolState(); + const minSpacingPx = toolState.selected === 'brush' ? toolState.brush.width * 0.05 : toolState.eraser.width * 0.05; if (lastAddedPoint) { // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number - if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < getBrushSpacingPx()) { + if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) { return; } } setLastAddedPoint(currentPos); - onPointAddedToLine({ layerId, point: [currentPos.x - selectedLayer.x, currentPos.y - selectedLayer.y] }); + onPointAddedToLine( + { + id: selectedEntity.id, + point: [currentPos.x - selectedEntity.x, currentPos.y - selectedEntity.y], + }, + selectedEntity.type + ); }; export const setStageEventHandlers = ({ stage, - getTool, + getToolState, + getCurrentFill, setTool, - getToolBuffer, setToolBuffer, getIsDrawing, setIsDrawing, @@ -108,17 +116,14 @@ export const setStageEventHandlers = ({ getLastAddedPoint, setLastAddedPoint, setStageAttrs, - getBrushColor, - getBrushSize, - getBrushSpacingPx, - getSelectedLayer, - getShouldInvert, + getSelectedEntity, getSpaceKey, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, - onBrushSizeChanged, + onBrushWidthChanged: onBrushSizeChanged, + onEraserWidthChanged: onEraserSizeChanged, }: Arg): (() => void) => { //#region mouseenter stage.on('mouseenter', (e) => { @@ -126,7 +131,7 @@ export const setStageEventHandlers = ({ if (!stage) { return; } - const tool = getTool(); + const tool = getToolState().selected; stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); }); @@ -137,13 +142,13 @@ export const setStageEventHandlers = ({ return; } setIsMouseDown(true); - const tool = getTool(); + const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); - const selectedLayer = getSelectedLayer(); - if (!pos || !selectedLayer) { + const selectedEntity = getSelectedEntity(); + if (!pos || !selectedEntity) { return; } - if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { + if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { return; } @@ -155,23 +160,37 @@ export const setStageEventHandlers = ({ setIsDrawing(true); setLastMouseDownPos(pos); - if (tool === 'brush') { - onBrushLineAdded({ - layerId: selectedLayer.id, - points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], - color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, - }); + if (toolState.selected === 'brush') { + onBrushLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + color: getCurrentFill(), + width: toolState.brush.width, + }, + selectedEntity.type + ); } - if (tool === 'eraser') { - onEraserLineAdded({ - layerId: selectedLayer.id, - points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], - }); - } - - if (tool === 'rect') { - // Setting the last mouse down pos starts a rect + if (toolState.selected === 'eraser') { + onEraserLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.eraser.width, + }, + selectedEntity.type + ); } }); @@ -183,12 +202,12 @@ export const setStageEventHandlers = ({ } setIsMouseDown(false); const pos = getLastCursorPos(); - const selectedLayer = getSelectedLayer(); + const selectedEntity = getSelectedEntity(); - if (!pos || !selectedLayer) { + if (!pos || !selectedEntity) { return; } - if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { + if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { return; } @@ -197,21 +216,24 @@ export const setStageEventHandlers = ({ return; } - const tool = getTool(); + const toolState = getToolState(); - if (tool === 'rect') { + if (toolState.selected === 'rect') { const lastMouseDownPos = getLastMouseDownPos(); if (lastMouseDownPos) { - onRectShapeAdded({ - layerId: selectedLayer.id, - rect: { - x: Math.min(pos.x, lastMouseDownPos.x), - y: Math.min(pos.y, lastMouseDownPos.y), - width: Math.abs(pos.x - lastMouseDownPos.x), - height: Math.abs(pos.y - lastMouseDownPos.y), + onRectShapeAdded( + { + id: selectedEntity.id, + rect: { + x: Math.min(pos.x, lastMouseDownPos.x), + y: Math.min(pos.y, lastMouseDownPos.y), + width: Math.abs(pos.x - lastMouseDownPos.x), + height: Math.abs(pos.y - lastMouseDownPos.y), + }, + color: getCurrentFill(), }, - color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, - }); + selectedEntity.type + ); } } @@ -225,16 +247,18 @@ export const setStageEventHandlers = ({ if (!stage) { return; } - const tool = getTool(); + const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); - const selectedLayer = getSelectedLayer(); + const selectedEntity = getSelectedEntity(); - stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); + stage + .findOne(`#${PREVIEW_TOOL_GROUP_ID}`) + ?.visible(toolState.selected === 'brush' || toolState.selected === 'eraser'); - if (!pos || !selectedLayer) { + if (!pos || !selectedEntity) { return; } - if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { + if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { return; } @@ -247,45 +271,49 @@ export const setStageEventHandlers = ({ return; } - if (tool === 'brush') { + if (toolState.selected === 'brush') { if (getIsDrawing()) { // Continue the last line - maybeAddNextPoint( - selectedLayer.id, - pos, - getLastAddedPoint, - setLastAddedPoint, - getBrushSpacingPx, - onPointAddedToLine - ); + maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); } else { // Start a new line - onBrushLineAdded({ - layerId: selectedLayer.id, - points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], - color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, - }); + onBrushLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.brush.width, + color: getCurrentFill(), + }, + selectedEntity.type + ); setIsDrawing(true); } } - if (tool === 'eraser') { + if (toolState.selected === 'eraser') { if (getIsDrawing()) { // Continue the last line - maybeAddNextPoint( - selectedLayer.id, - pos, - getLastAddedPoint, - setLastAddedPoint, - getBrushSpacingPx, - onPointAddedToLine - ); + maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); } else { // Start a new line - onEraserLineAdded({ - layerId: selectedLayer.id, - points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], - }); + onEraserLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.eraser.width, + }, + selectedEntity.type + ); setIsDrawing(true); } } @@ -301,15 +329,15 @@ export const setStageEventHandlers = ({ setIsDrawing(false); setLastCursorPos(null); setLastMouseDownPos(null); - const selectedLayer = getSelectedLayer(); - const tool = getTool(); + const selectedEntity = getSelectedEntity(); + const toolState = getToolState(); stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); - if (!pos || !selectedLayer) { + if (!pos || !selectedEntity) { return; } - if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { + if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { return; } if (getSpaceKey()) { @@ -317,12 +345,11 @@ export const setStageEventHandlers = ({ return; } if (getIsMouseDown()) { - if (tool === 'brush') { - onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); + if (toolState.selected === 'brush') { + onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); } - - if (tool === 'eraser') { - onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); + if (toolState.selected === 'eraser') { + onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); } } }); @@ -331,12 +358,17 @@ export const setStageEventHandlers = ({ e.evt.preventDefault(); if (e.evt.ctrlKey || e.evt.metaKey) { + const toolState = getToolState(); let delta = e.evt.deltaY; - if (getShouldInvert()) { + if (toolState.invertScroll) { delta = -delta; } // Holding ctrl or meta while scrolling changes the brush size - onBrushSizeChanged(calculateNewBrushSize(getBrushSize(), delta)); + if (toolState.selected === 'brush') { + onBrushSizeChanged(calculateNewBrushSize(toolState.brush.width, delta)); + } else if (toolState.selected === 'eraser') { + onEraserSizeChanged(calculateNewBrushSize(toolState.eraser.width, delta)); + } } else { // We need the absolute cursor position - not the scaled position const cursorPos = stage.getPointerPosition(); @@ -396,7 +428,7 @@ export const setStageEventHandlers = ({ setIsDrawing(false); setLastMouseDownPos(null); } else if (e.key === ' ') { - setToolBuffer(getTool()); + setToolBuffer(getToolState().selected); setTool('view'); } }; @@ -408,7 +440,7 @@ export const setStageEventHandlers = ({ return; } if (e.key === ' ') { - const toolBuffer = getToolBuffer(); + const toolBuffer = getToolState().selectedBuffer; setTool(toolBuffer ?? 'move'); setToolBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index feb91867ea..7b85bcfce3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -1,6 +1,6 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming'; -import type { ControlAdapterLayer } from 'features/controlLayers/store/types'; +import type { ControlAdapterData } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { ImageDTO } from 'services/api/types'; @@ -12,11 +12,11 @@ import type { ImageDTO } from 'services/api/types'; /** * Creates a control adapter layer. * @param stage The konva stage - * @param layerState The control adapter layer state + * @param ca The control adapter layer state */ -const createCALayer = (stage: Konva.Stage, layerState: ControlAdapterLayer): Konva.Layer => { +const createCALayer = (stage: Konva.Stage, ca: ControlAdapterData): Konva.Layer => { const konvaLayer = new Konva.Layer({ - id: layerState.id, + id: ca.id, name: CA_LAYER_NAME, imageSmoothingEnabled: false, listening: false, @@ -44,16 +44,16 @@ const createCALayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement): * the konva image. * @param stage The konva stage * @param konvaLayer The konva layer - * @param layerState The control adapter layer state + * @param ca The control adapter layer state * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source */ const updateCALayerImageSource = async ( stage: Konva.Stage, konvaLayer: Konva.Layer, - layerState: ControlAdapterLayer, + ca: ControlAdapterData, getImageDTO: (imageName: string) => Promise ): Promise => { - const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; + const image = ca.processedImage ?? ca.image; if (image) { const imageName = image.name; const imageDTO = await getImageDTO(imageName); @@ -61,7 +61,7 @@ const updateCALayerImageSource = async ( return; } const imageEl = new Image(); - const imageId = getCALayerImageId(layerState.id, imageName); + const imageId = getCALayerImageId(ca.id, imageName); imageEl.onload = () => { // Find the existing image or create a new one - must find using the name, bc the id may have just changed const konvaImage = @@ -72,7 +72,7 @@ const updateCALayerImageSource = async ( id: imageId, image: imageEl, }); - updateCALayerImageAttrs(stage, konvaImage, layerState); + updateCALayerImageAttrs(stage, konvaImage, ca); // Must cache after this to apply the filters konvaImage.cache(); imageEl.id = imageId; @@ -87,36 +87,33 @@ const updateCALayerImageSource = async ( * Updates the image attributes for a control adapter layer's image (width, height, visibility, opacity, filters). * @param stage The konva stage * @param konvaImage The konva image - * @param layerState The control adapter layer state + * @param ca The control adapter layer state */ -const updateCALayerImageAttrs = ( - stage: Konva.Stage, - konvaImage: Konva.Image, - layerState: ControlAdapterLayer -): void => { +const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterData): void => { let needsCache = false; // Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching, // but it doesn't seem to break anything. // TODO(psyche): Investigate and report upstream. - const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; + const filter = konvaImage.filters()[0] ?? null; + const filterNeedsUpdate = (filter === null && ca.filter !== 'none') || (filter && filter.name !== ca.filter); if ( - konvaImage.x() !== layerState.x || - konvaImage.y() !== layerState.y || - konvaImage.visible() !== layerState.isEnabled || - hasFilter !== layerState.isFilterEnabled + konvaImage.x() !== ca.x || + konvaImage.y() !== ca.y || + konvaImage.visible() !== ca.isEnabled || + filterNeedsUpdate ) { konvaImage.setAttrs({ - opacity: layerState.opacity, + opacity: ca.opacity, scaleX: 1, scaleY: 1, - visible: layerState.isEnabled, - filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [], + visible: ca.isEnabled, + filters: ca.filter === LightnessToAlphaFilter.name ? [LightnessToAlphaFilter] : [], }); needsCache = true; } - if (konvaImage.opacity() !== layerState.opacity) { - konvaImage.opacity(layerState.opacity); + if (konvaImage.opacity() !== ca.opacity) { + konvaImage.opacity(ca.opacity); } if (needsCache) { konvaImage.cache(); @@ -127,16 +124,16 @@ const updateCALayerImageAttrs = ( * Renders a control adapter layer. If the layer doesn't already exist, it is created. Otherwise, the layer is updated * with the current image source and attributes. * @param stage The konva stage - * @param layerState The control adapter layer state + * @param ca The control adapter layer state * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source */ export const renderCALayer = ( stage: Konva.Stage, - layerState: ControlAdapterLayer, + ca: ControlAdapterData, zIndex: number, getImageDTO: (imageName: string) => Promise ): void => { - const konvaLayer = stage.findOne(`#${layerState.id}`) ?? createCALayer(stage, layerState); + const konvaLayer = stage.findOne(`#${ca.id}`) ?? createCALayer(stage, ca); konvaLayer.zIndex(zIndex); @@ -146,8 +143,8 @@ export const renderCALayer = ( let imageSourceNeedsUpdate = false; if (canvasImageSource instanceof HTMLImageElement) { - const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; - if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) { + const image = ca.processedImage ?? ca.image; + if (image && canvasImageSource.id !== getCALayerImageId(ca.id, image.name)) { imageSourceNeedsUpdate = true; } else if (!image) { imageSourceNeedsUpdate = true; @@ -157,8 +154,8 @@ export const renderCALayer = ( } if (imageSourceNeedsUpdate) { - updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO); + updateCALayerImageSource(stage, konvaLayer, ca, getImageDTO); } else if (konvaImage) { - updateCALayerImageAttrs(stage, konvaImage, layerState); + updateCALayerImageAttrs(stage, konvaImage, ca); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 266bfd4aaa..d5dc53f3be 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -2,19 +2,17 @@ import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants'; import { PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; -import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer'; import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; -import type { LayerData, Tool } from 'features/controlLayers/store/types'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isInpaintMaskLayer, - isRasterLayer, - isRegionalGuidanceLayer, - isRenderableLayer, +import type { + CanvasEntity, + ControlAdapterData, + LayerData, + PosChangedArg, + RegionalGuidanceData, + Tool, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import { debounce } from 'lodash-es'; @@ -27,43 +25,42 @@ import type { ImageDTO } from 'services/api/types'; /** * Renders the layers on the stage. * @param stage The konva stage - * @param layerStates Array of all layer states - * @param globalMaskLayerOpacity The global mask layer opacity + * @param layers Array of all layer states + * @param rgGlobalOpacity The global mask layer opacity * @param tool The current tool * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source - * @param onLayerPosChanged Callback for when the layer's position changes + * @param onPosChanged Callback for when the layer's position changes */ const renderLayers = ( stage: Konva.Stage, - layerStates: LayerData[], - globalMaskLayerOpacity: number, + layers: LayerData[], + controlAdapters: ControlAdapterData[], + regions: RegionalGuidanceData[], + rgGlobalOpacity: number, tool: Tool, + selectedEntity: CanvasEntity | null, getImageDTO: (imageName: string) => Promise, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { - const layerIds = layerStates.filter(isRenderableLayer).map(mapId); + const renderableIds = [...layers.map(mapId), ...controlAdapters.map(mapId), ...regions.map(mapId)]; // Remove un-rendered layers for (const konvaLayer of stage.find(selectRenderableLayers)) { - if (!layerIds.includes(konvaLayer.id())) { + if (!renderableIds.includes(konvaLayer.id())) { konvaLayer.destroy(); } } // We'll need to ensure the tool preview layer is on top of the rest of the layers let zIndex = 0; - for (const layer of layerStates) { - if (isRegionalGuidanceLayer(layer)) { - renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged); - } else if (isControlAdapterLayer(layer)) { - renderCALayer(stage, layer, zIndex, getImageDTO); - } else if (isInitialImageLayer(layer)) { - renderIILayer(stage, layer, zIndex, getImageDTO); - } else if (isRasterLayer(layer)) { - renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged); - } else if (isInpaintMaskLayer(layer)) { - // - } - // IP Adapter layers are not rendered - // Increment the z-index for the tool layer + for (const layer of layers) { + renderRasterLayer(stage, layer, tool, zIndex, onPosChanged); + zIndex++; + } + for (const ca of controlAdapters) { + renderCALayer(stage, ca, zIndex, getImageDTO); + zIndex++; + } + for (const rg of regions) { + renderRGLayer(stage, rg, rgGlobalOpacity, tool, zIndex, selectedEntity, onPosChanged); zIndex++; } // Arrange the tool preview layer diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts index 351a39301e..9074f24046 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -5,7 +5,13 @@ import { LAYER_BBOX_NAME, PREVIEW_GENERATION_BBOX_DUMMY_RECT, } from 'features/controlLayers/konva/naming'; -import type { BrushLine, EraserLine, ImageObject, LayerData, RectShape } from 'features/controlLayers/store/types'; +import type { + BrushLine, + CanvasEntity, + EraserLine, + ImageObject, + RectShape, +} from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; @@ -174,12 +180,12 @@ export const createImageObjectGroup = async ( /** * Creates a bounding box rect for a layer. - * @param layerState The layer state for the layer to create the bounding box for + * @param entity The layer state for the layer to create the bounding box for * @param konvaLayer The konva layer to attach the bounding box to */ -export const createBboxRect = (layerState: LayerData, konvaLayer: Konva.Layer): Konva.Rect => { +export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => { const rect = new Konva.Rect({ - id: getLayerBboxId(layerState.id), + id: getLayerBboxId(entity.id), name: LAYER_BBOX_NAME, strokeWidth: 1, visible: false, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index d58d459ee0..4ee22d71a6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -18,7 +18,7 @@ import { PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; import { selectRenderableLayers } from 'features/controlLayers/konva/util'; -import type { LayerData, RgbaColor, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, CanvasV2State, RgbaColor, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; @@ -327,8 +327,8 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { * Renders the preview layer. * @param stage The konva stage * @param tool The selected tool - * @param color The selected layer's color - * @param selectedLayerType The selected layer's type + * @param currentFill The selected layer's color + * @param selectedEntity The selected layer's type * @param globalMaskLayerOpacity The global mask layer opacity * @param cursorPos The cursor position * @param lastMouseDownPos The position of the last mouse down event - used for the rect tool @@ -336,17 +336,16 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { */ export const renderToolPreview = ( stage: Konva.Stage, - tool: Tool, - brushColor: RgbaColor, - selectedLayerType: LayerData['type'] | null, - globalMaskLayerOpacity: number, + toolState: CanvasV2State['tool'], + currentFill: RgbaColor, + selectedEntity: CanvasEntity | null, cursorPos: Vector2d | null, lastMouseDownPos: Vector2d | null, - brushSize: number, isDrawing: boolean, isMouseDown: boolean ): void => { const layerCount = stage.find(selectRenderableLayers).length; + const tool = toolState.selected; // Update the stage's pointer style if (tool === 'view') { // View gets a hand @@ -354,7 +353,7 @@ export const renderToolPreview = ( } else if (layerCount === 0) { // We have no layers, so we should not render any tool stage.container().style.cursor = 'default'; - } else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') { + } else if (selectedEntity?.type !== 'regional_guidance' && selectedEntity?.type !== 'layer') { // Non-mask-guidance layers don't have tools stage.container().style.cursor = 'not-allowed'; } else if (tool === 'move') { @@ -377,7 +376,7 @@ export const renderToolPreview = ( if ( !cursorPos || layerCount === 0 || - (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') + (selectedEntity?.type !== 'regional_guidance' && selectedEntity?.type !== 'layer') ) { // We can bail early if the mouse isn't over the stage or there are no layers toolPreviewGroup.visible(false); @@ -394,24 +393,25 @@ export const renderToolPreview = ( if (cursorPos && (tool === 'brush' || tool === 'eraser')) { // Update the fill circle const brushPreviewFill = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_FILL_ID}`); + const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; brushPreviewFill?.setAttrs({ x: cursorPos.x, y: cursorPos.y, - radius: brushSize / 2, - fill: isDrawing ? '' : rgbaColorToString(brushColor), + radius, + fill: isDrawing ? '' : rgbaColorToString(currentFill), globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', }); // Update the inner border of the brush preview const brushPreviewInner = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); - brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); + brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); // Update the outer border of the brush preview const brushPreviewOuter = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); brushPreviewOuter?.setAttrs({ x: cursorPos.x, y: cursorPos.y, - radius: brushSize / 2 + 1, + radius: radius + 1, }); brushPreviewGroup.visible(true); @@ -426,7 +426,7 @@ export const renderToolPreview = ( y: Math.min(cursorPos.y, lastMouseDownPos.y), width: Math.abs(cursorPos.x - lastMouseDownPos.x), height: Math.abs(cursorPos.y - lastMouseDownPos.y), - fill: rgbaColorToString(brushColor), + fill: rgbaColorToString(currentFill), }); rectPreview?.visible(true); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts index 061c2ed0e6..d34d48063c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -1,6 +1,4 @@ -import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants'; import { - LAYER_BBOX_NAME, RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, RASTER_LAYER_IMAGE_NAME, @@ -9,7 +7,6 @@ import { RASTER_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; import { - createBboxRect, createBrushLine, createEraserLine, createImageObjectGroup, @@ -17,7 +14,7 @@ import { createRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; -import type { RasterLayer, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, LayerData, PosChangedArg, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; /** @@ -28,12 +25,12 @@ import Konva from 'konva'; * Creates a raster layer. * @param stage The konva stage * @param layerState The raster layer state - * @param onLayerPosChanged Callback for when the layer's position changes + * @param onPosChanged Callback for when the layer's position changes */ const createRasterLayer = ( stage: Konva.Stage, - layerState: RasterLayer, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void + layerState: LayerData, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): Konva.Layer => { // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ @@ -45,9 +42,9 @@ const createRasterLayer = ( // When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing // the position - we do not need to call this on the `dragmove` event. - if (onLayerPosChanged) { + if (onPosChanged) { konvaLayer.on('dragend', function (e) { - onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); + onPosChanged({ id: layerState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer'); }); } @@ -61,17 +58,17 @@ const createRasterLayer = ( * @param stage The konva stage * @param layerState The regional guidance layer state * @param tool The current tool - * @param onLayerPosChanged Callback for when the layer's position changes + * @param onPosChanged Callback for when the layer's position changes */ export const renderRasterLayer = async ( stage: Konva.Stage, - layerState: RasterLayer, + layerState: LayerData, tool: Tool, zIndex: number, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ) => { const konvaLayer = - stage.findOne(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onLayerPosChanged); + stage.findOne(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onPosChanged); // Update the layer's position and listening state konvaLayer.setAttrs({ @@ -128,23 +125,23 @@ export const renderRasterLayer = async ( konvaLayer.visible(layerState.isEnabled); } - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); + // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); - if (layerState.bbox) { - const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; - bboxRect.setAttrs({ - visible: active, - listening: active, - x: layerState.bbox.x, - y: layerState.bbox.y, - width: layerState.bbox.width, - height: layerState.bbox.height, - stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', - strokeWidth: 1 / stage.scaleX(), - }); - } else { - bboxRect.visible(false); - } + // if (layerState.bbox) { + // const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; + // bboxRect.setAttrs({ + // visible: active, + // listening: active, + // x: layerState.bbox.x, + // y: layerState.bbox.y, + // width: layerState.bbox.width, + // height: layerState.bbox.height, + // stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', + // strokeWidth: 1 / stage.scaleX(), + // }); + // } else { + // bboxRect.visible(false); + // } konvaObjectGroup.opacity(layerState.opacity); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts index e6bc7e1212..2f7f259baa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -18,7 +18,7 @@ import { createRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; -import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, PosChangedArg, RegionalGuidanceData, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; /** @@ -41,17 +41,17 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { /** * Creates a regional guidance layer. * @param stage The konva stage - * @param layerState The regional guidance layer state + * @param rg The regional guidance layer state * @param onLayerPosChanged Callback for when the layer's position changes */ const createRGLayer = ( stage: Konva.Stage, - layerState: RegionalGuidanceLayer, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void + rg: RegionalGuidanceData, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): Konva.Layer => { // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ - id: layerState.id, + id: rg.id, name: RG_LAYER_NAME, draggable: true, dragDistance: 0, @@ -59,9 +59,9 @@ const createRGLayer = ( // When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing // the position - we do not need to call this on the `dragmove` event. - if (onLayerPosChanged) { + if (onPosChanged) { konvaLayer.on('dragend', function (e) { - onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); + onPosChanged({ id: rg.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance'); }); } @@ -73,32 +73,32 @@ const createRGLayer = ( /** * Renders a raster layer. * @param stage The konva stage - * @param layerState The regional guidance layer state + * @param rg The regional guidance layer state * @param globalMaskLayerOpacity The global mask layer opacity * @param tool The current tool - * @param onLayerPosChanged Callback for when the layer's position changes + * @param onPosChanged Callback for when the layer's position changes */ export const renderRGLayer = ( stage: Konva.Stage, - layerState: RegionalGuidanceLayer, + rg: RegionalGuidanceData, globalMaskLayerOpacity: number, tool: Tool, zIndex: number, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void + selectedEntity: CanvasEntity | null, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { - const konvaLayer = - stage.findOne(`#${layerState.id}`) ?? createRGLayer(stage, layerState, onLayerPosChanged); + const konvaLayer = stage.findOne(`#${rg.id}`) ?? createRGLayer(stage, rg, onPosChanged); // Update the layer's position and listening state konvaLayer.setAttrs({ listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(layerState.x), - y: Math.floor(layerState.y), + x: Math.floor(rg.x), + y: Math.floor(rg.y), zIndex, }); // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(layerState.previewColor); + const rgbColor = rgbColorToString(rg.fill); const konvaObjectGroup = konvaLayer.findOne(`.${RG_LAYER_OBJECT_GROUP_NAME}`) ?? @@ -107,7 +107,7 @@ export const renderRGLayer = ( // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. let groupNeedsCache = false; - const objectIds = layerState.objects.map(mapId); + const objectIds = rg.objects.map(mapId); // Destroy any objects that are no longer in the redux state for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) { if (!objectIds.includes(objectNode.id())) { @@ -116,7 +116,7 @@ export const renderRGLayer = ( } } - for (const obj of layerState.objects) { + for (const obj of rg.objects) { if (obj.type === 'brush_line') { const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RG_LAYER_BRUSH_LINE_NAME); @@ -160,8 +160,8 @@ export const renderRGLayer = ( } // Only update layer visibility if it has changed. - if (konvaLayer.visible() !== layerState.isEnabled) { - konvaLayer.visible(layerState.isEnabled); + if (konvaLayer.visible() !== rg.isEnabled) { + konvaLayer.visible(rg.isEnabled); groupNeedsCache = true; } @@ -173,6 +173,7 @@ export const renderRGLayer = ( const compositingRect = konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer); + const isSelected = selectedEntity?.id === rg.id; /** * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows @@ -185,7 +186,7 @@ export const renderRGLayer = ( * Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to * a single raster image, and _then_ applied the 50% opacity. */ - if (layerState.isSelected && tool !== 'move') { + if (isSelected && tool !== 'move') { // We must clear the cache first so Konva will re-draw the group with the new compositing rect if (konvaObjectGroup.isCached()) { konvaObjectGroup.clearCache(); @@ -195,7 +196,7 @@ export const renderRGLayer = ( compositingRect.setAttrs({ // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already - ...(!layerState.bboxNeedsUpdate && layerState.bbox ? layerState.bbox : getLayerBboxFast(konvaLayer)), + ...(!rg.bboxNeedsUpdate && rg.bbox ? rg.bbox : getLayerBboxFast(konvaLayer)), fill: rgbColor, opacity: globalMaskLayerOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) @@ -215,18 +216,18 @@ export const renderRGLayer = ( konvaObjectGroup.opacity(globalMaskLayerOpacity); } - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, konvaLayer); - if (layerState.bbox) { - const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; + if (rg.bbox) { + const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; bboxRect.setAttrs({ visible: active, listening: active, - x: layerState.bbox.x, - y: layerState.bbox.y, - width: layerState.bbox.width, - height: layerState.bbox.height, - stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', + x: rg.bbox.x, + y: rg.bbox.y, + width: rg.bbox.width, + height: rg.bbox.height, + stroke: isSelected ? BBOX_SELECTED_STROKE : '', }); } else { bboxRect.visible(false); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 71df710258..ee2072eaf6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -11,21 +11,12 @@ import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/ import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; -import type { - CanvasV2State, - ControlAdapterData, - IPAdapterData, - LayerData, - RegionalGuidanceData, - RgbaColor, - StageAttrs, - Tool, -} from './types'; +import type { CanvasEntity, CanvasV2State, RgbaColor, StageAttrs, Tool } from './types'; import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { _version: 3, - lastSelectedItem: null, + selectedEntityIdentifier: null, prompts: { positivePrompt: '', negativePrompt: '', @@ -113,6 +104,12 @@ export const canvasV2Slice = createSlice({ invertScrollChanged: (state, action: PayloadAction) => { state.tool.invertScroll = action.payload; }, + toolChanged: (state, action: PayloadAction) => { + state.tool.selected = action.payload; + }, + toolBufferChanged: (state, action: PayloadAction) => { + state.tool.selectedBuffer = action.payload; + }, }, extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { @@ -146,6 +143,8 @@ export const { eraserWidthChanged, fillChanged, invertScrollChanged, + toolChanged, + toolBufferChanged, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; @@ -173,18 +172,9 @@ export const $stageAttrs = atom({ // Some nanostores that are manually synced to redux state to provide imperative access // TODO(psyche): -export const $tool = atom('brush'); -export const $toolBuffer = atom(null); -export const $brushWidth = atom(0); -export const $brushSpacingPx = atom(0); -export const $eraserWidth = atom(0); -export const $eraserSpacingPx = atom(0); -export const $fill = atom(DEFAULT_RGBA_COLOR); -export const $selectedLayer = atom(null); -export const $selectedRG = atom(null); -export const $selectedCA = atom(null); -export const $selectedIPA = atom(null); -export const $invertScroll = atom(false); +export const $toolState = atom(deepClone(initialState.tool)); +export const $currentFill = atom(DEFAULT_RGBA_COLOR); +export const $selectedEntity = atom(null); export const $bbox = atom({ x: 0, y: 0, width: 0, height: 0 }); export const canvasV2PersistConfig: PersistConfig = { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts index 45790bb2f1..7c6705ae9b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts @@ -7,12 +7,12 @@ import type { IRect } from 'konva/lib/types'; import { v4 as uuidv4 } from 'uuid'; import type { - AddBrushLineArg, - AddEraserLineArg, - AddImageObjectArg, - AddPointToLineArg, - AddRectShapeArg, + BrushLineAddedArg, + EraserLineAddedArg, + ImageObjectAddedArg, LayerData, + PointAddedToLineArg, + RectShapeAddedArg, } from './types'; import { isLine } from './types'; @@ -133,7 +133,7 @@ export const layersSlice = createSlice({ moveToStart(state.layers, layer); }, layerBrushLineAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, points, lineId, color, width } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -149,12 +149,12 @@ export const layersSlice = createSlice({ }); layer.bboxNeedsUpdate = true; }, - prepare: (payload: AddBrushLineArg) => ({ + prepare: (payload: BrushLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, }), }, layerEraserLineAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, points, lineId, width } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -169,11 +169,11 @@ export const layersSlice = createSlice({ }); layer.bboxNeedsUpdate = true; }, - prepare: (payload: AddEraserLineArg) => ({ + prepare: (payload: EraserLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, }), }, - layerLinePointAdded: (state, action: PayloadAction) => { + layerLinePointAdded: (state, action: PayloadAction) => { const { id, point } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -187,7 +187,7 @@ export const layersSlice = createSlice({ layer.bboxNeedsUpdate = true; }, layerRectAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, rect, rectId, color } = action.payload; if (rect.height === 0 || rect.width === 0) { // Ignore zero-area rectangles @@ -200,18 +200,15 @@ export const layersSlice = createSlice({ layer.objects.push({ type: 'rect_shape', id: getRectShapeId(id, rectId), - x: rect.x - layer.x, - y: rect.y - layer.y, - width: rect.width, - height: rect.height, + ...rect, color, }); layer.bboxNeedsUpdate = true; }, - prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }), + prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, layerImageAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, imageId, imageDTO } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -229,7 +226,7 @@ export const layersSlice = createSlice({ }); layer.bboxNeedsUpdate = true; }, - prepare: (payload: AddImageObjectArg) => ({ payload: { ...payload, imageId: uuidv4() } }), + prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, imageId: uuidv4() } }), }, }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts index 167feec396..7d177fabbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -14,11 +14,11 @@ import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { - AddBrushLineArg, - AddEraserLineArg, - AddPointToLineArg, - AddRectShapeArg, + BrushLineAddedArg, + EraserLineAddedArg, IPAdapterData, + PointAddedToLineArg, + RectShapeAddedArg, RegionalGuidanceData, RgbColor, } from './types'; @@ -306,7 +306,7 @@ export const regionalGuidanceSlice = createSlice({ ipa.clipVisionModel = clipVisionModel; }, rgBrushLineAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, points, lineId, color, width } = action.payload; const rg = selectRg(state, id); if (!rg) { @@ -315,21 +315,19 @@ export const regionalGuidanceSlice = createSlice({ rg.objects.push({ id: getBrushLineId(id, lineId), type: 'brush_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y], + points, strokeWidth: width, color, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; }, - prepare: (payload: AddBrushLineArg) => ({ + prepare: (payload: BrushLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, }), }, rgEraserLineAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, points, lineId, width } = action.payload; const rg = selectRg(state, id); if (!rg) { @@ -338,19 +336,17 @@ export const regionalGuidanceSlice = createSlice({ rg.objects.push({ id: getEraserLineId(id, lineId), type: 'eraser_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y], + points, strokeWidth: width, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; }, - prepare: (payload: AddEraserLineArg) => ({ + prepare: (payload: EraserLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, }), }, - rgLinePointAdded: (state, action: PayloadAction) => { + rgLinePointAdded: (state, action: PayloadAction) => { const { id, point } = action.payload; const rg = selectRg(state, id); if (!rg) { @@ -360,14 +356,12 @@ export const regionalGuidanceSlice = createSlice({ if (!lastObject || !isLine(lastObject)) { return; } - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener - lastObject.points.push(point[0] - rg.x, point[1] - rg.y); + lastObject.points.push(...point); rg.bboxNeedsUpdate = true; rg.imageCache = null; }, rgRectAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, rect, rectId, color } = action.payload; if (rect.height === 0 || rect.width === 0) { // Ignore zero-area rectangles @@ -380,16 +374,13 @@ export const regionalGuidanceSlice = createSlice({ rg.objects.push({ type: 'rect_shape', id: getRectShapeId(id, rectId), - x: rect.x - rg.x, - y: rect.y - rg.y, - width: rect.width, - height: rect.height, + ...rect, color, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; }, - prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }), + prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 860f3f2082..fc4b010067 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,3 +1,4 @@ +import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { zBeginEndStepPct, zCLIPVisionModelV2, @@ -243,7 +244,7 @@ const zInpaintMaskData = z.object({ }); export type InpaintMaskData = z.infer; -const zFilter = z.enum(['none', 'lightness_to_alpha']); +const zFilter = z.enum(['none', LightnessToAlphaFilter.name]); export type Filter = z.infer; const zControlAdapterData = z.object({ @@ -271,21 +272,12 @@ export type ControlAdapterConfig = Pick< 'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode' >; -const zCanvasItemIdentifier = z.object({ - type: z.enum([ - zLayerData.shape.type.value, - zIPAdapterData.shape.type.value, - zControlAdapterData.shape.type.value, - zRegionalGuidanceData.shape.type.value, - zInpaintMaskData.shape.type.value, - ]), - id: zId, -}); -type CanvasItemIdentifier = z.infer; +export type CanvasEntity = LayerData | IPAdapterData | ControlAdapterData | RegionalGuidanceData | InpaintMaskData; +export type CanvasEntityIdentifier = Pick; export type CanvasV2State = { _version: 3; - lastSelectedItem: CanvasItemIdentifier | null; + selectedEntityIdentifier: CanvasEntityIdentifier | null; prompts: { positivePrompt: ParameterPositivePrompt; negativePrompt: ParameterNegativePrompt; @@ -314,11 +306,17 @@ export type CanvasV2State = { }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; -export type AddEraserLineArg = { id: string; points: [number, number, number, number]; width: number }; -export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor }; -export type AddPointToLineArg = { id: string; point: [number, number] }; -export type AddRectShapeArg = { id: string; rect: IRect; color: RgbaColor }; -export type AddImageObjectArg = { id: string; imageDTO: ImageDTO }; +export type PosChangedArg = { id: string; x: number; y: number }; +export type BboxChangedArg = { id: string; bbox: IRect }; +export type EraserLineAddedArg = { + id: string; + points: [number, number, number, number]; + width: number; +}; +export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor }; +export type PointAddedToLineArg = { id: string; point: [number, number] }; +export type RectShapeAddedArg = { id: string; rect: IRect; color: RgbaColor }; +export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO }; //#region Type guards export const isLine = (obj: LayerObject): obj is BrushLine | EraserLine => { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index e58c0eaad7..7ff8cb1b9e 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -34,7 +34,7 @@ const selectImageUsages = createMemoizedSelector( const { imagesToDelete } = deleteImageModal; const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) => - getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name) + getImageUsage(canvas, nodes, controlAdapters, canvasV2, image_name) ); const imageUsageSummary: ImageUsage = { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 0a2a0587c7..456872e23b 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -84,7 +84,7 @@ export const selectImageUsage = createMemoizedSelector( } const imagesUsage = imagesToDelete.map((i) => - getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, i.image_name) + getImageUsage(canvas, nodes, controlAdapters, canvasV2, i.image_name) ); return imagesUsage; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index a0fa496ea6..87e78f22b4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -45,7 +45,7 @@ const DeleteBoardModal = (props: Props) => { [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectCanvasV2Slice], (canvas, nodes, controlAdapters, controlLayers) => { const allImageUsage = (boardImageNames ?? []).map((imageName) => - getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName) + getImageUsage(canvas, nodes, controlAdapters, canvasV2, imageName) ); const imageUsageSummary: ImageUsage = { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index 28eb083844..268ec55351 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -10,7 +10,7 @@ import { CANVAS_COHERENCE_NOISE, METADATA, NOISE, POSITIVE_CONDITIONING } from ' export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, prepend: boolean): BatchConfig => { const { iterations, model, shouldRandomizeSeed, seed } = state.generation; - const { shouldConcatPrompts } = state.controlLayers.present; + const { shouldConcatPrompts } = state.canvasV2; const { prompts, seedBehaviour } = state.dynamicPrompts; const data: Batch['data'] = []; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts index f130aa5671..9ee41158b4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts @@ -73,7 +73,7 @@ export const addControlLayers = async ( ): Promise => { const isSDXL = base === 'sdxl'; - const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, base)); + const validLayers = state.canvasV2.layers.filter((l) => isValidLayer(l, base)); const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter); for (const ca of validControlAdapters) { @@ -259,7 +259,7 @@ export const addControlLayers = async ( } } - g.upsertMetadata({ control_layers: { layers: validLayers, version: state.controlLayers.present._version } }); + g.upsertMetadata({ control_layers: { layers: validLayers, version: state.canvasV2._version } }); return validLayers; }; //#endregion @@ -421,7 +421,7 @@ const addInitialImageLayerToGraph = ( ) => { const { vaePrecision } = state.generation; const { refinerModel, refinerStart } = state.sdxl; - const { width, height } = state.controlLayers.present.size; + const { width, height } = state.canvasV2.size; assert(layer.isEnabled, 'Initial image layer is not enabled'); assert(layer.image, 'Initial image layer has no image'); @@ -567,8 +567,8 @@ const buildControlImage = ( */ const getRGLayerBlobs = async (layerIds?: string[], preview: boolean = false): Promise> => { const state = getStore().getState(); - const { layers } = state.controlLayers.present; - const { width, height } = state.controlLayers.present.size; + const { layers } = state.canvasV2; + const { width, height } = state.canvasV2.size; const reduxLayers = layers.filter(isRegionalGuidanceLayer); const container = document.createElement('div'); const stage = new Konva.Stage({ container, width, height }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts index 68286e337c..e03d54ebc2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts @@ -74,7 +74,7 @@ export const addHRF = ( vaeSource: Invocation<'vae_loader'> | Invocation<'main_model_loader'> | Invocation<'seamless'> ): Invocation<'l2i'> => { const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; - const { width, height } = state.controlLayers.present.size; + const { width, height } = state.canvasV2.size; const optimalDimension = selectOptimalDimension(state); const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index ed6dfbc224..0c40e98052 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -22,7 +22,7 @@ export const getPresetModifiedPrompts = ( state: RootState ): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => { const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = - state.controlLayers.present; + state.canvasV2.prompts; const { activeStylePresetId } = state.stylePreset; if (activeStylePresetId) { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index 0f32bdb435..73141cf325 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -13,7 +13,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamNegativePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt); + const prompt = useAppSelector((s) => s.canvasV2.prompts.negativePrompt); const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx index ae79064bcf..32c66231b9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -17,7 +17,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamPositivePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt); + const prompt = useAppSelector((s) => s.canvasV2.positivePrompt); const baseModel = useAppSelector((s) => s.generation.model)?.base; const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index 1dc0533ad8..9c2d8ebebe 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -15,7 +15,7 @@ const selectPromptsCount = createSelector( selectCanvasV2Slice, selectDynamicPromptsSlice, (controlLayers, dynamicPrompts) => - getShouldProcessPrompt(controlLayers.present.positivePrompt) ? dynamicPrompts.prompts.length : 1 + getShouldProcessPrompt(canvasV2.positivePrompt) ? dynamicPrompts.prompts.length : 1 ); type Props = { diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx index 3167cbb624..1f22c1b845 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx @@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLNegativeStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt2); + const prompt = useAppSelector((s) => s.canvasV2.negativePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx index 272323797a..0b86f3014d 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLPositiveStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt2); + const prompt = useAppSelector((s) => s.canvasV2.positivePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx index 0af3dfcee4..dc3b24402b 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi'; export const SDXLConcatButton = memo(() => { - const shouldConcatPrompts = useAppSelector((s) => s.controlLayers.present.shouldConcatPrompts); + const shouldConcatPrompts = useAppSelector((s) => s.canvasV2.shouldConcatPrompts); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index fbb6ca74de..61453be0ba 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -42,7 +42,7 @@ const selector = createMemoizedSelector( badges.push('locked'); } } else { - const { aspectRatio, width, height } = controlLayers.present.size; + const { aspectRatio, width, height } = canvasV2.size; badges.push(`${width}×${height}`); badges.push(aspectRatio.id); if (aspectRatio.isLocked) { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx index 3c8f274ecb..658f47196a 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx @@ -9,9 +9,9 @@ import { memo, useCallback } from 'react'; export const ImageSizeLinear = memo(() => { const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.controlLayers.present.size.width); - const height = useAppSelector((s) => s.controlLayers.present.size.height); - const aspectRatioState = useAppSelector((s) => s.controlLayers.present.size.aspectRatio); + const width = useAppSelector((s) => s.canvasV2.size.width); + const height = useAppSelector((s) => s.canvasV2.size.height); + const aspectRatioState = useAppSelector((s) => s.canvasV2.size.aspectRatio); const onChangeWidth = useCallback( (width: number) => { diff --git a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts index 121840db67..4a136d1d71 100644 --- a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts +++ b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts @@ -10,7 +10,7 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s }; export const usePresetModifiedPrompts = () => { - const { positivePrompt, negativePrompt } = useAppSelector((s) => s.controlLayers.present); + const { positivePrompt, negativePrompt } = useAppSelector((s) => s.canvasV2.prompts); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 166005663f..6a98798f90 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -44,7 +44,7 @@ const ParametersPanelTextToImage = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const activeTabName = useAppSelector(activeTabNameSelector); - const controlLayersCount = useAppSelector((s) => s.controlLayers.present.layers.length); + const controlLayersCount = useAppSelector((s) => s.canvasV2.layers.length); const controlLayersTitle = useMemo(() => { if (controlLayersCount === 0) { return t('controlLayers.controlLayers');