diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx index b93364e2ca..dad28b38ea 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx @@ -1,4 +1,5 @@ import { Box, chakra, Flex } from '@chakra-ui/react'; +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; @@ -9,6 +10,12 @@ import useCanvasMouseMove from 'features/canvas/hooks/useCanvasMouseMove'; import useCanvasMouseOut from 'features/canvas/hooks/useCanvasMouseOut'; import useCanvasMouseUp from 'features/canvas/hooks/useCanvasMouseUp'; import useCanvasWheel from 'features/canvas/hooks/useCanvasZoom'; +import { + $isModifyingBoundingBox, + $isMouseOverBoundingBox, + $isMovingStage, + $isTransformingBoundingBox, +} from 'features/canvas/store/canvasNanostore'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { canvasResized } from 'features/canvas/store/canvasSlice'; import { @@ -40,46 +47,27 @@ const selector = createMemoizedSelector( isMaskEnabled, stageScale, shouldShowBoundingBox, - isTransformingBoundingBox, - isMouseOverBoundingBox, - isMovingBoundingBox, stageDimensions, stageCoordinates, tool, - isMovingStage, shouldShowIntermediates, - shouldShowGrid, shouldRestrictStrokesToBox, + shouldShowGrid, shouldAntialias, } = canvas; - let stageCursor: string | undefined = 'none'; - - if (tool === 'move' || isStaging) { - if (isMovingStage) { - stageCursor = 'grabbing'; - } else { - stageCursor = 'grab'; - } - } else if (isTransformingBoundingBox) { - stageCursor = undefined; - } else if (shouldRestrictStrokesToBox && !isMouseOverBoundingBox) { - stageCursor = 'default'; - } - return { isMaskEnabled, - isModifyingBoundingBox: isTransformingBoundingBox || isMovingBoundingBox, shouldShowBoundingBox, shouldShowGrid, stageCoordinates, - stageCursor, stageDimensions, stageScale, tool, isStaging, shouldShowIntermediates, shouldAntialias, + shouldRestrictStrokesToBox, }; } ); @@ -91,29 +79,51 @@ const ChakraStage = chakra(Stage, { const IAICanvas = () => { const { isMaskEnabled, - isModifyingBoundingBox, shouldShowBoundingBox, shouldShowGrid, stageCoordinates, - stageCursor, stageDimensions, stageScale, tool, isStaging, shouldShowIntermediates, shouldAntialias, + shouldRestrictStrokesToBox, } = useAppSelector(selector); useCanvasHotkeys(); const dispatch = useAppDispatch(); const containerRef = useRef(null); const stageRef = useRef(null); const canvasBaseLayerRef = useRef(null); - + const isModifyingBoundingBox = useStore($isModifyingBoundingBox); + const isMovingStage = useStore($isMovingStage); + const isTransformingBoundingBox = useStore($isTransformingBoundingBox); + const isMouseOverBoundingBox = useStore($isMouseOverBoundingBox); const canvasStageRefCallback = useCallback((el: Konva.Stage) => { setCanvasStage(el as Konva.Stage); stageRef.current = el; }, []); - + const stageCursor = useMemo(() => { + if (tool === 'move' || isStaging) { + if (isMovingStage) { + return 'grabbing'; + } else { + return 'grab'; + } + } else if (isTransformingBoundingBox) { + return undefined; + } else if (shouldRestrictStrokesToBox && !isMouseOverBoundingBox) { + return 'default'; + } + return 'none'; + }, [ + isMouseOverBoundingBox, + isMovingStage, + isStaging, + isTransformingBoundingBox, + shouldRestrictStrokesToBox, + tool, + ]); const canvasBaseLayerRefCallback = useCallback((el: Konva.Layer) => { setCanvasBaseLayer(el as Konva.Layer); canvasBaseLayerRef.current = el; @@ -132,10 +142,9 @@ const IAICanvas = () => { didMouseMoveRef, lastCursorPositionRef ); - const handleMouseOut = useCanvasMouseOut(); const { handleDragStart, handleDragMove, handleDragEnd } = useCanvasDragMove(); - + const handleMouseOut = useCanvasMouseOut(); const handleContextMenu = useCallback( (e: KonvaEventObject) => e.evt.preventDefault(), [] diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx index 0b3df801ee..110fb4eef5 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx @@ -1,24 +1,18 @@ import { Box } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { stateSelector } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $cursorPosition } from 'features/canvas/store/canvasNanostore'; import roundToHundreth from 'features/canvas/util/roundToHundreth'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -const cursorPositionSelector = createSelector([stateSelector], ({ canvas }) => { - const { cursorPosition } = canvas; - - const { cursorX, cursorY } = cursorPosition - ? { cursorX: cursorPosition.x, cursorY: cursorPosition.y } - : { cursorX: -1, cursorY: -1 }; - - return `(${roundToHundreth(cursorX)}, ${roundToHundreth(cursorY)})`; -}); - const IAICanvasStatusTextCursorPos = () => { - const cursorCoordinatesString = useAppSelector(cursorPositionSelector); const { t } = useTranslation(); + const cursorPosition = useStore($cursorPosition); + const cursorCoordinatesString = useMemo(() => { + const x = cursorPosition?.x ?? -1; + const y = cursorPosition?.y ?? -1; + return `(${roundToHundreth(x)}, ${roundToHundreth(y)})`; + }, [cursorPosition?.x, cursorPosition?.y]); return ( {`${t( diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx index d96dcf871a..4b8a2ba781 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx @@ -1,29 +1,31 @@ +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; +import { + $cursorPosition, + $isMovingBoundingBox, + $isTransformingBoundingBox, +} from 'features/canvas/store/canvasNanostore'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { COLOR_PICKER_SIZE, COLOR_PICKER_STROKE_RADIUS, } from 'features/canvas/util/constants'; import type { GroupConfig } from 'konva/lib/Group'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { Circle, Group } from 'react-konva'; const canvasBrushPreviewSelector = createMemoizedSelector( stateSelector, ({ canvas }) => { const { - cursorPosition, brushSize, colorPickerColor, maskColor, brushColor, tool, layer, - shouldShowBrush, - isMovingBoundingBox, - isTransformingBoundingBox, stageScale, stageDimensions, boundingBoxCoordinates, @@ -80,9 +82,6 @@ const canvasBrushPreviewSelector = createMemoizedSelector( // : undefined; return { - cursorPosition, - brushX: cursorPosition ? cursorPosition.x : stageDimensions.width / 2, - brushY: cursorPosition ? cursorPosition.y : stageDimensions.height / 2, radius: brushSize / 2, colorPickerOuterRadius: COLOR_PICKER_SIZE / stageScale, colorPickerInnerRadius: @@ -92,16 +91,10 @@ const canvasBrushPreviewSelector = createMemoizedSelector( colorPickerColorString: rgbaColorToString(colorPickerColor), tool, layer, - shouldShowBrush, - shouldDrawBrushPreview: - !( - isMovingBoundingBox || - isTransformingBoundingBox || - !cursorPosition - ) && shouldShowBrush, strokeWidth: 1.5 / stageScale, dotRadius: 1.5 / stageScale, clip, + stageDimensions, }; } ); @@ -111,13 +104,10 @@ const canvasBrushPreviewSelector = createMemoizedSelector( */ const IAICanvasToolPreview = (props: GroupConfig) => { const { - brushX, - brushY, radius, maskColorString, tool, layer, - shouldDrawBrushPreview, dotRadius, strokeWidth, brushColorString, @@ -125,8 +115,28 @@ const IAICanvasToolPreview = (props: GroupConfig) => { colorPickerInnerRadius, colorPickerOuterRadius, clip, + stageDimensions, } = useAppSelector(canvasBrushPreviewSelector); + const cursorPosition = useStore($cursorPosition); + const isMovingBoundingBox = useStore($isMovingBoundingBox); + const isTransformingBoundingBox = useStore($isTransformingBoundingBox); + + const brushX = useMemo( + () => (cursorPosition ? cursorPosition.x : stageDimensions.width / 2), + [cursorPosition, stageDimensions] + ); + const brushY = useMemo( + () => (cursorPosition ? cursorPosition.y : stageDimensions.height / 2), + [cursorPosition, stageDimensions] + ); + + const shouldDrawBrushPreview = useMemo( + () => + !(isMovingBoundingBox || isTransformingBoundingBox || !cursorPosition), + [cursorPosition, isMovingBoundingBox, isTransformingBoundingBox] + ); + if (!shouldDrawBrushPreview) { return null; } diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx index 42bca1c23f..90ac8b56c0 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx @@ -1,3 +1,4 @@ +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; @@ -6,11 +7,16 @@ import { roundToMultiple, } from 'common/util/roundDownToMultiple'; import { - setBoundingBoxCoordinates, - setBoundingBoxDimensions, + $isDrawing, + $isMovingBoundingBox, + $isTransformingBoundingBox, setIsMouseOverBoundingBox, setIsMovingBoundingBox, setIsTransformingBoundingBox, +} from 'features/canvas/store/canvasNanostore'; +import { + setBoundingBoxCoordinates, + setBoundingBoxDimensions, setShouldSnapToGrid, } from 'features/canvas/store/canvasSlice'; import type Konva from 'konva'; @@ -30,9 +36,6 @@ const boundingBoxPreviewSelector = createMemoizedSelector( boundingBoxCoordinates, boundingBoxDimensions, stageScale, - isDrawing, - isTransformingBoundingBox, - isMovingBoundingBox, tool, shouldSnapToGrid, } = canvas; @@ -42,9 +45,6 @@ const boundingBoxPreviewSelector = createMemoizedSelector( return { boundingBoxCoordinates, boundingBoxDimensions, - isDrawing, - isMovingBoundingBox, - isTransformingBoundingBox, stageScale, shouldSnapToGrid, tool, @@ -63,9 +63,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { const { boundingBoxCoordinates, boundingBoxDimensions, - isDrawing, - isMovingBoundingBox, - isTransformingBoundingBox, + stageScale, shouldSnapToGrid, tool, @@ -75,7 +73,9 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { const transformerRef = useRef(null); const shapeRef = useRef(null); - + const isDrawing = useStore($isDrawing); + const isMovingBoundingBox = useStore($isMovingBoundingBox); + const isTransformingBoundingBox = useStore($isTransformingBoundingBox); const [isMouseOverBoundingBoxOutline, setIsMouseOverBoundingBoxOutline] = useState(false); @@ -209,26 +209,26 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { ); const handleStartedTransforming = useCallback(() => { - dispatch(setIsTransformingBoundingBox(true)); - }, [dispatch]); + setIsTransformingBoundingBox(true); + }, []); const handleEndedTransforming = useCallback(() => { - dispatch(setIsTransformingBoundingBox(false)); - dispatch(setIsMovingBoundingBox(false)); - dispatch(setIsMouseOverBoundingBox(false)); + setIsTransformingBoundingBox(false); + setIsMovingBoundingBox(false); + setIsMouseOverBoundingBox(false); setIsMouseOverBoundingBoxOutline(false); - }, [dispatch]); + }, []); const handleStartedMoving = useCallback(() => { - dispatch(setIsMovingBoundingBox(true)); - }, [dispatch]); + setIsMovingBoundingBox(true); + }, []); const handleEndedModifying = useCallback(() => { - dispatch(setIsTransformingBoundingBox(false)); - dispatch(setIsMovingBoundingBox(false)); - dispatch(setIsMouseOverBoundingBox(false)); + setIsTransformingBoundingBox(false); + setIsMovingBoundingBox(false); + setIsMouseOverBoundingBox(false); setIsMouseOverBoundingBoxOutline(false); - }, [dispatch]); + }, []); const handleMouseOver = useCallback(() => { setIsMouseOverBoundingBoxOutline(true); @@ -241,12 +241,12 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { }, [isMovingBoundingBox, isTransformingBoundingBox]); const handleMouseEnterBoundingBox = useCallback(() => { - dispatch(setIsMouseOverBoundingBox(true)); - }, [dispatch]); + setIsMouseOverBoundingBox(true); + }, []); const handleMouseLeaveBoundingBox = useCallback(() => { - dispatch(setIsMouseOverBoundingBox(false)); - }, [dispatch]); + setIsMouseOverBoundingBox(false); + }, []); return ( diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx index 257abc927f..26e03fdaaf 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx @@ -12,6 +12,7 @@ import { InvPopoverTrigger, } from 'common/components/InvPopover/wrapper'; import { InvSlider } from 'common/components/InvSlider/InvSlider'; +import { resetToolInteractionState } from 'features/canvas/store/canvasNanostore'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { addEraseRect, @@ -176,12 +177,15 @@ const IAICanvasToolChooserOptions = () => { const handleSelectBrushTool = useCallback(() => { dispatch(setTool('brush')); + resetToolInteractionState(); }, [dispatch]); const handleSelectEraserTool = useCallback(() => { dispatch(setTool('eraser')); + resetToolInteractionState(); }, [dispatch]); const handleSelectColorPickerTool = useCallback(() => { dispatch(setTool('colorPicker')); + resetToolInteractionState(); }, [dispatch]); const handleFillRect = useCallback(() => { dispatch(addFillRect()); diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts index 9ffcbaaaad..19005509f7 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts @@ -1,26 +1,25 @@ +import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { + $isMovingBoundingBox, setIsMovingStage, - setStageCoordinates, -} from 'features/canvas/store/canvasSlice'; +} from 'features/canvas/store/canvasNanostore'; +import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; +import { setStageCoordinates } from 'features/canvas/store/canvasSlice'; import type { KonvaEventObject } from 'konva/lib/Node'; import { useCallback } from 'react'; const useCanvasDrag = () => { const dispatch = useAppDispatch(); - const tool = useAppSelector((state) => state.canvas.tool); - const isMovingBoundingBox = useAppSelector( - (state) => state.canvas.isMovingBoundingBox - ); const isStaging = useAppSelector(isStagingSelector); - + const tool = useAppSelector((state) => state.canvas.tool); + const isMovingBoundingBox = useStore($isMovingBoundingBox); const handleDragStart = useCallback(() => { if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) { return; } - dispatch(setIsMovingStage(true)); - }, [dispatch, isMovingBoundingBox, isStaging, tool]); + setIsMovingStage(true); + }, [isMovingBoundingBox, isStaging, tool]); const handleDragMove = useCallback( (e: KonvaEventObject) => { @@ -39,10 +38,14 @@ const useCanvasDrag = () => { if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) { return; } - dispatch(setIsMovingStage(false)); - }, [dispatch, isMovingBoundingBox, isStaging, tool]); + setIsMovingStage(false); + }, [isMovingBoundingBox, isStaging, tool]); - return { handleDragStart, handleDragMove, handleDragEnd }; + return { + handleDragStart, + handleDragMove, + handleDragEnd, + }; }; export default useCanvasDrag; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts index ecdec38af9..16599c566b 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts @@ -1,10 +1,13 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + resetCanvasInteractionState, + resetToolInteractionState, +} from 'features/canvas/store/canvasNanostore'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { clearMask, - resetCanvasInteractionState, setIsMaskEnabled, setShouldShowBoundingBox, setShouldSnapToGrid, @@ -20,7 +23,6 @@ const selector = createMemoizedSelector( [stateSelector, activeTabNameSelector, isStagingSelector], ({ canvas }, activeTabName, isStaging) => { const { - cursorPosition, shouldLockBoundingBox, shouldShowBoundingBox, tool, @@ -30,7 +32,6 @@ const selector = createMemoizedSelector( return { activeTabName, - isCursorOnCanvas: Boolean(cursorPosition), shouldLockBoundingBox, shouldShowBoundingBox, tool, @@ -102,7 +103,7 @@ const useInpaintingCanvasHotkeys = () => { useHotkeys( 'esc', () => { - dispatch(resetCanvasInteractionState()); + resetCanvasInteractionState(); }, { enabled: () => true, @@ -134,6 +135,7 @@ const useInpaintingCanvasHotkeys = () => { if (tool !== 'move') { previousToolRef.current = tool; dispatch(setTool('move')); + resetToolInteractionState(); } if ( diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts index 102a015608..9204dd1517 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts @@ -1,12 +1,12 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { - addLine, setIsDrawing, setIsMovingStage, -} from 'features/canvas/store/canvasSlice'; +} from 'features/canvas/store/canvasNanostore'; +import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; +import { addLine } from 'features/canvas/store/canvasSlice'; import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import type Konva from 'konva'; @@ -42,7 +42,7 @@ const useCanvasMouseDown = (stageRef: MutableRefObject) => { stageRef.current.container().focus(); if (tool === 'move' || isStaging) { - dispatch(setIsMovingStage(true)); + setIsMovingStage(true); return; } @@ -59,7 +59,7 @@ const useCanvasMouseDown = (stageRef: MutableRefObject) => { e.evt.preventDefault(); - dispatch(setIsDrawing(true)); + setIsDrawing(true); // Add a new line starting from the current cursor position. dispatch(addLine([scaledCursorPosition.x, scaledCursorPosition.y])); diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts index 2ed748bd4b..8adc556768 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts @@ -1,11 +1,13 @@ +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { - addPointToCurrentLine, + $isDrawing, setCursorPosition, -} from 'features/canvas/store/canvasSlice'; +} from 'features/canvas/store/canvasNanostore'; +import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; +import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice'; import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import type Konva from 'konva'; @@ -18,10 +20,9 @@ import useColorPicker from './useColorUnderCursor'; const selector = createMemoizedSelector( [activeTabNameSelector, stateSelector, isStagingSelector], (activeTabName, { canvas }, isStaging) => { - const { tool, isDrawing } = canvas; + const { tool } = canvas; return { tool, - isDrawing, activeTabName, isStaging, }; @@ -34,7 +35,8 @@ const useCanvasMouseMove = ( lastCursorPositionRef: MutableRefObject ) => { const dispatch = useAppDispatch(); - const { isDrawing, tool, isStaging } = useAppSelector(selector); + const isDrawing = useStore($isDrawing); + const { tool, isStaging } = useAppSelector(selector); const { updateColorUnderCursor } = useColorPicker(); return useCallback(() => { @@ -48,7 +50,7 @@ const useCanvasMouseMove = ( return; } - dispatch(setCursorPosition(scaledCursorPosition)); + setCursorPosition(scaledCursorPosition); lastCursorPositionRef.current = scaledCursorPosition; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts index fcb3b3223f..0b7220eb0b 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts @@ -1,13 +1,12 @@ -import { useAppDispatch } from 'app/store/storeHooks'; -import { mouseLeftCanvas } from 'features/canvas/store/canvasSlice'; +import { setCanvasInteractionStateMouseOut } from 'features/canvas/store/canvasNanostore'; import { useCallback } from 'react'; const useCanvasMouseOut = () => { - const dispatch = useAppDispatch(); + const onMouseOut = useCallback(() => { + setCanvasInteractionStateMouseOut(); + }, []); - return useCallback(() => { - dispatch(mouseLeftCanvas()); - }, [dispatch]); + return onMouseOut; }; export default useCanvasMouseOut; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts index 979f2933dd..5d81a6cb65 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts @@ -1,27 +1,27 @@ +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + $isDrawing, + setIsDrawing, + setIsMovingStage, +} from 'features/canvas/store/canvasNanostore'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { // addPointToCurrentEraserLine, addPointToCurrentLine, - setIsDrawing, - setIsMovingStage, } from 'features/canvas/store/canvasSlice'; import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import type Konva from 'konva'; import type { MutableRefObject } from 'react'; import { useCallback } from 'react'; const selector = createMemoizedSelector( - [activeTabNameSelector, stateSelector, isStagingSelector], - (activeTabName, { canvas }, isStaging) => { - const { tool, isDrawing } = canvas; + [stateSelector, isStagingSelector], + ({ canvas }, isStaging) => { return { - tool, - isDrawing, - activeTabName, + tool: canvas.tool, isStaging, }; } @@ -32,11 +32,12 @@ const useCanvasMouseUp = ( didMouseMoveRef: MutableRefObject ) => { const dispatch = useAppDispatch(); - const { tool, isDrawing, isStaging } = useAppSelector(selector); + const isDrawing = useStore($isDrawing); + const { tool, isStaging } = useAppSelector(selector); return useCallback(() => { if (tool === 'move' || isStaging) { - dispatch(setIsMovingStage(false)); + setIsMovingStage(false); return; } @@ -59,7 +60,7 @@ const useCanvasMouseUp = ( } else { didMouseMoveRef.current = false; } - dispatch(setIsDrawing(false)); + setIsDrawing(false); }, [didMouseMoveRef, dispatch, isDrawing, isStaging, stageRef, tool]); }; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts index d1423730f9..f86f435842 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts @@ -1,6 +1,8 @@ +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { $isMoveStageKeyHeld } from 'features/canvas/store/canvasNanostore'; import { setStageCoordinates, setStageScale, @@ -16,17 +18,15 @@ import { clamp } from 'lodash-es'; import type { MutableRefObject } from 'react'; import { useCallback } from 'react'; -const selector = createMemoizedSelector([stateSelector], ({ canvas }) => { - const { isMoveStageKeyHeld, stageScale } = canvas; - return { - isMoveStageKeyHeld, - stageScale, - }; -}); +const selector = createMemoizedSelector( + [stateSelector], + (state) => state.canvas.stageScale +); const useCanvasWheel = (stageRef: MutableRefObject) => { const dispatch = useAppDispatch(); - const { isMoveStageKeyHeld, stageScale } = useAppSelector(selector); + const stageScale = useAppSelector(selector); + const isMoveStageKeyHeld = useStore($isMoveStageKeyHeld); return useCallback( (e: KonvaEventObject) => { diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts b/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts new file mode 100644 index 0000000000..dc36ebd8b5 --- /dev/null +++ b/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts @@ -0,0 +1,77 @@ +import type { Vector2d } from 'konva/lib/types'; +import { atom, computed } from 'nanostores'; + +export const $cursorPosition = atom(null); +export const $isDrawing = atom(false); +export const $isMouseOverBoundingBox = atom(false); +export const $isMoveBoundingBoxKeyHeld = atom(false); +export const $isMoveStageKeyHeld = atom(false); +export const $isMovingBoundingBox = atom(false); +export const $isMovingStage = atom(false); +export const $isTransformingBoundingBox = atom(false); +export const $isModifyingBoundingBox = computed( + [$isTransformingBoundingBox, $isMovingBoundingBox], + (isTransformingBoundingBox, isMovingBoundingBox) => + isTransformingBoundingBox || isMovingBoundingBox +); + +export const resetCanvasInteractionState = () => { + $cursorPosition.set(null); + $isDrawing.set(false); + $isMouseOverBoundingBox.set(false); + $isMoveBoundingBoxKeyHeld.set(false); + $isMoveStageKeyHeld.set(false); + $isMovingBoundingBox.set(false); + $isMovingStage.set(false); +}; + +export const setCursorPosition = (cursorPosition: Vector2d | null) => { + $cursorPosition.set(cursorPosition); +}; + +export const setIsDrawing = (isDrawing: boolean) => { + $isDrawing.set(isDrawing); +}; + +export const setIsMouseOverBoundingBox = (isMouseOverBoundingBox: boolean) => { + $isMouseOverBoundingBox.set(isMouseOverBoundingBox); +}; + +export const setIsMoveBoundingBoxKeyHeld = ( + isMoveBoundingBoxKeyHeld: boolean +) => { + $isMoveBoundingBoxKeyHeld.set(isMoveBoundingBoxKeyHeld); +}; + +export const setIsMoveStageKeyHeld = (isMoveStageKeyHeld: boolean) => { + $isMoveStageKeyHeld.set(isMoveStageKeyHeld); +}; + +export const setIsMovingBoundingBox = (isMovingBoundingBox: boolean) => { + $isMovingBoundingBox.set(isMovingBoundingBox); +}; + +export const setIsMovingStage = (isMovingStage: boolean) => { + $isMovingStage.set(isMovingStage); +}; + +export const setIsTransformingBoundingBox = ( + isTransformingBoundingBox: boolean +) => { + $isTransformingBoundingBox.set(isTransformingBoundingBox); +}; + +export const resetToolInteractionState = () => { + setIsTransformingBoundingBox(false); + setIsMouseOverBoundingBox(false); + setIsMovingBoundingBox(false); + setIsMovingStage(false); +}; + +export const setCanvasInteractionStateMouseOut = () => { + setCursorPosition(null); + setIsDrawing(false); + setIsMouseOverBoundingBox(false); + setIsMovingBoundingBox(false); + setIsTransformingBoundingBox(false); +}; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts b/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts index aa536be2e2..9394b685ca 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts @@ -3,4 +3,4 @@ import type { CanvasState } from './canvasTypes'; /** * Canvas slice persist denylist */ -export const canvasPersistDenylist: (keyof CanvasState)[] = ['cursorPosition']; +export const canvasPersistDenylist: (keyof CanvasState)[] = []; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index 2cdff33a69..f1e5a88242 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -36,6 +36,11 @@ import { isCanvasMaskLine, } from './canvasTypes'; +/** + * The maximum history length to keep in the past/future layer states. + */ +const MAX_HISTORY = 128; + export const initialLayerState: CanvasLayerState = { objects: [], stagingArea: { @@ -52,21 +57,11 @@ export const initialCanvasState: CanvasState = { brushColor: { r: 90, g: 90, b: 255, a: 1 }, brushSize: 50, colorPickerColor: { r: 90, g: 90, b: 255, a: 1 }, - cursorPosition: null, futureLayerStates: [], - isDrawing: false, isMaskEnabled: true, - isMouseOverBoundingBox: false, - isMoveBoundingBoxKeyHeld: false, - isMoveStageKeyHeld: false, - isMovingBoundingBox: false, - isMovingStage: false, - isTransformingBoundingBox: false, layer: 'base', layerState: initialLayerState, maskColor: { r: 255, g: 90, b: 90, a: 1 }, - maxHistory: 128, - minimumStageScale: 1, pastLayerStates: [], scaledBoundingBoxDimensions: { width: 512, height: 512 }, shouldAntialias: true, @@ -77,10 +72,7 @@ export const initialCanvasState: CanvasState = { shouldPreserveMaskedArea: false, shouldRestrictStrokesToBox: true, shouldShowBoundingBox: true, - shouldShowBrush: true, - shouldShowBrushPreview: false, shouldShowCanvasDebugInfo: false, - shouldShowCheckboardTransparency: false, shouldShowGrid: true, shouldShowIntermediates: true, shouldShowStagingImage: true, @@ -98,14 +90,7 @@ export const canvasSlice = createSlice({ initialState: initialCanvasState, reducers: { setTool: (state, action: PayloadAction) => { - const tool = action.payload; state.tool = action.payload; - if (tool !== 'move') { - state.isTransformingBoundingBox = false; - state.isMouseOverBoundingBox = false; - state.isMovingBoundingBox = false; - state.isMovingStage = false; - } }, setLayer: (state, action: PayloadAction) => { state.layer = action.payload; @@ -146,21 +131,6 @@ export const canvasSlice = createSlice({ state.isMaskEnabled = action.payload; state.layer = action.payload ? 'mask' : 'base'; }, - setShouldShowCheckboardTransparency: ( - state, - action: PayloadAction - ) => { - state.shouldShowCheckboardTransparency = action.payload; - }, - setShouldShowBrushPreview: (state, action: PayloadAction) => { - state.shouldShowBrushPreview = action.payload; - }, - setShouldShowBrush: (state, action: PayloadAction) => { - state.shouldShowBrush = action.payload; - }, - setCursorPosition: (state, action: PayloadAction) => { - state.cursorPosition = action.payload; - }, setInitialCanvasImage: (state, action: PayloadAction) => { const image = action.payload; const { width, height } = image; @@ -273,9 +243,6 @@ export const canvasSlice = createSlice({ ) => { state.shouldDarkenOutsideBoundingBox = action.payload; }, - setIsDrawing: (state, action: PayloadAction) => { - state.isDrawing = action.payload; - }, clearCanvasHistory: (state) => { state.pastLayerStates = []; state.futureLayerStates = []; @@ -289,21 +256,6 @@ export const canvasSlice = createSlice({ setShouldShowBoundingBox: (state, action: PayloadAction) => { state.shouldShowBoundingBox = action.payload; }, - setIsTransformingBoundingBox: (state, action: PayloadAction) => { - state.isTransformingBoundingBox = action.payload; - }, - setIsMovingBoundingBox: (state, action: PayloadAction) => { - state.isMovingBoundingBox = action.payload; - }, - setIsMouseOverBoundingBox: (state, action: PayloadAction) => { - state.isMouseOverBoundingBox = action.payload; - }, - setIsMoveBoundingBoxKeyHeld: (state, action: PayloadAction) => { - state.isMoveBoundingBoxKeyHeld = action.payload; - }, - setIsMoveStageKeyHeld: (state, action: PayloadAction) => { - state.isMoveStageKeyHeld = action.payload; - }, canvasBatchIdAdded: (state, action: PayloadAction) => { state.batchIds.push(action.payload); }, @@ -333,7 +285,7 @@ export const canvasSlice = createSlice({ state.pastLayerStates.push(cloneDeep(state.layerState)); - if (state.pastLayerStates.length > state.maxHistory) { + if (state.pastLayerStates.length > MAX_HISTORY) { state.pastLayerStates.shift(); } @@ -352,7 +304,7 @@ export const canvasSlice = createSlice({ discardStagedImages: (state) => { state.pastLayerStates.push(cloneDeep(state.layerState)); - if (state.pastLayerStates.length > state.maxHistory) { + if (state.pastLayerStates.length > MAX_HISTORY) { state.pastLayerStates.shift(); } @@ -371,7 +323,7 @@ export const canvasSlice = createSlice({ state.pastLayerStates.push(cloneDeep(state.layerState)); - if (state.pastLayerStates.length > state.maxHistory) { + if (state.pastLayerStates.length > MAX_HISTORY) { state.pastLayerStates.shift(); } @@ -390,7 +342,7 @@ export const canvasSlice = createSlice({ state.pastLayerStates.push(cloneDeep(state.layerState)); - if (state.pastLayerStates.length > state.maxHistory) { + if (state.pastLayerStates.length > MAX_HISTORY) { state.pastLayerStates.shift(); } @@ -419,7 +371,7 @@ export const canvasSlice = createSlice({ state.pastLayerStates.push(cloneDeep(state.layerState)); - if (state.pastLayerStates.length > state.maxHistory) { + if (state.pastLayerStates.length > MAX_HISTORY) { state.pastLayerStates.shift(); } @@ -461,7 +413,7 @@ export const canvasSlice = createSlice({ state.futureLayerStates.unshift(cloneDeep(state.layerState)); - if (state.futureLayerStates.length > state.maxHistory) { + if (state.futureLayerStates.length > MAX_HISTORY) { state.futureLayerStates.pop(); } @@ -476,7 +428,7 @@ export const canvasSlice = createSlice({ state.pastLayerStates.push(cloneDeep(state.layerState)); - if (state.pastLayerStates.length > state.maxHistory) { + if (state.pastLayerStates.length > MAX_HISTORY) { state.pastLayerStates.shift(); } @@ -485,9 +437,6 @@ export const canvasSlice = createSlice({ setShouldShowGrid: (state, action: PayloadAction) => { state.shouldShowGrid = action.payload; }, - setIsMovingStage: (state, action: PayloadAction) => { - state.isMovingStage = action.payload; - }, setShouldSnapToGrid: (state, action: PayloadAction) => { state.shouldSnapToGrid = action.payload; }, @@ -653,7 +602,7 @@ export const canvasSlice = createSlice({ state.pastLayerStates.push(cloneDeep(state.layerState)); - if (state.pastLayerStates.length > state.maxHistory) { + if (state.pastLayerStates.length > MAX_HISTORY) { state.pastLayerStates.shift(); } @@ -773,23 +722,6 @@ export const canvasSlice = createSlice({ state.layerState.objects = [action.payload]; }, - resetCanvasInteractionState: (state) => { - state.cursorPosition = null; - state.isDrawing = false; - state.isMouseOverBoundingBox = false; - state.isMoveBoundingBoxKeyHeld = false; - state.isMoveStageKeyHeld = false; - state.isMovingBoundingBox = false; - state.isMovingStage = false; - state.isTransformingBoundingBox = false; - }, - mouseLeftCanvas: (state) => { - state.cursorPosition = null; - state.isDrawing = false; - state.isMouseOverBoundingBox = false; - state.isMovingBoundingBox = false; - state.isTransformingBoundingBox = false; - }, }, extraReducers: (builder) => { builder.addCase(appSocketQueueItemStatusChanged, (state, action) => { @@ -848,12 +780,10 @@ export const { commitStagingAreaImage, discardStagedImages, fitBoundingBoxToStage, - mouseLeftCanvas, nextStagingAreaImage, prevStagingAreaImage, redo, resetCanvas, - resetCanvasInteractionState, resetCanvasView, setBoundingBoxCoordinates, setBoundingBoxDimensions, @@ -863,16 +793,8 @@ export const { setBrushColor, setBrushSize, setColorPickerColor, - setCursorPosition, setInitialCanvasImage, - setIsDrawing, setIsMaskEnabled, - setIsMouseOverBoundingBox, - setIsMoveBoundingBoxKeyHeld, - setIsMoveStageKeyHeld, - setIsMovingBoundingBox, - setIsMovingStage, - setIsTransformingBoundingBox, setLayer, setMaskColor, setMergedCanvas, @@ -882,10 +804,7 @@ export const { setShouldLockBoundingBox, setShouldPreserveMaskedArea, setShouldShowBoundingBox, - setShouldShowBrush, - setShouldShowBrushPreview, setShouldShowCanvasDebugInfo, - setShouldShowCheckboardTransparency, setShouldShowGrid, setShouldShowIntermediates, setShouldShowStagingImage, diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts index a4efd796df..ae778ae11e 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts @@ -123,21 +123,11 @@ export interface CanvasState { brushColor: RgbaColor; brushSize: number; colorPickerColor: RgbaColor; - cursorPosition: Vector2d | null; futureLayerStates: CanvasLayerState[]; - isDrawing: boolean; isMaskEnabled: boolean; - isMouseOverBoundingBox: boolean; - isMoveBoundingBoxKeyHeld: boolean; - isMoveStageKeyHeld: boolean; - isMovingBoundingBox: boolean; - isMovingStage: boolean; - isTransformingBoundingBox: boolean; layer: CanvasLayer; layerState: CanvasLayerState; maskColor: RgbaColor; - maxHistory: number; - minimumStageScale: number; pastLayerStates: CanvasLayerState[]; scaledBoundingBoxDimensions: Dimensions; shouldAntialias: boolean; @@ -148,10 +138,7 @@ export interface CanvasState { shouldPreserveMaskedArea: boolean; shouldRestrictStrokesToBox: boolean; shouldShowBoundingBox: boolean; - shouldShowBrush: boolean; - shouldShowBrushPreview: boolean; shouldShowCanvasDebugInfo: boolean; - shouldShowCheckboardTransparency: boolean; shouldShowGrid: boolean; shouldShowIntermediates: boolean; shouldShowStagingImage: boolean;