feat(ui): move canvas interaction state to nanostores

This drastically reduces the computation needed when moving the cursor. It also correctly separates ephemeral interaction state from redux, where it is not needed.

Also removed some unused canvas state.
This commit is contained in:
psychedelicious 2024-01-01 12:23:25 +11:00 committed by Kent Keirsey
parent 2a38606342
commit 7c548c5bf3
16 changed files with 258 additions and 251 deletions

View File

@ -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<HTMLDivElement>(null);
const stageRef = useRef<Konva.Stage | null>(null);
const canvasBaseLayerRef = useRef<Konva.Layer | null>(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<MouseEvent>) => e.evt.preventDefault(),
[]

View File

@ -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 (
<Box>{`${t(

View File

@ -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<number>(
() => (cursorPosition ? cursorPosition.x : stageDimensions.width / 2),
[cursorPosition, stageDimensions]
);
const brushY = useMemo<number>(
() => (cursorPosition ? cursorPosition.y : stageDimensions.height / 2),
[cursorPosition, stageDimensions]
);
const shouldDrawBrushPreview = useMemo(
() =>
!(isMovingBoundingBox || isTransformingBoundingBox || !cursorPosition),
[cursorPosition, isMovingBoundingBox, isTransformingBoundingBox]
);
if (!shouldDrawBrushPreview) {
return null;
}

View File

@ -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<Konva.Transformer>(null);
const shapeRef = useRef<Konva.Rect>(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 (
<Group {...rest}>

View File

@ -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());

View File

@ -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<MouseEvent>) => {
@ -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;

View File

@ -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 (

View File

@ -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<Konva.Stage | null>) => {
stageRef.current.container().focus();
if (tool === 'move' || isStaging) {
dispatch(setIsMovingStage(true));
setIsMovingStage(true);
return;
}
@ -59,7 +59,7 @@ const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
e.evt.preventDefault();
dispatch(setIsDrawing(true));
setIsDrawing(true);
// Add a new line starting from the current cursor position.
dispatch(addLine([scaledCursorPosition.x, scaledCursorPosition.y]));

View File

@ -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<Vector2d>
) => {
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;

View File

@ -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;

View File

@ -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<boolean>
) => {
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]);
};

View File

@ -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<Konva.Stage | null>) => {
const dispatch = useAppDispatch();
const { isMoveStageKeyHeld, stageScale } = useAppSelector(selector);
const stageScale = useAppSelector(selector);
const isMoveStageKeyHeld = useStore($isMoveStageKeyHeld);
return useCallback(
(e: KonvaEventObject<WheelEvent>) => {

View File

@ -0,0 +1,77 @@
import type { Vector2d } from 'konva/lib/types';
import { atom, computed } from 'nanostores';
export const $cursorPosition = atom<Vector2d | null>(null);
export const $isDrawing = atom<boolean>(false);
export const $isMouseOverBoundingBox = atom<boolean>(false);
export const $isMoveBoundingBoxKeyHeld = atom<boolean>(false);
export const $isMoveStageKeyHeld = atom<boolean>(false);
export const $isMovingBoundingBox = atom<boolean>(false);
export const $isMovingStage = atom<boolean>(false);
export const $isTransformingBoundingBox = atom<boolean>(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);
};

View File

@ -3,4 +3,4 @@ import type { CanvasState } from './canvasTypes';
/**
* Canvas slice persist denylist
*/
export const canvasPersistDenylist: (keyof CanvasState)[] = ['cursorPosition'];
export const canvasPersistDenylist: (keyof CanvasState)[] = [];

View File

@ -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<CanvasTool>) => {
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<CanvasLayer>) => {
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<boolean>
) => {
state.shouldShowCheckboardTransparency = action.payload;
},
setShouldShowBrushPreview: (state, action: PayloadAction<boolean>) => {
state.shouldShowBrushPreview = action.payload;
},
setShouldShowBrush: (state, action: PayloadAction<boolean>) => {
state.shouldShowBrush = action.payload;
},
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
state.cursorPosition = action.payload;
},
setInitialCanvasImage: (state, action: PayloadAction<ImageDTO>) => {
const image = action.payload;
const { width, height } = image;
@ -273,9 +243,6 @@ export const canvasSlice = createSlice({
) => {
state.shouldDarkenOutsideBoundingBox = action.payload;
},
setIsDrawing: (state, action: PayloadAction<boolean>) => {
state.isDrawing = action.payload;
},
clearCanvasHistory: (state) => {
state.pastLayerStates = [];
state.futureLayerStates = [];
@ -289,21 +256,6 @@ export const canvasSlice = createSlice({
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
state.shouldShowBoundingBox = action.payload;
},
setIsTransformingBoundingBox: (state, action: PayloadAction<boolean>) => {
state.isTransformingBoundingBox = action.payload;
},
setIsMovingBoundingBox: (state, action: PayloadAction<boolean>) => {
state.isMovingBoundingBox = action.payload;
},
setIsMouseOverBoundingBox: (state, action: PayloadAction<boolean>) => {
state.isMouseOverBoundingBox = action.payload;
},
setIsMoveBoundingBoxKeyHeld: (state, action: PayloadAction<boolean>) => {
state.isMoveBoundingBoxKeyHeld = action.payload;
},
setIsMoveStageKeyHeld: (state, action: PayloadAction<boolean>) => {
state.isMoveStageKeyHeld = action.payload;
},
canvasBatchIdAdded: (state, action: PayloadAction<string>) => {
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<boolean>) => {
state.shouldShowGrid = action.payload;
},
setIsMovingStage: (state, action: PayloadAction<boolean>) => {
state.isMovingStage = action.payload;
},
setShouldSnapToGrid: (state, action: PayloadAction<boolean>) => {
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,

View File

@ -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;