diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts index a7491ab01b..ac32d6fa01 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts @@ -4,9 +4,9 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { parseify } from 'common/util/serialize'; import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice'; -import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; import { getCanvasData } from 'features/canvas/util/getCanvasData'; import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode'; +import { blobToDataURL } from "features/controlLayers/konva/util"; import { canvasGraphBuilt } from 'features/nodes/store/actions'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph'; 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 7b579d2a4a..6f918f9959 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 @@ -1,7 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppDispatch, RootState } from 'app/store/store'; -import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { caImageChanged, caProcessedImageChanged, @@ -160,10 +159,6 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening) // We need to reset the features where the image is in use - none of these work if their image(s) don't exist - if (imagesUsage.some((i) => i.isLayerImage)) { - dispatch(resetCanvas()); - } - imageDTOs.forEach((imageDTO) => { deleteNodesImages(state, dispatch, imageDTO); deleteControlAdapterImages(state, dispatch, imageDTO); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 79b4bc69cd..a82f3f265c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; -import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; import { boardIdSelected, galleryViewChanged, @@ -12,7 +11,6 @@ import { } from 'features/gallery/store/gallerySlice'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; -import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; import { getCategories, getListImagesUrl } from 'services/api/util'; @@ -47,11 +45,10 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi imageDTORequest.unsubscribe(); // Add canvas images to the staging area - // TODO(psyche): canvas batchid processing, [] -> canvas.batchIds + // TODO(psyche): canvas batchid processing // if (canvas.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { - if ([].includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { - dispatch(addImageToStagingArea(imageDTO)); - } + // dispatch(addImageToStagingArea(imageDTO)); + // } if (!imageDTO.is_intermediate) { // update the total images for the board diff --git a/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx b/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx deleted file mode 100644 index e49976e532..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Button, ConfirmationAlertDialog, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { clearCanvasHistory } from 'features/canvas/store/canvasSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiTrashSimpleFill } from 'react-icons/pi'; - -const ClearCanvasHistoryButtonModal = () => { - const isStaging = useAppSelector(isStagingSelector); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const { isOpen, onOpen, onClose } = useDisclosure(); - const acceptCallback = useCallback(() => dispatch(clearCanvasHistory()), [dispatch]); - - return ( - <> - - -

{t('unifiedCanvas.clearCanvasHistoryMessage')}

-
-

{t('unifiedCanvas.clearCanvasHistoryConfirm')}

-
- - ); -}; -export default memo(ClearCanvasHistoryButtonModal); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx deleted file mode 100644 index c08b70a783..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { Box, chakra, Flex } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import useCanvasDragMove from 'features/canvas/hooks/useCanvasDragMove'; -import useCanvasHotkeys from 'features/canvas/hooks/useCanvasHotkeys'; -import useCanvasMouseDown from 'features/canvas/hooks/useCanvasMouseDown'; -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 { - $canvasBaseLayer, - $canvasStage, - $isModifyingBoundingBox, - $isMouseOverBoundingBox, - $isMovingStage, - $isTransformingBoundingBox, - $tool, -} from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { canvasResized, selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import type Konva from 'konva'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import type { Vector2d } from 'konva/lib/types'; -import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; -import { Layer, Stage } from 'react-konva'; - -import IAICanvasBoundingBoxOverlay from './IAICanvasBoundingBoxOverlay'; -import IAICanvasGrid from './IAICanvasGrid'; -import IAICanvasIntermediateImage from './IAICanvasIntermediateImage'; -import IAICanvasMaskCompositor from './IAICanvasMaskCompositor'; -import IAICanvasMaskLines from './IAICanvasMaskLines'; -import IAICanvasObjectRenderer from './IAICanvasObjectRenderer'; -import IAICanvasStagingArea from './IAICanvasStagingArea'; -import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar'; -import IAICanvasStatusText from './IAICanvasStatusText'; -import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox'; -import IAICanvasToolPreview from './IAICanvasToolPreview'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - return { - stageCoordinates: canvas.stageCoordinates, - stageDimensions: canvas.stageDimensions, - }; -}); - -const ChakraStage = chakra(Stage, { - shouldForwardProp: (prop) => !['sx'].includes(prop), -}); - -const IAICanvas = () => { - const isStaging = useAppSelector(isStagingSelector); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const shouldShowBoundingBox = useAppSelector((s) => s.canvas.shouldShowBoundingBox); - const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const shouldShowIntermediates = useAppSelector((s) => s.canvas.shouldShowIntermediates); - const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias); - const shouldRestrictStrokesToBox = useAppSelector((s) => s.canvas.shouldRestrictStrokesToBox); - const { stageCoordinates, stageDimensions } = useAppSelector(selector); - 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 tool = useStore($tool); - useCanvasHotkeys(); - const canvasStageRefCallback = useCallback((stageElement: Konva.Stage) => { - $canvasStage.set(stageElement); - stageRef.current = stageElement; - }, []); - 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((layerElement: Konva.Layer) => { - $canvasBaseLayer.set(layerElement); - canvasBaseLayerRef.current = layerElement; - }, []); - - const lastCursorPositionRef = useRef({ x: 0, y: 0 }); - - // Use refs for values that do not affect rendering, other values in redux - const didMouseMoveRef = useRef(false); - - const handleWheel = useCanvasWheel(stageRef); - const handleMouseDown = useCanvasMouseDown(stageRef); - const handleMouseUp = useCanvasMouseUp(stageRef, didMouseMoveRef); - const handleMouseMove = useCanvasMouseMove(stageRef, didMouseMoveRef, lastCursorPositionRef); - const { handleDragStart, handleDragMove, handleDragEnd } = useCanvasDragMove(); - const handleMouseOut = useCanvasMouseOut(); - const handleContextMenu = useCallback((e: KonvaEventObject) => e.evt.preventDefault(), []); - - useEffect(() => { - if (!containerRef.current) { - return; - } - const resizeObserver = new ResizeObserver(() => { - if (!containerRef.current) { - return; - } - const { width, height } = containerRef.current.getBoundingClientRect(); - dispatch(canvasResized({ width, height })); - }); - - resizeObserver.observe(containerRef.current); - const { width, height } = containerRef.current.getBoundingClientRect(); - dispatch(canvasResized({ width, height })); - - return () => { - resizeObserver.disconnect(); - }; - }, [dispatch]); - - const stageStyles = useMemo( - () => ({ - outline: 'none', - overflow: 'hidden', - cursor: stageCursor ? stageCursor : undefined, - canvas: { - outline: 'none', - }, - }), - [stageCursor] - ); - - const scale = useMemo(() => ({ x: stageScale, y: stageScale }), [stageScale]); - - return ( - - - - - - - - - - - - - - - - - - - {!isStaging && } - - {shouldShowIntermediates && } - - - - - - - - ); -}; - -export default memo(IAICanvas); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx deleted file mode 100644 index 3cb75c09c6..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { memo } from 'react'; -import { Group, Rect } from 'react-konva'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { boundingBoxCoordinates, boundingBoxDimensions, stageDimensions, stageCoordinates } = canvas; - - return { - boundingBoxCoordinates, - boundingBoxDimensions, - stageCoordinates, - stageDimensions, - }; -}); - -const IAICanvasBoundingBoxOverlay = () => { - const { boundingBoxCoordinates, boundingBoxDimensions, stageCoordinates, stageDimensions } = useAppSelector(selector); - const shouldDarkenOutsideBoundingBox = useAppSelector((s) => s.canvas.shouldDarkenOutsideBoundingBox); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - - return ( - - - - - ); -}; - -export default memo(IAICanvasBoundingBoxOverlay); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx deleted file mode 100644 index b9105ce9dd..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx +++ /dev/null @@ -1,126 +0,0 @@ -// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/ -import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import type { ReactElement } from 'react'; -import { memo, useCallback, useMemo } from 'react'; -import { Group, Line as KonvaLine } from 'react-konva'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - return { - stageCoordinates: canvas.stageCoordinates, - stageDimensions: canvas.stageDimensions, - }; -}); - -const baseGridLineColor = getArbitraryBaseColor(27); -const fineGridLineColor = getArbitraryBaseColor(18); - -const IAICanvasGrid = () => { - const { stageCoordinates, stageDimensions } = useAppSelector(selector); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - - const gridSpacing = useMemo(() => { - if (stageScale >= 2) { - return 8; - } - if (stageScale >= 1 && stageScale < 2) { - return 16; - } - if (stageScale >= 0.5 && stageScale < 1) { - return 32; - } - return 64; - }, [stageScale]); - - const unscale = useCallback( - (value: number) => { - return value / stageScale; - }, - [stageScale] - ); - - const gridLines = useMemo(() => { - const { width, height } = stageDimensions; - const { x, y } = stageCoordinates; - - const stageRect = { - x1: 0, - y1: 0, - x2: width, - y2: height, - offset: { - x: unscale(x), - y: unscale(y), - }, - }; - - const gridOffset = { - x: Math.ceil(unscale(x) / gridSpacing) * gridSpacing, - y: Math.ceil(unscale(y) / gridSpacing) * gridSpacing, - }; - - const gridRect = { - x1: -gridOffset.x, - y1: -gridOffset.y, - x2: unscale(width) - gridOffset.x + gridSpacing, - y2: unscale(height) - gridOffset.y + gridSpacing, - }; - - const gridFullRect = { - x1: Math.min(stageRect.x1, gridRect.x1), - y1: Math.min(stageRect.y1, gridRect.y1), - x2: Math.max(stageRect.x2, gridRect.x2), - y2: Math.max(stageRect.y2, gridRect.y2), - }; - - const // find the x & y size of the grid - xSize = gridFullRect.x2 - gridFullRect.x1; - const ySize = gridFullRect.y2 - gridFullRect.y1; - // compute the number of steps required on each axis. - const xSteps = Math.round(xSize / gridSpacing) + 1; - const ySteps = Math.round(ySize / gridSpacing) + 1; - - const strokeWidth = unscale(1); - - const gridLines: ReactElement[] = new Array(xSteps + ySteps); - let _x = 0; - let _y = 0; - for (let i = 0; i < xSteps; i++) { - _x = gridFullRect.x1 + i * gridSpacing; - gridLines.push( - - ); - } - - for (let i = 0; i < ySteps; i++) { - _y = gridFullRect.y1 + i * gridSpacing; - gridLines.push( - - ); - } - - return gridLines; - }, [stageDimensions, stageCoordinates, unscale, gridSpacing]); - - return {gridLines}; -}; - -export default memo(IAICanvasGrid); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx deleted file mode 100644 index 75ae983f23..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query'; -import { $authToken } from 'app/store/nanostores/authToken'; -import type { CanvasImage } from 'features/canvas/store/canvasTypes'; -import { memo } from 'react'; -import { Image } from 'react-konva'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import useImage from 'use-image'; - -import IAICanvasImageErrorFallback from './IAICanvasImageErrorFallback'; - -type IAICanvasImageProps = { - canvasImage: CanvasImage; -}; -const IAICanvasImage = (props: IAICanvasImageProps) => { - const { x, y, imageName } = props.canvasImage; - const { currentData: imageDTO, isError } = useGetImageDTOQuery(imageName ?? skipToken); - const [image, status] = useImage(imageDTO?.image_url ?? '', $authToken.get() ? 'use-credentials' : 'anonymous'); - - if (isError || status === 'failed') { - return ; - } - - return ; -}; - -export default memo(IAICanvasImage); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx deleted file mode 100644 index 1606dfa844..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useToken } from '@invoke-ai/ui-library'; -import type { CanvasImage } from 'features/canvas/store/canvasTypes'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Group, Rect, Text } from 'react-konva'; - -type IAICanvasImageErrorFallbackProps = { - canvasImage: CanvasImage; -}; -const IAICanvasImageErrorFallback = ({ canvasImage }: IAICanvasImageErrorFallbackProps) => { - const [rectFill, textFill] = useToken('colors', ['base.500', 'base.900']); - const { t } = useTranslation(); - return ( - - - - - ); -}; - -export default memo(IAICanvasImageErrorFallback); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx deleted file mode 100644 index bd9b93997e..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { selectSystemSlice } from 'features/system/store/systemSlice'; -import { memo, useEffect, useState } from 'react'; -import { Image as KonvaImage } from 'react-konva'; - -const progressImageSelector = createMemoizedSelector([selectSystemSlice, selectCanvasSlice], (system, canvas) => { - const { denoiseProgress } = system; - const { batchIds } = canvas; - - return { - progressImage: - denoiseProgress && batchIds.includes(denoiseProgress.batch_id) ? denoiseProgress.progress_image : undefined, - boundingBox: canvas.layerState.stagingArea.boundingBox, - }; -}); - -const IAICanvasIntermediateImage = () => { - const { progressImage, boundingBox } = useAppSelector(progressImageSelector); - const [loadedImageElement, setLoadedImageElement] = useState(null); - - useEffect(() => { - if (!progressImage) { - return; - } - - const tempImage = new Image(); - - tempImage.onload = () => { - setLoadedImageElement(tempImage); - }; - - tempImage.src = progressImage.dataURL; - }, [progressImage]); - - if (!(progressImage && boundingBox) || !loadedImageElement) { - return null; - } - - return ( - - ); -}; - -export default memo(IAICanvasIntermediateImage); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositor.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositor.tsx deleted file mode 100644 index a339cf5352..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositor.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { getColoredMaskSVG } from 'features/canvas/util/getColoredMaskSVG'; -import type Konva from 'konva'; -import type { RectConfig } from 'konva/lib/shapes/Rect'; -import { isNumber } from 'lodash-es'; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Rect } from 'react-konva'; - -const canvasMaskCompositerSelector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - return { - stageCoordinates: canvas.stageCoordinates, - stageDimensions: canvas.stageDimensions, - }; -}); - -type IAICanvasMaskCompositorProps = RectConfig; - -const IAICanvasMaskCompositor = (props: IAICanvasMaskCompositorProps) => { - const { ...rest } = props; - - const { stageCoordinates, stageDimensions } = useAppSelector(canvasMaskCompositerSelector); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const maskColorString = useAppSelector((s) => rgbaColorToString(s.canvas.maskColor)); - const [fillPatternImage, setFillPatternImage] = useState(null); - - const [offset, setOffset] = useState(0); - - const rectRef = useRef(null); - const incrementOffset = useCallback(() => { - setOffset(offset + 1); - setTimeout(incrementOffset, 500); - }, [offset]); - - useEffect(() => { - if (fillPatternImage) { - return; - } - const image = new Image(); - - image.onload = () => { - setFillPatternImage(image); - }; - image.src = getColoredMaskSVG(maskColorString); - }, [fillPatternImage, maskColorString]); - - useEffect(() => { - if (!fillPatternImage) { - return; - } - fillPatternImage.src = getColoredMaskSVG(maskColorString); - }, [fillPatternImage, maskColorString]); - - useEffect(() => { - const timer = setInterval(() => setOffset((i) => (i + 1) % 5), 50); - return () => clearInterval(timer); - }, []); - - const fillPatternScale = useMemo(() => ({ x: 1 / stageScale, y: 1 / stageScale }), [stageScale]); - - if ( - !fillPatternImage || - !isNumber(stageCoordinates.x) || - !isNumber(stageCoordinates.y) || - !isNumber(stageScale) || - !isNumber(stageDimensions.width) || - !isNumber(stageDimensions.height) - ) { - return null; - } - - return ( - - ); -}; - -export default memo(IAICanvasMaskCompositor); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx deleted file mode 100644 index 27a733cb5e..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { isCanvasMaskLine } from 'features/canvas/store/canvasTypes'; -import type { GroupConfig } from 'konva/lib/Group'; -import { memo } from 'react'; -import { Group, Line } from 'react-konva'; - -type InpaintingCanvasLinesProps = GroupConfig; - -/** - * Draws the lines which comprise the mask. - * - * Uses globalCompositeOperation to handle the brush and eraser tools. - */ -const IAICanvasLines = (props: InpaintingCanvasLinesProps) => { - const objects = useAppSelector((s) => s.canvas.layerState.objects); - - return ( - - {objects.filter(isCanvasMaskLine).map((line, i) => ( - 0 - strokeWidth={line.strokeWidth * 2} - tension={0} - lineCap="round" - lineJoin="round" - shadowForStrokeEnabled={false} - listening={false} - globalCompositeOperation={line.tool === 'brush' ? 'source-over' : 'destination-out'} - /> - ))} - - ); -}; - -export default memo(IAICanvasLines); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx deleted file mode 100644 index 23005a9d24..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { - isCanvasBaseImage, - isCanvasBaseLine, - isCanvasEraseRect, - isCanvasFillRect, -} from 'features/canvas/store/canvasTypes'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { memo } from 'react'; -import { Group, Line, Rect } from 'react-konva'; - -import IAICanvasImage from './IAICanvasImage'; - -const IAICanvasObjectRenderer = () => { - const objects = useAppSelector((s) => s.canvas.layerState.objects); - - return ( - - {objects.map((obj, i) => { - if (isCanvasBaseImage(obj)) { - return ; - } else if (isCanvasBaseLine(obj)) { - const line = ( - 0 - strokeWidth={obj.strokeWidth * 2} - tension={0} - lineCap="round" - lineJoin="round" - shadowForStrokeEnabled={false} - listening={false} - globalCompositeOperation={obj.tool === 'brush' ? 'source-over' : 'destination-out'} - /> - ); - if (obj.clip) { - return ( - - {line} - - ); - } else { - return line; - } - } else if (isCanvasFillRect(obj)) { - return ( - - ); - } else if (isCanvasEraseRect(obj)) { - return ( - - ); - } - })} - - ); -}; - -export default memo(IAICanvasObjectRenderer); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx deleted file mode 100644 index 8b9b580e63..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import type { GroupConfig } from 'konva/lib/Group'; -import { memo } from 'react'; -import { Group, Rect } from 'react-konva'; - -import IAICanvasImage from './IAICanvasImage'; - -const dash = [4, 4]; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { - layerState, - shouldShowStagingImage, - shouldShowStagingOutline, - boundingBoxCoordinates: stageBoundingBoxCoordinates, - boundingBoxDimensions: stageBoundingBoxDimensions, - } = canvas; - - const { selectedImageIndex, images, boundingBox } = layerState.stagingArea; - - return { - currentStagingAreaImage: - images.length > 0 && selectedImageIndex !== undefined ? images[selectedImageIndex] : undefined, - isOnFirstImage: selectedImageIndex === 0, - isOnLastImage: selectedImageIndex === images.length - 1, - shouldShowStagingImage, - shouldShowStagingOutline, - x: boundingBox?.x ?? stageBoundingBoxCoordinates.x, - y: boundingBox?.y ?? stageBoundingBoxCoordinates.y, - width: boundingBox?.width ?? stageBoundingBoxDimensions.width, - height: boundingBox?.height ?? stageBoundingBoxDimensions.height, - }; -}); - -type Props = GroupConfig; - -const IAICanvasStagingArea = (props: Props) => { - const { currentStagingAreaImage, shouldShowStagingImage, shouldShowStagingOutline, x, y, width, height } = - useAppSelector(selector); - - return ( - - {shouldShowStagingImage && currentStagingAreaImage && } - {shouldShowStagingOutline && ( - - - - - )} - - ); -}; - -export default memo(IAICanvasStagingArea); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx deleted file mode 100644 index ed39459976..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { Button, ButtonGroup, Flex, IconButton } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { stagingAreaImageSaved } from 'features/canvas/store/actions'; -import { - commitStagingAreaImage, - discardStagedImage, - discardStagedImages, - nextStagingAreaImage, - prevStagingAreaImage, - selectCanvasSlice, - setShouldShowStagingImage, - setShouldShowStagingOutline, -} from 'features/canvas/store/canvasSlice'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiArrowLeftBold, - PiArrowRightBold, - PiCheckBold, - PiEyeBold, - PiEyeSlashBold, - PiFloppyDiskBold, - PiTrashSimpleBold, - PiXBold, -} from 'react-icons/pi'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { - layerState: { - stagingArea: { images, selectedImageIndex }, - }, - shouldShowStagingOutline, - shouldShowStagingImage, - } = canvas; - - return { - currentIndex: selectedImageIndex, - total: images.length, - currentStagingAreaImage: images.length > 0 ? images[selectedImageIndex] : undefined, - shouldShowStagingImage, - shouldShowStagingOutline, - }; -}); - -const ClearStagingIntermediatesIconButton = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const totalStagedImages = useAppSelector((s) => s.canvas.layerState.stagingArea.images.length); - - const handleDiscardStagingArea = useCallback(() => { - dispatch(discardStagedImages()); - }, [dispatch]); - - const handleDiscardStagingImage = useCallback(() => { - // Discarding all staged images triggers cancelation of all canvas batches. It's too easy to accidentally - // click the discard button, so to prevent accidental cancelation of all batches, we only discard the current - // image if there are more than one staged images. - if (totalStagedImages > 1) { - dispatch(discardStagedImage()); - } - }, [dispatch, totalStagedImages]); - - return ( - <> - } - onClick={handleDiscardStagingImage} - colorScheme="invokeBlue" - fontSize={16} - isDisabled={totalStagedImages <= 1} - /> - } - onClick={handleDiscardStagingArea} - colorScheme="error" - fontSize={16} - /> - - ); -}; - -const IAICanvasStagingAreaToolbar = () => { - const dispatch = useAppDispatch(); - const { currentStagingAreaImage, shouldShowStagingImage, currentIndex, total } = useAppSelector(selector); - - const { t } = useTranslation(); - - const handleMouseOver = useCallback(() => { - dispatch(setShouldShowStagingOutline(true)); - }, [dispatch]); - - const handleMouseOut = useCallback(() => { - dispatch(setShouldShowStagingOutline(false)); - }, [dispatch]); - - const handlePrevImage = useCallback(() => dispatch(prevStagingAreaImage()), [dispatch]); - - const handleNextImage = useCallback(() => dispatch(nextStagingAreaImage()), [dispatch]); - - const handleAccept = useCallback(() => dispatch(commitStagingAreaImage()), [dispatch]); - - useHotkeys(['left'], handlePrevImage, { - enabled: () => true, - preventDefault: true, - }); - - useHotkeys(['right'], handleNextImage, { - enabled: () => true, - preventDefault: true, - }); - - useHotkeys(['enter'], handleAccept, { - enabled: () => true, - preventDefault: true, - }); - - useHotkeys( - ['esc'], - () => { - handleDiscardStagingArea(); - }, - { - preventDefault: true, - } - ); - - const { data: imageDTO } = useGetImageDTOQuery(currentStagingAreaImage?.imageName ?? skipToken); - - const handleToggleShouldShowStagingImage = useCallback(() => { - dispatch(setShouldShowStagingImage(!shouldShowStagingImage)); - }, [dispatch, shouldShowStagingImage]); - - const handleSaveToGallery = useCallback(() => { - if (!imageDTO) { - return; - } - - dispatch( - stagingAreaImageSaved({ - imageDTO, - }) - ); - }, [dispatch, imageDTO]); - - useHotkeys( - ['shift+s'], - () => { - shouldShowStagingImage && handleSaveToGallery(); - }, - { - preventDefault: true, - }, - [shouldShowStagingImage, handleSaveToGallery] - ); - - const handleDiscardStagingArea = useCallback(() => { - dispatch(discardStagedImages()); - }, [dispatch]); - - if (!currentStagingAreaImage) { - return null; - } - - return ( - - - } - onClick={handlePrevImage} - colorScheme="invokeBlue" - isDisabled={!shouldShowStagingImage} - /> - - } - onClick={handleNextImage} - colorScheme="invokeBlue" - isDisabled={!shouldShowStagingImage} - /> - - - } - onClick={handleAccept} - colorScheme="invokeBlue" - /> - : } - onClick={handleToggleShouldShowStagingImage} - colorScheme="invokeBlue" - /> - } - onClick={handleSaveToGallery} - colorScheme="invokeBlue" - /> - - - - ); -}; - -export default memo(IAICanvasStagingAreaToolbar); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx deleted file mode 100644 index 4c8153b83f..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import roundToHundreth from 'features/canvas/util/roundToHundreth'; -import GenerationModeStatusText from 'features/parameters/components/Canvas/GenerationModeStatusText'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import IAICanvasStatusTextCursorPos from './IAICanvasStatusText/IAICanvasStatusTextCursorPos'; - -const warningColor = 'var(--invoke-colors-warning-500)'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { - stageDimensions: { width: stageWidth, height: stageHeight }, - stageCoordinates: { x: stageX, y: stageY }, - boundingBoxDimensions: { width: boxWidth, height: boxHeight }, - scaledBoundingBoxDimensions: { width: scaledBoxWidth, height: scaledBoxHeight }, - boundingBoxCoordinates: { x: boxX, y: boxY }, - stageScale, - shouldShowCanvasDebugInfo, - layer, - boundingBoxScaleMethod, - shouldPreserveMaskedArea, - } = canvas; - - let boundingBoxColor = 'inherit'; - - if ( - (boundingBoxScaleMethod === 'none' && (boxWidth < 512 || boxHeight < 512)) || - (boundingBoxScaleMethod === 'manual' && scaledBoxWidth * scaledBoxHeight < 512 * 512) - ) { - boundingBoxColor = warningColor; - } - - const activeLayerColor = layer === 'mask' ? warningColor : 'inherit'; - - return { - activeLayerColor, - layer, - boundingBoxColor, - boundingBoxCoordinatesString: `(${roundToHundreth(boxX)}, ${roundToHundreth(boxY)})`, - boundingBoxDimensionsString: `${boxWidth}×${boxHeight}`, - scaledBoundingBoxDimensionsString: `${scaledBoxWidth}×${scaledBoxHeight}`, - canvasCoordinatesString: `${roundToHundreth(stageX)}×${roundToHundreth(stageY)}`, - canvasDimensionsString: `${stageWidth}×${stageHeight}`, - canvasScaleString: Math.round(stageScale * 100), - shouldShowCanvasDebugInfo, - shouldShowBoundingBox: boundingBoxScaleMethod !== 'auto', - shouldShowScaledBoundingBox: boundingBoxScaleMethod !== 'none', - shouldPreserveMaskedArea, - }; -}); - -const IAICanvasStatusText = () => { - const { - activeLayerColor, - layer, - boundingBoxColor, - boundingBoxCoordinatesString, - boundingBoxDimensionsString, - scaledBoundingBoxDimensionsString, - shouldShowScaledBoundingBox, - canvasCoordinatesString, - canvasDimensionsString, - canvasScaleString, - shouldShowCanvasDebugInfo, - shouldShowBoundingBox, - shouldPreserveMaskedArea, - } = useAppSelector(selector); - - const { t } = useTranslation(); - - return ( - - - {`${t('unifiedCanvas.activeLayer')}: ${t(`unifiedCanvas.${layer}`)}`} - {`${t('unifiedCanvas.canvasScale')}: ${canvasScaleString}%`} - {shouldPreserveMaskedArea && ( - - {t('unifiedCanvas.preserveMaskedArea')}: {t('common.on')} - - )} - {shouldShowBoundingBox && ( - {`${t('unifiedCanvas.boundingBox')}: ${boundingBoxDimensionsString}`} - )} - {shouldShowScaledBoundingBox && ( - {`${t( - 'unifiedCanvas.scaledBoundingBox' - )}: ${scaledBoundingBoxDimensionsString}`} - )} - {shouldShowCanvasDebugInfo && ( - <> - {`${t('unifiedCanvas.boundingBoxPosition')}: ${boundingBoxCoordinatesString}`} - {`${t('unifiedCanvas.canvasDimensions')}: ${canvasDimensionsString}`} - {`${t('unifiedCanvas.canvasPosition')}: ${canvasCoordinatesString}`} - - - )} - - ); -}; - -export default memo(IAICanvasStatusText); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx deleted file mode 100644 index a7e9ecb157..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Box } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { $cursorPosition } from 'features/canvas/store/canvasNanostore'; -import roundToHundreth from 'features/canvas/util/roundToHundreth'; -import { memo, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -const IAICanvasStatusTextCursorPos = () => { - 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('unifiedCanvas.cursorPosition')}: ${cursorCoordinatesString}`}; -}; - -export default memo(IAICanvasStatusTextCursorPos); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx deleted file mode 100644 index be1a4c2d4c..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useStore } from '@nanostores/react'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - $cursorPosition, - $isMovingBoundingBox, - $isTransformingBoundingBox, - $tool, -} from 'features/canvas/store/canvasNanostore'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -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, useMemo } from 'react'; -import { Circle, Group } from 'react-konva'; - -const canvasBrushPreviewSelector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { stageDimensions, boundingBoxCoordinates, boundingBoxDimensions, shouldRestrictStrokesToBox } = canvas; - - const clip = shouldRestrictStrokesToBox - ? { - clipX: boundingBoxCoordinates.x, - clipY: boundingBoxCoordinates.y, - clipWidth: boundingBoxDimensions.width, - clipHeight: boundingBoxDimensions.height, - } - : {}; - - // // big brain time; this is the *inverse* of the clip that is needed for shouldRestrictStrokesToBox - // // it took some fiddling to work out, so I am leaving it here in case it is needed for something else... - // const clipFunc = shouldRestrictStrokesToBox - // ? (ctx: SceneContext) => { - // console.log( - // stageCoordinates.x / stageScale, - // stageCoordinates.y / stageScale, - // stageDimensions.height / stageScale, - // stageDimensions.width / stageScale - // ); - // ctx.fillStyle = 'red'; - // ctx.rect( - // -stageCoordinates.x / stageScale, - // -stageCoordinates.y / stageScale, - // stageDimensions.width / stageScale, - // stageCoordinates.y / stageScale + boundingBoxCoordinates.y - // ); - // ctx.rect( - // -stageCoordinates.x / stageScale, - // boundingBoxCoordinates.y + boundingBoxDimensions.height, - // stageDimensions.width / stageScale, - // stageDimensions.height / stageScale - // ); - // ctx.rect( - // -stageCoordinates.x / stageScale, - // -stageCoordinates.y / stageScale, - // stageCoordinates.x / stageScale + boundingBoxCoordinates.x, - // stageDimensions.height / stageScale - // ); - // ctx.rect( - // boundingBoxCoordinates.x + boundingBoxDimensions.width, - // -stageCoordinates.y / stageScale, - // stageDimensions.width / stageScale - - // (boundingBoxCoordinates.x + boundingBoxDimensions.width), - // stageDimensions.height / stageScale - // ); - // } - // : undefined; - - return { - clip, - stageDimensions, - }; -}); - -/** - * Draws a black circle around the canvas brush preview. - */ -const IAICanvasToolPreview = (props: GroupConfig) => { - const radius = useAppSelector((s) => s.canvas.brushSize / 2); - const maskColorString = useAppSelector((s) => rgbaColorToString({ ...s.canvas.maskColor, a: 0.5 })); - const tool = useStore($tool); - const layer = useAppSelector((s) => s.canvas.layer); - const dotRadius = useAppSelector((s) => 1.5 / s.canvas.stageScale); - const strokeWidth = useAppSelector((s) => 1.5 / s.canvas.stageScale); - const brushColorString = useAppSelector((s) => rgbaColorToString(s.canvas.brushColor)); - const colorPickerColorString = useAppSelector((s) => rgbaColorToString(s.canvas.colorPickerColor)); - const colorPickerInnerRadius = useAppSelector( - (s) => (COLOR_PICKER_SIZE - COLOR_PICKER_STROKE_RADIUS + 1) / s.canvas.stageScale - ); - const colorPickerOuterRadius = useAppSelector((s) => COLOR_PICKER_SIZE / s.canvas.stageScale); - const { 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; - } - - return ( - - {tool === 'colorPicker' ? ( - <> - - - - ) : ( - <> - - - - - )} - - - - ); -}; - -export default memo(IAICanvasToolPreview); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx deleted file mode 100644 index 4650506a42..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { useShiftModifier } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { roundDownToMultiple, roundDownToMultipleMin, roundToMultiple } from 'common/util/roundDownToMultiple'; -import { - $isDrawing, - $isMouseOverBoundingBox, - $isMouseOverBoundingBoxOutline, - $isMovingBoundingBox, - $isTransformingBoundingBox, - $tool, -} from 'features/canvas/store/canvasNanostore'; -import { - aspectRatioChanged, - setBoundingBoxCoordinates, - setBoundingBoxDimensions, - setShouldSnapToGrid, -} from 'features/canvas/store/canvasSlice'; -import { CANVAS_GRID_SIZE_COARSE, CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/constants'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import type Konva from 'konva'; -import type { GroupConfig } from 'konva/lib/Group'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import type { Vector2d } from 'konva/lib/types'; -import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { Group, Rect, Transformer } from 'react-konva'; - -const borderDash = [4, 4]; - -type IAICanvasBoundingBoxPreviewProps = GroupConfig; - -const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { - const { ...rest } = props; - const dispatch = useAppDispatch(); - const boundingBoxCoordinates = useAppSelector((s) => s.canvas.boundingBoxCoordinates); - const boundingBoxDimensions = useAppSelector((s) => s.canvas.boundingBoxDimensions); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid); - const hitStrokeWidth = useAppSelector((s) => 20 / s.canvas.stageScale); - const aspectRatio = useAppSelector((s) => s.canvas.aspectRatio); - const optimalDimension = useAppSelector(selectOptimalDimension); - const transformerRef = useRef(null); - const shapeRef = useRef(null); - const shift = useShiftModifier(); - const tool = useStore($tool); - const isDrawing = useStore($isDrawing); - const isMovingBoundingBox = useStore($isMovingBoundingBox); - const isTransformingBoundingBox = useStore($isTransformingBoundingBox); - const isMouseOverBoundingBoxOutline = useStore($isMouseOverBoundingBoxOutline); - - useEffect(() => { - if (!transformerRef.current || !shapeRef.current) { - return; - } - transformerRef.current.nodes([shapeRef.current]); - transformerRef.current.getLayer()?.batchDraw(); - }, []); - - const gridSize = useMemo(() => (shift ? CANVAS_GRID_SIZE_FINE : CANVAS_GRID_SIZE_COARSE), [shift]); - const scaledStep = useMemo(() => gridSize * stageScale, [gridSize, stageScale]); - - useHotkeys( - 'N', - () => { - dispatch(setShouldSnapToGrid(!shouldSnapToGrid)); - }, - [shouldSnapToGrid] - ); - - const handleOnDragMove = useCallback( - (e: KonvaEventObject) => { - if (!shouldSnapToGrid) { - dispatch( - setBoundingBoxCoordinates({ - x: Math.floor(e.target.x()), - y: Math.floor(e.target.y()), - }) - ); - return; - } - - const dragX = e.target.x(); - const dragY = e.target.y(); - - const newX = roundToMultiple(dragX, gridSize); - const newY = roundToMultiple(dragY, gridSize); - - e.target.x(newX); - e.target.y(newY); - - dispatch( - setBoundingBoxCoordinates({ - x: newX, - y: newY, - }) - ); - }, - [dispatch, gridSize, shouldSnapToGrid] - ); - - const handleOnTransform = useCallback( - (_e: KonvaEventObject) => { - /** - * The Konva Transformer changes the object's anchor point and scale factor, - * not its width and height. We need to un-scale the width and height before - * setting the values. - */ - if (!shapeRef.current) { - return; - } - - const rect = shapeRef.current; - - const scaleX = rect.scaleX(); - const scaleY = rect.scaleY(); - - // undo the scaling - const width = Math.round(rect.width() * scaleX); - const height = Math.round(rect.height() * scaleY); - - const x = Math.round(rect.x()); - const y = Math.round(rect.y()); - - if (aspectRatio.isLocked) { - const newDimensions = calculateNewSize(aspectRatio.value, width * height); - dispatch( - setBoundingBoxDimensions( - { - width: roundDownToMultipleMin(newDimensions.width, gridSize), - height: roundDownToMultipleMin(newDimensions.height, gridSize), - }, - optimalDimension - ) - ); - } else { - dispatch( - setBoundingBoxDimensions( - { - width: roundDownToMultipleMin(width, gridSize), - height: roundDownToMultipleMin(height, gridSize), - }, - optimalDimension - ) - ); - dispatch( - aspectRatioChanged({ - isLocked: false, - id: 'Free', - value: width / height, - }) - ); - } - - dispatch( - setBoundingBoxCoordinates({ - x: shouldSnapToGrid ? roundDownToMultiple(x, gridSize) : x, - y: shouldSnapToGrid ? roundDownToMultiple(y, gridSize) : y, - }) - ); - - // Reset the scale now that the coords/dimensions have been un-scaled - rect.scaleX(1); - rect.scaleY(1); - }, - [aspectRatio.isLocked, aspectRatio.value, dispatch, shouldSnapToGrid, gridSize, optimalDimension] - ); - - const anchorDragBoundFunc = useCallback( - ( - oldPos: Vector2d, // old absolute position of anchor point - newPos: Vector2d, // new absolute position (potentially) of anchor point - _e: MouseEvent - ) => { - /** - * Konva does not transform with width or height. It transforms the anchor point - * and scale factor. This is then sent to the shape's onTransform listeners. - * - * We need to snap the new dimensions to steps of 8 (or 64). But because the whole - * stage is scaled, our actual desired step is actually 8 (or 64) * the stage scale. - * - * Additionally, we need to ensure we offset the position so that we snap to a - * multiple of 8 (or 64) that is aligned with the grid, and not from the absolute zero - * coordinate. - */ - - // Calculate the offset of the grid. - const offsetX = oldPos.x % scaledStep; - const offsetY = oldPos.y % scaledStep; - - const newCoordinates = { - x: roundDownToMultiple(newPos.x, scaledStep) + offsetX, - y: roundDownToMultiple(newPos.y, scaledStep) + offsetY, - }; - - return newCoordinates; - }, - [scaledStep] - ); - - const handleStartedTransforming = useCallback(() => { - $isTransformingBoundingBox.set(true); - }, []); - - const handleEndedTransforming = useCallback(() => { - $isTransformingBoundingBox.set(false); - $isMovingBoundingBox.set(false); - $isMouseOverBoundingBox.set(false); - $isMouseOverBoundingBoxOutline.set(false); - }, []); - - const handleStartedMoving = useCallback(() => { - $isMovingBoundingBox.set(true); - }, []); - - const handleEndedModifying = useCallback(() => { - $isTransformingBoundingBox.set(false); - $isMovingBoundingBox.set(false); - $isMouseOverBoundingBox.set(false); - $isMouseOverBoundingBoxOutline.set(false); - }, []); - - const handleMouseOver = useCallback(() => { - $isMouseOverBoundingBoxOutline.set(true); - }, []); - - const handleMouseOut = useCallback(() => { - !isTransformingBoundingBox && !isMovingBoundingBox && $isMouseOverBoundingBoxOutline.set(false); - }, [isMovingBoundingBox, isTransformingBoundingBox]); - - const handleMouseEnterBoundingBox = useCallback(() => { - $isMouseOverBoundingBox.set(true); - }, []); - - const handleMouseLeaveBoundingBox = useCallback(() => { - $isMouseOverBoundingBox.set(false); - }, []); - - const stroke = useMemo(() => { - if (isMouseOverBoundingBoxOutline || isMovingBoundingBox || isTransformingBoundingBox) { - return 'rgba(255,255,255,0.5)'; - } - return 'white'; - }, [isMouseOverBoundingBoxOutline, isMovingBoundingBox, isTransformingBoundingBox]); - - const strokeWidth = useMemo(() => { - if (isMouseOverBoundingBoxOutline || isMovingBoundingBox || isTransformingBoundingBox) { - return 6 / stageScale; - } - return 1 / stageScale; - }, [isMouseOverBoundingBoxOutline, isMovingBoundingBox, isTransformingBoundingBox, stageScale]); - - const enabledAnchors = useMemo(() => { - if (tool !== 'move') { - return emptyArray; - } - if (aspectRatio.isLocked) { - // TODO: The math to resize the bbox when locked and using other handles is confusing. - // Workaround for now is to only allow resizing from the bottom-right handle. - return ['bottom-right']; - } - return undefined; - }, [aspectRatio.isLocked, tool]); - - return ( - - - - - - ); -}; - -export default memo(IAICanvasBoundingBox); - -const emptyArray: string[] = []; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx deleted file mode 100644 index 6dacb7c59d..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import type { FormLabelProps } from '@invoke-ai/ui-library'; -import { - Box, - Button, - ButtonGroup, - Checkbox, - Flex, - FormControl, - FormControlGroup, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIColorPicker from 'common/components/IAIColorPicker'; -import { canvasMaskSavedToGallery } from 'features/canvas/store/actions'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { - clearMask, - setIsMaskEnabled, - setLayer, - setMaskColor, - setShouldPreserveMaskedArea, -} from 'features/canvas/store/canvasSlice'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import type { RgbaColor } from 'react-colorful'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiExcludeBold, PiFloppyDiskBackFill, PiTrashSimpleFill } from 'react-icons/pi'; - -const formLabelProps: FormLabelProps = { - flexGrow: 1, -}; - -const IAICanvasMaskOptions = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const layer = useAppSelector((s) => s.canvas.layer); - const maskColor = useAppSelector((s) => s.canvas.maskColor); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const shouldPreserveMaskedArea = useAppSelector((s) => s.canvas.shouldPreserveMaskedArea); - const isStaging = useAppSelector(isStagingSelector); - - useHotkeys( - ['q'], - () => { - handleToggleMaskLayer(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [layer] - ); - - useHotkeys( - ['shift+c'], - () => { - handleClearMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['h'], - () => { - handleToggleEnableMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [isMaskEnabled] - ); - - const handleToggleMaskLayer = useCallback(() => { - dispatch(setLayer(layer === 'mask' ? 'base' : 'mask')); - }, [dispatch, layer]); - - const handleClearMask = useCallback(() => { - dispatch(clearMask()); - }, [dispatch]); - - const handleToggleEnableMask = useCallback(() => { - dispatch(setIsMaskEnabled(!isMaskEnabled)); - }, [dispatch, isMaskEnabled]); - - const handleSaveMask = useCallback(async () => { - dispatch(canvasMaskSavedToGallery()); - }, [dispatch]); - - const handleChangePreserveMaskedArea = useCallback( - (e: ChangeEvent) => { - dispatch(setShouldPreserveMaskedArea(e.target.checked)); - }, - [dispatch] - ); - - const handleChangeMaskColor = useCallback( - (newColor: RgbaColor) => { - dispatch(setMaskColor(newColor)); - }, - [dispatch] - ); - - return ( - - - } - isChecked={layer === 'mask'} - isDisabled={isStaging} - /> - - - - - - - {`${t('unifiedCanvas.enableMask')} (H)`} - - - - {t('unifiedCanvas.preserveMaskedArea')} - - - - - - - - - - - - - - - ); -}; - -export default memo(IAICanvasMaskOptions); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton.tsx deleted file mode 100644 index d156944a60..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { redo } from 'features/canvas/store/canvasSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiArrowClockwiseBold } from 'react-icons/pi'; - -const IAICanvasRedoButton = () => { - const dispatch = useAppDispatch(); - const canRedo = useAppSelector((s) => s.canvas.futureLayerStates.length > 0); - const activeTabName = useAppSelector(activeTabNameSelector); - - const { t } = useTranslation(); - - const handleRedo = useCallback(() => { - dispatch(redo()); - }, [dispatch]); - - useHotkeys( - ['meta+shift+z', 'ctrl+shift+z', 'control+y', 'meta+y'], - () => { - handleRedo(); - }, - { - enabled: () => canRedo, - preventDefault: true, - }, - [activeTabName, canRedo] - ); - - return ( - } - onClick={handleRedo} - isDisabled={!canRedo} - /> - ); -}; - -export default memo(IAICanvasRedoButton); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx deleted file mode 100644 index 83ee900a43..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import type { FormLabelProps } from '@invoke-ai/ui-library'; -import { - Checkbox, - Flex, - FormControl, - FormControlGroup, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ClearCanvasHistoryButtonModal from 'features/canvas/components/ClearCanvasHistoryButtonModal'; -import { - setShouldAntialias, - setShouldAutoSave, - setShouldCropToBoundingBoxOnSave, - setShouldDarkenOutsideBoundingBox, - setShouldFitImageSize, - setShouldInvertBrushSizeScrollDirection, - setShouldRestrictStrokesToBox, - setShouldShowCanvasDebugInfo, - setShouldShowGrid, - setShouldShowIntermediates, - setShouldSnapToGrid, -} from 'features/canvas/store/canvasSlice'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiGearSixBold } from 'react-icons/pi'; - -const formLabelProps: FormLabelProps = { - flexGrow: 1, -}; - -const IAICanvasSettingsButtonPopover = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const shouldAutoSave = useAppSelector((s) => s.canvas.shouldAutoSave); - const shouldCropToBoundingBoxOnSave = useAppSelector((s) => s.canvas.shouldCropToBoundingBoxOnSave); - const shouldDarkenOutsideBoundingBox = useAppSelector((s) => s.canvas.shouldDarkenOutsideBoundingBox); - const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); - const shouldShowCanvasDebugInfo = useAppSelector((s) => s.canvas.shouldShowCanvasDebugInfo); - const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid); - const shouldShowIntermediates = useAppSelector((s) => s.canvas.shouldShowIntermediates); - const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid); - const shouldRestrictStrokesToBox = useAppSelector((s) => s.canvas.shouldRestrictStrokesToBox); - const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias); - const shouldFitImageSize = useAppSelector((s) => s.canvas.shouldFitImageSize); - - useHotkeys( - ['n'], - () => { - dispatch(setShouldSnapToGrid(!shouldSnapToGrid)); - }, - { - enabled: true, - preventDefault: true, - }, - [shouldSnapToGrid] - ); - - const handleChangeShouldSnapToGrid = useCallback( - (e: ChangeEvent) => dispatch(setShouldSnapToGrid(e.target.checked)), - [dispatch] - ); - - const handleChangeShouldShowIntermediates = useCallback( - (e: ChangeEvent) => dispatch(setShouldShowIntermediates(e.target.checked)), - [dispatch] - ); - const handleChangeShouldShowGrid = useCallback( - (e: ChangeEvent) => dispatch(setShouldShowGrid(e.target.checked)), - [dispatch] - ); - const handleChangeShouldDarkenOutsideBoundingBox = useCallback( - (e: ChangeEvent) => dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked)), - [dispatch] - ); - const handleChangeShouldInvertBrushSizeScrollDirection = useCallback( - (e: ChangeEvent) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)), - [dispatch] - ); - const handleChangeShouldAutoSave = useCallback( - (e: ChangeEvent) => dispatch(setShouldAutoSave(e.target.checked)), - [dispatch] - ); - const handleChangeShouldCropToBoundingBoxOnSave = useCallback( - (e: ChangeEvent) => dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked)), - [dispatch] - ); - const handleChangeShouldRestrictStrokesToBox = useCallback( - (e: ChangeEvent) => dispatch(setShouldRestrictStrokesToBox(e.target.checked)), - [dispatch] - ); - const handleChangeShouldShowCanvasDebugInfo = useCallback( - (e: ChangeEvent) => dispatch(setShouldShowCanvasDebugInfo(e.target.checked)), - [dispatch] - ); - const handleChangeShouldAntialias = useCallback( - (e: ChangeEvent) => dispatch(setShouldAntialias(e.target.checked)), - [dispatch] - ); - const handleChangeShouldFitImageSize = useCallback( - (e: ChangeEvent) => dispatch(setShouldFitImageSize(e.target.checked)), - [dispatch] - ); - - return ( - - - } - /> - - - - - - - {t('unifiedCanvas.showIntermediates')} - - - - {t('unifiedCanvas.showGrid')} - - - - {t('unifiedCanvas.snapToGrid')} - - - - {t('unifiedCanvas.darkenOutsideSelection')} - - - - {t('unifiedCanvas.autoSaveToGallery')} - - - - {t('unifiedCanvas.saveBoxRegionOnly')} - - - - {t('unifiedCanvas.limitStrokesToBox')} - - - - {t('unifiedCanvas.invertBrushSizeScrollDirection')} - - - - {t('unifiedCanvas.showCanvasDebugInfo')} - - - - {t('unifiedCanvas.antialiasing')} - - - - {t('unifiedCanvas.initialFitImageSize')} - - - - - - - - - ); -}; - -export default memo(IAICanvasSettingsButtonPopover); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx deleted file mode 100644 index 697434739e..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { - Box, - ButtonGroup, - CompositeNumberInput, - CompositeSlider, - Flex, - FormControl, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIColorPicker from 'common/components/IAIColorPicker'; -import { $tool, resetToolInteractionState } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { addEraseRect, addFillRect, setBrushColor, setBrushSize } from 'features/canvas/store/canvasSlice'; -import { clamp } from 'lodash-es'; -import { memo, useCallback } from 'react'; -import type { RgbaColor } from 'react-colorful'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiEraserBold, - PiEyedropperBold, - PiPaintBrushBold, - PiPaintBucketBold, - PiSlidersHorizontalBold, - PiXBold, -} from 'react-icons/pi'; - -const marks = [1, 25, 50, 75, 100]; - -const IAICanvasToolChooserOptions = () => { - const dispatch = useAppDispatch(); - const tool = useStore($tool); - const brushColor = useAppSelector((s) => s.canvas.brushColor); - const brushSize = useAppSelector((s) => s.canvas.brushSize); - const isStaging = useAppSelector(isStagingSelector); - const { t } = useTranslation(); - - useHotkeys( - ['b'], - () => { - handleSelectBrushTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['e'], - () => { - handleSelectEraserTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [tool] - ); - - useHotkeys( - ['c'], - () => { - handleSelectColorPickerTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [tool] - ); - - useHotkeys( - ['shift+f'], - () => { - handleFillRect(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - } - ); - - useHotkeys( - ['delete', 'backspace'], - () => { - handleEraseBoundingBox(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - } - ); - - useHotkeys( - ['BracketLeft'], - () => { - if (brushSize - 5 <= 5) { - dispatch(setBrushSize(Math.max(brushSize - 1, 1))); - } else { - dispatch(setBrushSize(Math.max(brushSize - 5, 1))); - } - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushSize] - ); - - useHotkeys( - ['BracketRight'], - () => { - dispatch(setBrushSize(Math.min(brushSize + 5, 500))); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushSize] - ); - - useHotkeys( - ['Shift+BracketLeft'], - () => { - dispatch( - setBrushColor({ - ...brushColor, - a: clamp(brushColor.a - 0.05, 0.05, 1), - }) - ); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushColor] - ); - - useHotkeys( - ['Shift+BracketRight'], - () => { - dispatch( - setBrushColor({ - ...brushColor, - a: clamp(brushColor.a + 0.05, 0.05, 1), - }) - ); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushColor] - ); - - const handleSelectBrushTool = useCallback(() => { - $tool.set('brush'); - resetToolInteractionState(); - }, []); - const handleSelectEraserTool = useCallback(() => { - $tool.set('eraser'); - resetToolInteractionState(); - }, []); - const handleSelectColorPickerTool = useCallback(() => { - $tool.set('colorPicker'); - resetToolInteractionState(); - }, []); - const handleFillRect = useCallback(() => { - dispatch(addFillRect()); - }, [dispatch]); - const handleEraseBoundingBox = useCallback(() => { - dispatch(addEraseRect()); - }, [dispatch]); - const handleChangeBrushSize = useCallback( - (newSize: number) => { - dispatch(setBrushSize(newSize)); - }, - [dispatch] - ); - const handleChangeBrushColor = useCallback( - (newColor: RgbaColor) => { - dispatch(setBrushColor(newColor)); - }, - [dispatch] - ); - - return ( - - } - isChecked={tool === 'brush' && !isStaging} - onClick={handleSelectBrushTool} - isDisabled={isStaging} - /> - } - isChecked={tool === 'eraser' && !isStaging} - isDisabled={isStaging} - onClick={handleSelectEraserTool} - /> - } - isDisabled={isStaging} - onClick={handleFillRect} - /> - } - isDisabled={isStaging} - onClick={handleEraseBoundingBox} - /> - } - isChecked={tool === 'colorPicker' && !isStaging} - isDisabled={isStaging} - onClick={handleSelectColorPickerTool} - /> - - - } - /> - - - - - - - {t('unifiedCanvas.brushSize')} - - - - - - - - - - - - - ); -}; - -export default memo(IAICanvasToolChooserOptions); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx deleted file mode 100644 index 5ed5ffe573..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { ButtonGroup, Combobox, Flex, FormControl, IconButton, Tooltip } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard'; -import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; -import { useSingleAndDoubleClick } from 'common/hooks/useSingleAndDoubleClick'; -import { - canvasCopiedToClipboard, - canvasDownloadedAsImage, - canvasMerged, - canvasSavedToGallery, -} from 'features/canvas/store/actions'; -import { $canvasBaseLayer, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { - resetCanvas, - resetCanvasView, - setIsMaskEnabled, - setLayer, - setShouldShowBoundingBox, -} from 'features/canvas/store/canvasSlice'; -import type { CanvasLayer } from 'features/canvas/store/canvasTypes'; -import { memo, useCallback, useMemo } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiCopyBold, - PiCrosshairSimpleBold, - PiDownloadSimpleBold, - PiEyeBold, - PiEyeSlashBold, - PiFloppyDiskBold, - PiHandGrabbingBold, - PiStackBold, - PiTrashSimpleBold, - PiUploadSimpleBold, -} from 'react-icons/pi'; - -import IAICanvasMaskOptions from './IAICanvasMaskOptions'; -import IAICanvasRedoButton from './IAICanvasRedoButton'; -import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover'; -import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions'; -import IAICanvasUndoButton from './IAICanvasUndoButton'; - -const IAICanvasToolbar = () => { - const dispatch = useAppDispatch(); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const layer = useAppSelector((s) => s.canvas.layer); - const tool = useStore($tool); - const isStaging = useAppSelector(isStagingSelector); - const { t } = useTranslation(); - const { isClipboardAPIAvailable } = useCopyImageToClipboard(); - const shouldShowBoundingBox = useAppSelector((s) => s.canvas.shouldShowBoundingBox); - - const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({ - postUploadAction: { type: 'SET_CANVAS_INITIAL_IMAGE' }, - }); - - useHotkeys( - ['v'], - () => { - handleSelectMoveTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - 'shift+h', - () => { - dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox)); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [shouldShowBoundingBox] - ); - - useHotkeys( - ['r'], - () => { - handleResetCanvasView(); - }, - { - enabled: () => true, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['shift+m'], - () => { - handleMergeVisible(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['shift+s'], - () => { - !isStaging && handleSaveToGallery(); - }, - { - enabled: true, - preventDefault: true, - }, - [isStaging] - ); - - useHotkeys( - ['meta+c', 'ctrl+c'], - () => { - handleCopyImageToClipboard(); - }, - { - enabled: () => !isStaging && isClipboardAPIAvailable, - preventDefault: true, - }, - [isClipboardAPIAvailable] - ); - - useHotkeys( - ['shift+d'], - () => { - handleDownloadAsImage(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - const handleSelectMoveTool = useCallback(() => { - $tool.set('move'); - }, []); - - const handleSetShouldShowBoundingBox = useCallback(() => { - dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox)); - }, [dispatch, shouldShowBoundingBox]); - - const handleResetCanvasView = useCallback( - (shouldScaleTo1 = false) => { - const canvasBaseLayer = $canvasBaseLayer.get(); - if (!canvasBaseLayer) { - return; - } - const clientRect = canvasBaseLayer.getClientRect({ - skipTransform: true, - }); - dispatch( - resetCanvasView({ - contentRect: clientRect, - shouldScaleTo1, - }) - ); - }, - [dispatch] - ); - const onSingleClick = useCallback(() => { - handleResetCanvasView(false); - }, [handleResetCanvasView]); - const onDoubleClick = useCallback(() => { - handleResetCanvasView(true); - }, [handleResetCanvasView]); - - const handleClickResetCanvasView = useSingleAndDoubleClick({ - onSingleClick, - onDoubleClick, - }); - - const handleResetCanvas = useCallback(() => { - dispatch(resetCanvas()); - }, [dispatch]); - - const handleMergeVisible = useCallback(() => { - dispatch(canvasMerged()); - }, [dispatch]); - - const handleSaveToGallery = useCallback(() => { - dispatch(canvasSavedToGallery()); - }, [dispatch]); - - const handleCopyImageToClipboard = useCallback(() => { - if (!isClipboardAPIAvailable) { - return; - } - dispatch(canvasCopiedToClipboard()); - }, [dispatch, isClipboardAPIAvailable]); - - const handleDownloadAsImage = useCallback(() => { - dispatch(canvasDownloadedAsImage()); - }, [dispatch]); - - const handleChangeLayer = useCallback( - (v) => { - if (!v) { - return; - } - dispatch(setLayer(v.value as CanvasLayer)); - if (v.value === 'mask' && !isMaskEnabled) { - dispatch(setIsMaskEnabled(true)); - } - }, - [dispatch, isMaskEnabled] - ); - - const layerOptions = useMemo<{ label: string; value: CanvasLayer }[]>( - () => [ - { label: t('unifiedCanvas.base'), value: 'base' }, - { label: t('unifiedCanvas.mask'), value: 'mask' }, - ], - [t] - ); - const layerValue = useMemo(() => layerOptions.filter((o) => o.value === layer)[0] ?? null, [layer, layerOptions]); - - return ( - - - - - - - - - - - - } - isChecked={tool === 'move' || isStaging} - onClick={handleSelectMoveTool} - /> - : } - onClick={handleSetShouldShowBoundingBox} - isDisabled={isStaging} - /> - } - onClick={handleClickResetCanvasView} - /> - - - - } - onClick={handleMergeVisible} - isDisabled={isStaging} - /> - } - onClick={handleSaveToGallery} - isDisabled={isStaging} - /> - {isClipboardAPIAvailable && ( - } - onClick={handleCopyImageToClipboard} - isDisabled={isStaging} - /> - )} - } - onClick={handleDownloadAsImage} - isDisabled={isStaging} - /> - - - - - - - - } - isDisabled={isStaging} - {...getUploadButtonProps()} - /> - - } - onClick={handleResetCanvas} - colorScheme="error" - isDisabled={isStaging} - /> - - - - - - ); -}; - -export default memo(IAICanvasToolbar); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton.tsx deleted file mode 100644 index f1fcdf96e5..0000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { undo } from 'features/canvas/store/canvasSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; - -const IAICanvasUndoButton = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const activeTabName = useAppSelector(activeTabNameSelector); - const canUndo = useAppSelector((s) => s.canvas.pastLayerStates.length > 0); - - const handleUndo = useCallback(() => { - dispatch(undo()); - }, [dispatch]); - - useHotkeys( - ['meta+z', 'ctrl+z'], - () => { - handleUndo(); - }, - { - enabled: () => canUndo, - preventDefault: true, - }, - [activeTabName, canUndo] - ); - - return ( - } - onClick={handleUndo} - isDisabled={!canUndo} - /> - ); -}; - -export default memo(IAICanvasUndoButton); diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts deleted file mode 100644 index d47b1c6ccd..0000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isMovingBoundingBox, $isMovingStage, $tool } 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 isStaging = useAppSelector(isStagingSelector); - const handleDragStart = useCallback(() => { - if (!(($tool.get() === 'move' || isStaging) && !$isMovingBoundingBox.get())) { - return; - } - $isMovingStage.set(true); - }, [isStaging]); - - const handleDragMove = useCallback( - (e: KonvaEventObject) => { - const tool = $tool.get(); - if (!((tool === 'move' || isStaging) && !$isMovingBoundingBox.get())) { - return; - } - - const newCoordinates = { x: e.target.x(), y: e.target.y() }; - - dispatch(setStageCoordinates(newCoordinates)); - }, - [dispatch, isStaging] - ); - - const handleDragEnd = useCallback(() => { - if (!(($tool.get() === 'move' || isStaging) && !$isMovingBoundingBox.get())) { - return; - } - $isMovingStage.set(false); - }, [isStaging]); - - return { - handleDragStart, - handleDragMove, - handleDragEnd, - }; -}; - -export default useCanvasDrag; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasGenerationMode.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasGenerationMode.ts deleted file mode 100644 index 45852cd1bc..0000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasGenerationMode.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import type { GenerationMode } from 'features/canvas/store/canvasTypes'; -import { getCanvasData } from 'features/canvas/util/getCanvasData'; -import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode'; -import { useEffect, useState } from 'react'; -import { useDebounce } from 'react-use'; - -export const useCanvasGenerationMode = () => { - const layerState = useAppSelector((s) => s.canvas.layerState); - - const boundingBoxCoordinates = useAppSelector((s) => s.canvas.boundingBoxCoordinates); - const boundingBoxDimensions = useAppSelector((s) => s.canvas.boundingBoxDimensions); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - - const shouldPreserveMaskedArea = useAppSelector((s) => s.canvas.shouldPreserveMaskedArea); - const [generationMode, setGenerationMode] = useState(); - - useEffect(() => { - setGenerationMode(undefined); - }, [layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea]); - - useDebounce( - async () => { - // Build canvas blobs - const canvasBlobsAndImageData = await getCanvasData( - layerState, - boundingBoxCoordinates, - boundingBoxDimensions, - isMaskEnabled, - shouldPreserveMaskedArea - ); - - if (!canvasBlobsAndImageData) { - return; - } - - const { baseImageData, maskImageData } = canvasBlobsAndImageData; - - // Determine the generation mode - const generationMode = getCanvasGenerationMode(baseImageData, maskImageData); - - setGenerationMode(generationMode); - }, - 1000, - [layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea] - ); - - return generationMode; -}; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts deleted file mode 100644 index ec833c5f3d..0000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - $canvasStage, - $tool, - $toolStash, - resetCanvasInteractionState, - resetToolInteractionState, -} from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { clearMask, setIsMaskEnabled, setShouldSnapToGrid } from 'features/canvas/store/canvasSlice'; -import { isInteractiveTarget } from 'features/canvas/util/isInteractiveTarget'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { useCallback, useEffect } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; - -const useInpaintingCanvasHotkeys = () => { - const dispatch = useAppDispatch(); - const activeTabName = useAppSelector(activeTabNameSelector); - const isStaging = useAppSelector(isStagingSelector); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid); - - // Beta Keys - const handleClearMask = useCallback(() => dispatch(clearMask()), [dispatch]); - - useHotkeys( - ['shift+c'], - () => { - handleClearMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - const handleToggleEnableMask = () => dispatch(setIsMaskEnabled(!isMaskEnabled)); - - useHotkeys( - ['h'], - () => { - handleToggleEnableMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [isMaskEnabled] - ); - - useHotkeys( - ['n'], - () => { - dispatch(setShouldSnapToGrid(!shouldSnapToGrid)); - }, - { - enabled: true, - preventDefault: true, - }, - [shouldSnapToGrid] - ); - // - - useHotkeys( - 'esc', - () => { - resetCanvasInteractionState(); - }, - { - enabled: () => true, - preventDefault: true, - } - ); - - const onKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') { - return; - } - if ($toolStash.get() || $tool.get() === 'move') { - return; - } - $canvasStage.get()?.container().focus(); - $toolStash.set($tool.get()); - $tool.set('move'); - resetToolInteractionState(); - }, - [activeTabName] - ); - const onKeyUp = useCallback( - (e: KeyboardEvent) => { - if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') { - return; - } - if (!$toolStash.get() || $tool.get() !== 'move') { - return; - } - $canvasStage.get()?.container().focus(); - $tool.set($toolStash.get() ?? 'move'); - $toolStash.set(null); - }, - [activeTabName] - ); - - useEffect(() => { - window.addEventListener('keydown', onKeyDown); - window.addEventListener('keyup', onKeyUp); - - return () => { - window.removeEventListener('keydown', onKeyDown); - window.removeEventListener('keyup', onKeyUp); - }; - }, [onKeyDown, onKeyUp]); -}; - -export default useInpaintingCanvasHotkeys; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts deleted file mode 100644 index b392b72e3f..0000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isDrawing, $isMovingStage, $tool } 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 type Konva from 'konva'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -import useColorPicker from './useColorUnderCursor'; - -const useCanvasMouseDown = (stageRef: MutableRefObject) => { - const dispatch = useAppDispatch(); - const isStaging = useAppSelector(isStagingSelector); - const { commitColorUnderCursor } = useColorPicker(); - - return useCallback( - (e: KonvaEventObject) => { - if (!stageRef.current) { - return; - } - - stageRef.current.container().focus(); - const tool = $tool.get(); - - if (tool === 'move' || isStaging) { - $isMovingStage.set(true); - return; - } - - if (tool === 'colorPicker') { - commitColorUnderCursor(); - return; - } - - const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - - if (!scaledCursorPosition) { - return; - } - - e.evt.preventDefault(); - - $isDrawing.set(true); - - // Add a new line starting from the current cursor position. - dispatch( - addLine({ - points: [scaledCursorPosition.x, scaledCursorPosition.y], - tool, - }) - ); - }, - [stageRef, isStaging, dispatch, commitColorUnderCursor] - ); -}; - -export default useCanvasMouseDown; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts deleted file mode 100644 index 6d0a97031b..0000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $cursorPosition, $isDrawing, $tool } 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 type Konva from 'konva'; -import type { Vector2d } from 'konva/lib/types'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -import useColorPicker from './useColorUnderCursor'; - -const useCanvasMouseMove = ( - stageRef: MutableRefObject, - didMouseMoveRef: MutableRefObject, - lastCursorPositionRef: MutableRefObject -) => { - const dispatch = useAppDispatch(); - const isStaging = useAppSelector(isStagingSelector); - const { updateColorUnderCursor } = useColorPicker(); - - return useCallback(() => { - if (!stageRef.current) { - return; - } - - const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - - if (!scaledCursorPosition) { - return; - } - - $cursorPosition.set(scaledCursorPosition); - - lastCursorPositionRef.current = scaledCursorPosition; - const tool = $tool.get(); - - if (tool === 'colorPicker') { - updateColorUnderCursor(); - return; - } - - if (!$isDrawing.get() || tool === 'move' || isStaging) { - return; - } - - didMouseMoveRef.current = true; - dispatch(addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])); - }, [didMouseMoveRef, dispatch, isStaging, lastCursorPositionRef, stageRef, updateColorUnderCursor]); -}; - -export default useCanvasMouseMove; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts deleted file mode 100644 index 0b7220eb0b..0000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { setCanvasInteractionStateMouseOut } from 'features/canvas/store/canvasNanostore'; -import { useCallback } from 'react'; - -const useCanvasMouseOut = () => { - const onMouseOut = useCallback(() => { - setCanvasInteractionStateMouseOut(); - }, []); - - 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 deleted file mode 100644 index e3c291f1e1..0000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isDrawing, $isMovingStage, $tool } 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 type Konva from 'konva'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -const useCanvasMouseUp = ( - stageRef: MutableRefObject, - didMouseMoveRef: MutableRefObject -) => { - const dispatch = useAppDispatch(); - const isDrawing = useStore($isDrawing); - const isStaging = useAppSelector(isStagingSelector); - - return useCallback(() => { - if ($tool.get() === 'move' || isStaging) { - $isMovingStage.set(false); - return; - } - - if (!didMouseMoveRef.current && isDrawing && stageRef.current) { - const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - - if (!scaledCursorPosition) { - return; - } - - /** - * Extend the current line. - * In this case, the mouse didn't move, so we append the same point to - * the line's existing points. This allows the line to render as a circle - * centered on that point. - */ - dispatch(addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])); - } else { - didMouseMoveRef.current = false; - } - $isDrawing.set(false); - }, [didMouseMoveRef, dispatch, isDrawing, isStaging, stageRef]); -}; - -export default useCanvasMouseUp; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts deleted file mode 100644 index 1434bc9afc..0000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { $ctrl, $meta } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isMoveStageKeyHeld } from 'features/canvas/store/canvasNanostore'; -import { setBrushSize, setStageCoordinates, setStageScale } from 'features/canvas/store/canvasSlice'; -import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; -import type Konva from 'konva'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import { clamp } from 'lodash-es'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -export const calculateNewBrushSize = (brushSize: number, delta: number) => { - // This equation was derived by fitting a curve to the desired brush sizes and deltas - // see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565 - const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize); - // This needs to be clamped to prevent the delta from getting too large - const finalDelta = clamp(targetDelta, -20, 20); - // The new brush size is also clamped to prevent it from getting too large or small - const newBrushSize = clamp(brushSize + finalDelta, 1, 500); - - return newBrushSize; -}; - -const useCanvasWheel = (stageRef: MutableRefObject) => { - const dispatch = useAppDispatch(); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const isMoveStageKeyHeld = useStore($isMoveStageKeyHeld); - const brushSize = useAppSelector((s) => s.canvas.brushSize); - const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); - - return useCallback( - (e: KonvaEventObject) => { - // stop default scrolling - if (!stageRef.current || isMoveStageKeyHeld) { - return; - } - - e.evt.preventDefault(); - - // checking for ctrl key is pressed or not, - // so that brush size can be controlled using ctrl + scroll up/down - - // Invert the delta if the property is set to true - let delta = e.evt.deltaY; - if (shouldInvertBrushSizeScrollDirection) { - delta = -delta; - } - - if ($ctrl.get() || $meta.get()) { - dispatch(setBrushSize(calculateNewBrushSize(brushSize, delta))); - } else { - const cursorPos = stageRef.current.getPointerPosition(); - let delta = e.evt.deltaY; - - if (!cursorPos) { - return; - } - - const mousePointTo = { - x: (cursorPos.x - stageRef.current.x()) / stageScale, - y: (cursorPos.y - stageRef.current.y()) / stageScale, - }; - // when we zoom on trackpad, e.evt.ctrlKey is true - // in that case lets revert direction - if (e.evt.ctrlKey) { - delta = -delta; - } - - const newScale = clamp(stageScale * CANVAS_SCALE_BY ** delta, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE); - - const newCoordinates = { - x: cursorPos.x - mousePointTo.x * newScale, - y: cursorPos.y - mousePointTo.y * newScale, - }; - - dispatch(setStageScale(newScale)); - dispatch(setStageCoordinates(newCoordinates)); - } - }, - [stageRef, isMoveStageKeyHeld, brushSize, dispatch, stageScale, shouldInvertBrushSizeScrollDirection] - ); -}; - -export default useCanvasWheel; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts b/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts deleted file mode 100644 index f07433a3de..0000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useAppDispatch } from 'app/store/storeHooks'; -import { $canvasBaseLayer, $canvasStage, $tool } from 'features/canvas/store/canvasNanostore'; -import { commitColorPickerColor, setColorPickerColor } from 'features/canvas/store/canvasSlice'; -import Konva from 'konva'; -import { useCallback } from 'react'; - -const useColorPicker = () => { - const dispatch = useAppDispatch(); - - const updateColorUnderCursor = useCallback(() => { - const stage = $canvasStage.get(); - const canvasBaseLayer = $canvasBaseLayer.get(); - if (!stage || !canvasBaseLayer) { - return; - } - - const position = stage.getPointerPosition(); - - if (!position) { - return; - } - - const pixelRatio = Konva.pixelRatio; - - const [r, g, b, a] = canvasBaseLayer - .getContext() - .getImageData(position.x * pixelRatio, position.y * pixelRatio, 1, 1).data; - - if (r === undefined || g === undefined || b === undefined || a === undefined) { - return; - } - - dispatch(setColorPickerColor({ r, g, b, a })); - }, [dispatch]); - - const commitColorUnderCursor = useCallback(() => { - dispatch(commitColorPickerColor()); - $tool.set('brush'); - }, [dispatch]); - - return { updateColorUnderCursor, commitColorUnderCursor }; -}; - -export default useColorPicker; diff --git a/invokeai/frontend/web/src/features/canvas/store/actions.ts b/invokeai/frontend/web/src/features/canvas/store/actions.ts deleted file mode 100644 index b6483b7f3a..0000000000 --- a/invokeai/frontend/web/src/features/canvas/store/actions.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import type { ImageDTO } from 'services/api/types'; - -export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery'); - -export const canvasMaskSavedToGallery = createAction('canvas/canvasMaskSavedToGallery'); - -export const canvasCopiedToClipboard = createAction('canvas/canvasCopiedToClipboard'); - -export const canvasDownloadedAsImage = createAction('canvas/canvasDownloadedAsImage'); - -export const canvasMerged = createAction('canvas/canvasMerged'); - -export const stagingAreaImageSaved = createAction<{ imageDTO: ImageDTO }>('canvas/stagingAreaImageSaved'); - -export const canvasMaskToControlAdapter = createAction<{ id: string }>('canvas/canvasMaskToControlAdapter'); - -export const canvasImageToControlAdapter = createAction<{ id: string }>('canvas/canvasImageToControlAdapter'); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts b/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts deleted file mode 100644 index b225f66677..0000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { CanvasTool } from 'features/canvas/store/canvasTypes'; -import type Konva from 'konva'; -import type { Vector2d } from 'konva/lib/types'; -import { atom, computed } from 'nanostores'; - -export const $cursorPosition = atom(null); -export const $tool = atom('move'); -export const $toolStash = atom(null); -export const $isDrawing = atom(false); -export const $isMouseOverBoundingBox = atom(false); -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 $isMouseOverBoundingBoxOutline = atom(false); -export const $isModifyingBoundingBox = computed( - [$isTransformingBoundingBox, $isMovingBoundingBox], - (isTransformingBoundingBox, isMovingBoundingBox) => isTransformingBoundingBox || isMovingBoundingBox -); - -export const resetCanvasInteractionState = () => { - $cursorPosition.set(null); - $isDrawing.set(false); - $isMoveBoundingBoxKeyHeld.set(false); - $isMoveStageKeyHeld.set(false); - $isMovingBoundingBox.set(false); - $isMovingStage.set(false); -}; - -export const resetToolInteractionState = () => { - $isTransformingBoundingBox.set(false); - $isMovingBoundingBox.set(false); - $isMovingStage.set(false); -}; - -export const setCanvasInteractionStateMouseOut = () => { - $cursorPosition.set(null); -}; -export const $canvasBaseLayer = atom(null); -export const $canvasStage = atom(null); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts deleted file mode 100644 index 29dc4c9fb8..0000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; - -import { selectCanvasSlice } from './canvasSlice'; - -export const isStagingSelector = createSelector( - selectCanvasSlice, - (canvas) => canvas.batchIds.length > 0 || canvas.layerState.stagingArea.images.length > 0 -); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts deleted file mode 100644 index e06caea357..0000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ /dev/null @@ -1,728 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; -import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; -import { modelChanged } from 'features/canvas/store/canvasSlice'; -import calculateCoordinates from 'features/canvas/util/calculateCoordinates'; -import calculateScale from 'features/canvas/util/calculateScale'; -import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants'; -import floorCoordinates from 'features/canvas/util/floorCoordinates'; -import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types'; -import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; -import type { IRect, Vector2d } from 'konva/lib/types'; -import { clamp } from 'lodash-es'; -import type { RgbaColor } from 'react-colorful'; -import { queueApi } from 'services/api/endpoints/queue'; -import type { ImageDTO } from 'services/api/types'; -import { socketQueueItemStatusChanged } from 'services/events/actions'; - -import type { - BoundingBoxScaleMethod, - CanvasBaseLine, - CanvasImage, - CanvasLayer, - CanvasLayerState, - CanvasMaskLine, - CanvasState, - CanvasTool, - Dimensions, -} from './canvasTypes'; -import { isCanvasAnyLine, isCanvasMaskLine } from './canvasTypes'; -import { CANVAS_GRID_SIZE_FINE } from './constants'; - -/** - * The maximum history length to keep in the past/future layer states. - */ -const MAX_HISTORY = 100; - -const initialLayerState: CanvasLayerState = { - objects: [], - stagingArea: { - images: [], - selectedImageIndex: -1, - }, -}; - -const initialCanvasState: CanvasState = { - _version: 1, - boundingBoxCoordinates: { x: 0, y: 0 }, - boundingBoxDimensions: { width: 512, height: 512 }, - boundingBoxScaleMethod: 'auto', - brushColor: { r: 90, g: 90, b: 255, a: 1 }, - brushSize: 50, - colorPickerColor: { r: 90, g: 90, b: 255, a: 1 }, - futureLayerStates: [], - isMaskEnabled: true, - layer: 'base', - layerState: initialLayerState, - maskColor: { r: 255, g: 90, b: 90, a: 1 }, - pastLayerStates: [], - scaledBoundingBoxDimensions: { width: 512, height: 512 }, - shouldAntialias: true, - shouldAutoSave: false, - shouldCropToBoundingBoxOnSave: false, - shouldDarkenOutsideBoundingBox: false, - shouldFitImageSize: true, - shouldInvertBrushSizeScrollDirection: false, - shouldLockBoundingBox: false, - shouldPreserveMaskedArea: false, - shouldRestrictStrokesToBox: true, - shouldShowBoundingBox: true, - shouldShowCanvasDebugInfo: false, - shouldShowGrid: true, - shouldShowIntermediates: true, - shouldShowStagingImage: true, - shouldShowStagingOutline: true, - shouldSnapToGrid: true, - stageCoordinates: { x: 0, y: 0 }, - stageDimensions: { width: 0, height: 0 }, - stageScale: 1, - batchIds: [], - aspectRatio: { - id: '1:1', - value: 1, - isLocked: false, - }, -}; - -const setBoundingBoxDimensionsReducer = ( - state: CanvasState, - payload: Partial, - optimalDimension: number -) => { - const boundingBoxDimensions = payload; - const newDimensions = { - ...state.boundingBoxDimensions, - ...boundingBoxDimensions, - }; - state.boundingBoxDimensions = newDimensions; - if (state.boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions(newDimensions, optimalDimension); - state.scaledBoundingBoxDimensions = scaledDimensions; - } -}; - -export const canvasSlice = createSlice({ - name: 'canvas', - initialState: initialCanvasState, - reducers: { - setLayer: (state, action: PayloadAction) => { - state.layer = action.payload; - }, - setMaskColor: (state, action: PayloadAction) => { - state.maskColor = action.payload; - }, - setBrushColor: (state, action: PayloadAction) => { - state.brushColor = action.payload; - }, - setBrushSize: (state, action: PayloadAction) => { - state.brushSize = action.payload; - }, - clearMask: (state) => { - pushToPrevLayerStates(state); - state.layerState.objects = state.layerState.objects.filter((obj) => !isCanvasMaskLine(obj)); - state.futureLayerStates = []; - state.shouldPreserveMaskedArea = false; - }, - toggleShouldInvertMask: (state) => { - state.shouldPreserveMaskedArea = !state.shouldPreserveMaskedArea; - }, - toggleShouldShowMask: (state) => { - state.isMaskEnabled = !state.isMaskEnabled; - }, - setShouldPreserveMaskedArea: (state, action: PayloadAction) => { - state.shouldPreserveMaskedArea = action.payload; - }, - setIsMaskEnabled: (state, action: PayloadAction) => { - state.isMaskEnabled = action.payload; - state.layer = action.payload ? 'mask' : 'base'; - }, - setInitialCanvasImage: { - reducer: (state, action: PayloadActionWithOptimalDimension) => { - const { width, height, image_name } = action.payload; - const { optimalDimension } = action.meta; - const { stageDimensions, shouldFitImageSize } = state; - - const newBoundingBoxDimensions = shouldFitImageSize - ? { - width: roundDownToMultiple(width, CANVAS_GRID_SIZE_FINE), - height: roundDownToMultiple(height, CANVAS_GRID_SIZE_FINE), - } - : { - width: roundDownToMultiple(clamp(width, CANVAS_GRID_SIZE_FINE, optimalDimension), CANVAS_GRID_SIZE_FINE), - height: roundDownToMultiple( - clamp(height, CANVAS_GRID_SIZE_FINE, optimalDimension), - CANVAS_GRID_SIZE_FINE - ), - }; - - const newBoundingBoxCoordinates = { - x: roundToMultiple(width / 2 - newBoundingBoxDimensions.width / 2, CANVAS_GRID_SIZE_FINE), - y: roundToMultiple(height / 2 - newBoundingBoxDimensions.height / 2, CANVAS_GRID_SIZE_FINE), - }; - - if (state.boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions(newBoundingBoxDimensions, optimalDimension); - state.scaledBoundingBoxDimensions = scaledDimensions; - } - - state.boundingBoxDimensions = newBoundingBoxDimensions; - state.boundingBoxCoordinates = newBoundingBoxCoordinates; - - pushToPrevLayerStates(state); - - state.layerState = { - ...deepClone(initialLayerState), - objects: [ - { - kind: 'image', - layer: 'base', - x: 0, - y: 0, - width, - height, - imageName: image_name, - }, - ], - }; - state.futureLayerStates = []; - - const newScale = calculateScale( - stageDimensions.width, - stageDimensions.height, - width, - height, - STAGE_PADDING_PERCENTAGE - ); - - const newCoordinates = calculateCoordinates( - stageDimensions.width, - stageDimensions.height, - 0, - 0, - width, - height, - newScale - ); - state.stageScale = newScale; - state.stageCoordinates = newCoordinates; - }, - prepare: (payload: ImageDTO, optimalDimension: number) => ({ - payload, - meta: { - optimalDimension, - }, - }), - }, - setBoundingBoxCoordinates: (state, action: PayloadAction) => { - state.boundingBoxCoordinates = floorCoordinates(action.payload); - }, - setStageCoordinates: (state, action: PayloadAction) => { - state.stageCoordinates = action.payload; - }, - setStageScale: (state, action: PayloadAction) => { - state.stageScale = action.payload; - }, - setShouldDarkenOutsideBoundingBox: (state, action: PayloadAction) => { - state.shouldDarkenOutsideBoundingBox = action.payload; - }, - setShouldInvertBrushSizeScrollDirection: (state, action: PayloadAction) => { - state.shouldInvertBrushSizeScrollDirection = action.payload; - }, - clearCanvasHistory: (state) => { - state.pastLayerStates = []; - state.futureLayerStates = []; - }, - setShouldLockBoundingBox: (state, action: PayloadAction) => { - state.shouldLockBoundingBox = action.payload; - }, - setShouldShowBoundingBox: (state, action: PayloadAction) => { - state.shouldShowBoundingBox = action.payload; - }, - canvasBatchIdAdded: (state, action: PayloadAction) => { - state.batchIds.push(action.payload); - }, - canvasBatchIdsReset: (state) => { - state.batchIds = []; - }, - stagingAreaInitialized: ( - state, - action: PayloadAction<{ - boundingBox: IRect; - }> - ) => { - const { boundingBox } = action.payload; - - state.layerState.stagingArea = { - boundingBox, - images: [], - selectedImageIndex: -1, - }; - }, - addImageToStagingArea: (state, action: PayloadAction) => { - const image = action.payload; - - if (!image || !state.layerState.stagingArea.boundingBox) { - return; - } - - pushToPrevLayerStates(state); - - state.layerState.stagingArea.images.push({ - kind: 'image', - layer: 'base', - ...state.layerState.stagingArea.boundingBox, - imageName: image.image_name, - }); - - state.layerState.stagingArea.selectedImageIndex = state.layerState.stagingArea.images.length - 1; - - state.futureLayerStates = []; - }, - discardStagedImages: (state) => { - pushToPrevLayerStates(state); - resetStagingArea(state); - state.futureLayerStates = []; - }, - discardStagedImage: (state) => { - const { images, selectedImageIndex } = state.layerState.stagingArea; - pushToPrevLayerStates(state); - images.splice(selectedImageIndex, 1); - state.layerState.stagingArea.selectedImageIndex = Math.max(0, images.length - 1); - state.futureLayerStates = []; - }, - addFillRect: (state) => { - const { boundingBoxCoordinates, boundingBoxDimensions, brushColor } = state; - - pushToPrevLayerStates(state); - - state.layerState.objects.push({ - kind: 'fillRect', - layer: 'base', - ...boundingBoxCoordinates, - ...boundingBoxDimensions, - color: brushColor, - }); - - state.futureLayerStates = []; - }, - addEraseRect: (state) => { - const { boundingBoxCoordinates, boundingBoxDimensions } = state; - - pushToPrevLayerStates(state); - - state.layerState.objects.push({ - kind: 'eraseRect', - layer: 'base', - ...boundingBoxCoordinates, - ...boundingBoxDimensions, - }); - - state.futureLayerStates = []; - }, - addLine: (state, action: PayloadAction<{ points: number[]; tool: CanvasTool }>) => { - const { layer, brushColor, brushSize, shouldRestrictStrokesToBox } = state; - const { points, tool } = action.payload; - - if (tool === 'move' || tool === 'colorPicker') { - return; - } - - const newStrokeWidth = brushSize / 2; - - // set & then spread this to only conditionally add the "color" key - const newColor = layer === 'base' && tool === 'brush' ? { color: brushColor } : {}; - - pushToPrevLayerStates(state); - - const newLine: CanvasMaskLine | CanvasBaseLine = { - kind: 'line', - layer, - tool, - strokeWidth: newStrokeWidth, - points, - ...newColor, - }; - - if (shouldRestrictStrokesToBox) { - newLine.clip = { - ...state.boundingBoxCoordinates, - ...state.boundingBoxDimensions, - }; - } - - state.layerState.objects.push(newLine); - - state.futureLayerStates = []; - }, - addPointToCurrentLine: (state, action: PayloadAction) => { - const lastLine = state.layerState.objects.findLast(isCanvasAnyLine); - - if (!lastLine) { - return; - } - - lastLine.points.push(...action.payload); - }, - undo: (state) => { - const targetState = state.pastLayerStates.pop(); - - if (!targetState) { - return; - } - - pushToFutureLayerStates(state); - - state.layerState = targetState; - }, - redo: (state) => { - const targetState = state.futureLayerStates.shift(); - - if (!targetState) { - return; - } - - pushToPrevLayerStates(state); - - state.layerState = targetState; - }, - setShouldShowGrid: (state, action: PayloadAction) => { - state.shouldShowGrid = action.payload; - }, - setShouldSnapToGrid: (state, action: PayloadAction) => { - state.shouldSnapToGrid = action.payload; - }, - setShouldAutoSave: (state, action: PayloadAction) => { - state.shouldAutoSave = action.payload; - }, - setShouldShowIntermediates: (state, action: PayloadAction) => { - state.shouldShowIntermediates = action.payload; - }, - resetCanvas: (state) => { - pushToPrevLayerStates(state); - state.layerState = deepClone(initialLayerState); - state.futureLayerStates = []; - state.boundingBoxCoordinates = { - ...initialCanvasState.boundingBoxCoordinates, - }; - state.boundingBoxDimensions = { - ...initialCanvasState.boundingBoxDimensions, - }; - state.stageScale = calculateScale( - state.stageDimensions.width, - state.stageDimensions.height, - state.boundingBoxDimensions.width, - state.boundingBoxDimensions.height, - STAGE_PADDING_PERCENTAGE - ); - state.stageCoordinates = calculateCoordinates( - state.stageDimensions.width, - state.stageDimensions.height, - 0, - 0, - state.boundingBoxDimensions.width, - state.boundingBoxDimensions.height, - 1 - ); - }, - canvasResized: (state, action: PayloadAction<{ width: number; height: number }>) => { - state.stageDimensions = { - width: Math.floor(action.payload.width), - height: Math.floor(action.payload.height), - }; - }, - resetCanvasView: ( - state, - action: PayloadAction<{ - contentRect: IRect; - shouldScaleTo1?: boolean; - }> - ) => { - const { contentRect, shouldScaleTo1 } = action.payload; - const { - stageDimensions: { width: stageWidth, height: stageHeight }, - } = state; - - const newScale = shouldScaleTo1 - ? 1 - : calculateScale( - stageWidth, - stageHeight, - contentRect.width || state.boundingBoxDimensions.width, - contentRect.height || state.boundingBoxDimensions.height, - STAGE_PADDING_PERCENTAGE - ); - - const newCoordinates = calculateCoordinates( - stageWidth, - stageHeight, - contentRect.x || state.boundingBoxCoordinates.x, - contentRect.y || state.boundingBoxCoordinates.y, - contentRect.width || state.boundingBoxDimensions.width, - contentRect.height || state.boundingBoxDimensions.height, - newScale - ); - - state.stageScale = newScale; - state.stageCoordinates = newCoordinates; - }, - nextStagingAreaImage: (state) => { - if (!state.layerState.stagingArea.images.length) { - return; - } - - const nextIndex = state.layerState.stagingArea.selectedImageIndex + 1; - const lastIndex = state.layerState.stagingArea.images.length - 1; - - state.layerState.stagingArea.selectedImageIndex = nextIndex > lastIndex ? 0 : nextIndex; - }, - prevStagingAreaImage: (state) => { - if (!state.layerState.stagingArea.images.length) { - return; - } - - const prevIndex = state.layerState.stagingArea.selectedImageIndex - 1; - const lastIndex = state.layerState.stagingArea.images.length - 1; - - state.layerState.stagingArea.selectedImageIndex = prevIndex < 0 ? lastIndex : prevIndex; - }, - commitStagingAreaImage: (state) => { - if (!state.layerState.stagingArea.images.length) { - return; - } - - const { images, selectedImageIndex } = state.layerState.stagingArea; - - pushToPrevLayerStates(state); - - const imageToCommit = images[selectedImageIndex]; - - if (imageToCommit) { - state.layerState.objects.push({ - ...imageToCommit, - }); - } - - resetStagingArea(state); - state.futureLayerStates = []; - }, - setBoundingBoxScaleMethod: { - reducer: (state, action: PayloadActionWithOptimalDimension) => { - const boundingBoxScaleMethod = action.payload; - const { optimalDimension } = action.meta; - state.boundingBoxScaleMethod = boundingBoxScaleMethod; - - if (boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions(state.boundingBoxDimensions, optimalDimension); - state.scaledBoundingBoxDimensions = scaledDimensions; - } - }, - prepare: (payload: BoundingBoxScaleMethod, optimalDimension: number) => ({ - payload, - meta: { - optimalDimension, - }, - }), - }, - setScaledBoundingBoxDimensions: (state, action: PayloadAction>) => { - state.scaledBoundingBoxDimensions = { - ...state.scaledBoundingBoxDimensions, - ...action.payload, - }; - }, - setBoundingBoxDimensions: { - reducer: (state, action: PayloadActionWithOptimalDimension>) => { - setBoundingBoxDimensionsReducer(state, action.payload, action.meta.optimalDimension); - }, - prepare: (payload: Partial, optimalDimension: number) => ({ - payload, - meta: { - optimalDimension, - }, - }), - }, - setShouldShowStagingImage: (state, action: PayloadAction) => { - state.shouldShowStagingImage = action.payload; - }, - setShouldShowStagingOutline: (state, action: PayloadAction) => { - state.shouldShowStagingOutline = action.payload; - }, - setShouldShowCanvasDebugInfo: (state, action: PayloadAction) => { - state.shouldShowCanvasDebugInfo = action.payload; - }, - setShouldRestrictStrokesToBox: (state, action: PayloadAction) => { - state.shouldRestrictStrokesToBox = action.payload; - }, - setShouldAntialias: (state, action: PayloadAction) => { - state.shouldAntialias = action.payload; - }, - setShouldFitImageSize: (state, action: PayloadAction) => { - state.shouldFitImageSize = action.payload; - }, - setShouldCropToBoundingBoxOnSave: (state, action: PayloadAction) => { - state.shouldCropToBoundingBoxOnSave = action.payload; - }, - setColorPickerColor: (state, action: PayloadAction) => { - state.colorPickerColor = action.payload; - }, - commitColorPickerColor: (state) => { - state.brushColor = { - ...state.colorPickerColor, - a: state.brushColor.a, - }; - }, - setMergedCanvas: (state, action: PayloadAction) => { - pushToPrevLayerStates(state); - - state.futureLayerStates = []; - - state.layerState.objects = [action.payload]; - }, - aspectRatioChanged: (state, action: PayloadAction) => { - state.aspectRatio = action.payload; - }, - }, - extraReducers: (builder) => { - builder.addCase(modelChanged, (state, action) => { - const newModel = action.payload; - if (!newModel || action.meta.previousModel?.base === newModel.base) { - // Model was cleared or the base didn't change - return; - } - const optimalDimension = getOptimalDimension(action.payload); - const { width, height } = state.boundingBoxDimensions; - if (getIsSizeOptimal(width, height, optimalDimension)) { - return; - } - const newSize = calculateNewSize(state.aspectRatio.value, optimalDimension * optimalDimension); - setBoundingBoxDimensionsReducer(state, newSize, optimalDimension); - }); - - builder.addCase(socketQueueItemStatusChanged, (state, action) => { - const batch_status = action.payload.data.batch_status; - if (!state.batchIds.includes(batch_status.batch_id)) { - return; - } - - if (batch_status.in_progress === 0 && batch_status.pending === 0) { - state.batchIds = state.batchIds.filter((id) => id !== batch_status.batch_id); - } - - const queueItemStatus = action.payload.data.status; - if (queueItemStatus === 'canceled' || queueItemStatus === 'failed') { - resetStagingAreaIfEmpty(state); - } - }); - builder.addMatcher(queueApi.endpoints.clearQueue.matchFulfilled, (state) => { - state.batchIds = []; - resetStagingAreaIfEmpty(state); - }); - builder.addMatcher(queueApi.endpoints.cancelByBatchIds.matchFulfilled, (state, action) => { - state.batchIds = state.batchIds.filter((id) => !action.meta.arg.originalArgs.batch_ids.includes(id)); - resetStagingAreaIfEmpty(state); - }); - }, -}); - -export const { - addEraseRect, - addFillRect, - addImageToStagingArea, - addLine, - addPointToCurrentLine, - clearCanvasHistory, - clearMask, - commitColorPickerColor, - commitStagingAreaImage, - discardStagedImages, - discardStagedImage, - nextStagingAreaImage, - prevStagingAreaImage, - redo, - resetCanvas, - resetCanvasView, - setBoundingBoxCoordinates, - setBoundingBoxDimensions, - setBoundingBoxScaleMethod, - setBrushColor, - setBrushSize, - setColorPickerColor, - setInitialCanvasImage, - setIsMaskEnabled, - setLayer, - setMaskColor, - setMergedCanvas, - setShouldAutoSave, - setShouldCropToBoundingBoxOnSave, - setShouldDarkenOutsideBoundingBox, - setShouldInvertBrushSizeScrollDirection, - setShouldPreserveMaskedArea, - setShouldShowBoundingBox, - setShouldShowCanvasDebugInfo, - setShouldShowGrid, - setShouldShowIntermediates, - setShouldShowStagingImage, - setShouldShowStagingOutline, - setShouldSnapToGrid, - setStageCoordinates, - setStageScale, - undo, - setScaledBoundingBoxDimensions, - setShouldRestrictStrokesToBox, - stagingAreaInitialized, - setShouldAntialias, - setShouldFitImageSize, - canvasResized, - canvasBatchIdAdded, - canvasBatchIdsReset, - aspectRatioChanged, -} = canvasSlice.actions; - -export const selectCanvasSlice = (state: RootState) => state.canvas; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateCanvasState = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - state.aspectRatio = initialAspectRatioState; - } - return state; -}; - -export const canvasPersistConfig: PersistConfig = { - name: canvasSlice.name, - initialState: initialCanvasState, - migrate: migrateCanvasState, - persistDenylist: ['shouldShowStagingImage', 'shouldShowStagingOutline'], -}; - -const pushToPrevLayerStates = (state: CanvasState) => { - state.pastLayerStates.push(deepClone(state.layerState)); - if (state.pastLayerStates.length > MAX_HISTORY) { - state.pastLayerStates = state.pastLayerStates.slice(-MAX_HISTORY); - } -}; - -const pushToFutureLayerStates = (state: CanvasState) => { - state.futureLayerStates.unshift(deepClone(state.layerState)); - if (state.futureLayerStates.length > MAX_HISTORY) { - state.futureLayerStates = state.futureLayerStates.slice(0, MAX_HISTORY); - } -}; - -const resetStagingAreaIfEmpty = (state: CanvasState) => { - if (state.batchIds.length === 0 && state.layerState.stagingArea.images.length === 0) { - resetStagingArea(state); - } -}; - -const resetStagingArea = (state: CanvasState) => { - state.layerState.stagingArea = { ...initialCanvasState.layerState.stagingArea }; - state.shouldShowStagingImage = initialCanvasState.shouldShowStagingImage; - state.shouldShowStagingOutline = initialCanvasState.shouldShowStagingOutline; -}; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts deleted file mode 100644 index c41c6f329f..0000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { IRect, Vector2d } from 'konva/lib/types'; -import type { RgbaColor } from 'react-colorful'; -import { z } from 'zod'; - -export type CanvasLayer = 'base' | 'mask'; - -const zBoundingBoxScaleMethod = z.enum(['none', 'auto', 'manual']); -export type BoundingBoxScaleMethod = z.infer; -export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => - zBoundingBoxScaleMethod.safeParse(v).success; - -type CanvasDrawingTool = 'brush' | 'eraser'; - -export type CanvasTool = CanvasDrawingTool | 'move' | 'colorPicker'; - -export type Dimensions = { - width: number; - height: number; -}; - -export type CanvasImage = { - kind: 'image'; - layer: 'base'; - x: number; - y: number; - width: number; - height: number; - imageName: string; -}; - -export type CanvasMaskLine = { - layer: 'mask'; - kind: 'line'; - tool: CanvasDrawingTool; - strokeWidth: number; - points: number[]; - clip?: IRect; -}; - -export type CanvasBaseLine = { - layer: 'base'; - color?: RgbaColor; - kind: 'line'; - tool: CanvasDrawingTool; - strokeWidth: number; - points: number[]; - clip?: IRect; -}; - -type CanvasFillRect = { - kind: 'fillRect'; - layer: 'base'; - x: number; - y: number; - width: number; - height: number; - color: RgbaColor; -}; - -type CanvasEraseRect = { - kind: 'eraseRect'; - layer: 'base'; - x: number; - y: number; - width: number; - height: number; -}; - -type CanvasObject = CanvasImage | CanvasBaseLine | CanvasMaskLine | CanvasFillRect | CanvasEraseRect; - -export type CanvasLayerState = { - objects: CanvasObject[]; - stagingArea: { - images: CanvasImage[]; - selectedImageIndex: number; - boundingBox?: IRect; - }; -}; - -// type guards -export const isCanvasMaskLine = (obj: CanvasObject): obj is CanvasMaskLine => - obj.kind === 'line' && obj.layer === 'mask'; - -export const isCanvasBaseLine = (obj: CanvasObject): obj is CanvasBaseLine => - obj.kind === 'line' && obj.layer === 'base'; - -export const isCanvasBaseImage = (obj: CanvasObject): obj is CanvasImage => - obj.kind === 'image' && obj.layer === 'base'; - -export const isCanvasFillRect = (obj: CanvasObject): obj is CanvasFillRect => - obj.kind === 'fillRect' && obj.layer === 'base'; - -export const isCanvasEraseRect = (obj: CanvasObject): obj is CanvasEraseRect => - obj.kind === 'eraseRect' && obj.layer === 'base'; - -export const isCanvasAnyLine = (obj: CanvasObject): obj is CanvasMaskLine | CanvasBaseLine => obj.kind === 'line'; - -export interface CanvasState { - _version: 1; - boundingBoxCoordinates: Vector2d; - boundingBoxDimensions: Dimensions; - boundingBoxScaleMethod: BoundingBoxScaleMethod; - brushColor: RgbaColor; - brushSize: number; - colorPickerColor: RgbaColor; - futureLayerStates: CanvasLayerState[]; - isMaskEnabled: boolean; - layer: CanvasLayer; - layerState: CanvasLayerState; - maskColor: RgbaColor; - pastLayerStates: CanvasLayerState[]; - scaledBoundingBoxDimensions: Dimensions; - shouldAntialias: boolean; - shouldAutoSave: boolean; - shouldCropToBoundingBoxOnSave: boolean; - shouldDarkenOutsideBoundingBox: boolean; - shouldFitImageSize: boolean; - shouldInvertBrushSizeScrollDirection: boolean; - shouldLockBoundingBox: boolean; - shouldPreserveMaskedArea: boolean; - shouldRestrictStrokesToBox: boolean; - shouldShowBoundingBox: boolean; - shouldShowCanvasDebugInfo: boolean; - shouldShowGrid: boolean; - shouldShowIntermediates: boolean; - shouldShowStagingImage: boolean; - shouldShowStagingOutline: boolean; - shouldSnapToGrid: boolean; - stageCoordinates: Vector2d; - stageDimensions: Dimensions; - stageScale: number; - generationMode?: GenerationMode; - batchIds: string[]; - aspectRatio: AspectRatioState; -} - -export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; diff --git a/invokeai/frontend/web/src/features/canvas/store/constants.ts b/invokeai/frontend/web/src/features/canvas/store/constants.ts deleted file mode 100644 index 450c246194..0000000000 --- a/invokeai/frontend/web/src/features/canvas/store/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const CANVAS_GRID_SIZE_COARSE = 64; -export const CANVAS_GRID_SIZE_FINE = 8; -export const CANVAS_TAB_TESTID = 'unified-canvas-tab'; diff --git a/invokeai/frontend/web/src/features/canvas/util/blobToDataURL.ts b/invokeai/frontend/web/src/features/canvas/util/blobToDataURL.ts deleted file mode 100644 index f29010c99c..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/blobToDataURL.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const blobToDataURL = (blob: Blob): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (_e) => resolve(reader.result as string); - reader.onerror = (_e) => reject(reader.error); - reader.onabort = (_e) => reject(new Error('Read aborted')); - reader.readAsDataURL(blob); - }); -}; - -export function imageDataToDataURL(imageData: ImageData): string { - const { width, height } = imageData; - - // Create a canvas to transfer the ImageData to - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - - // Draw the ImageData onto the canvas - const ctx = canvas.getContext('2d'); - if (!ctx) { - throw new Error('Unable to get canvas context'); - } - ctx.putImageData(imageData, 0, 0); - - // Convert the canvas to a data URL (base64) - return canvas.toDataURL(); -} diff --git a/invokeai/frontend/web/src/features/canvas/util/calculateCoordinates.ts b/invokeai/frontend/web/src/features/canvas/util/calculateCoordinates.ts deleted file mode 100644 index fe9c14b2ba..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/calculateCoordinates.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Vector2d } from 'konva/lib/types'; - -const calculateCoordinates = ( - containerWidth: number, - containerHeight: number, - containerX: number, - containerY: number, - contentWidth: number, - contentHeight: number, - scale: number -): Vector2d => { - const x = Math.floor(containerWidth / 2 - (containerX + contentWidth / 2) * scale); - const y = Math.floor(containerHeight / 2 - (containerY + contentHeight / 2) * scale); - return { x, y }; -}; - -export default calculateCoordinates; diff --git a/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts b/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts deleted file mode 100644 index 255cb2850b..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts +++ /dev/null @@ -1,14 +0,0 @@ -const calculateScale = ( - containerWidth: number, - containerHeight: number, - contentWidth: number, - contentHeight: number, - padding = 0.95 -): number => { - const scaleX = (containerWidth * padding) / contentWidth; - const scaleY = (containerHeight * padding) / contentHeight; - const scaleFit = Math.min(1, Math.min(scaleX, scaleY)); - return scaleFit ? scaleFit : 1; -}; - -export default calculateScale; diff --git a/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts b/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts deleted file mode 100644 index 44220c8ba4..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Gets a Blob from a canvas. - */ -export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise => - new Promise((resolve, reject) => { - canvas.toBlob((blob) => { - if (blob) { - resolve(blob); - return; - } - reject('Unable to create Blob'); - }); - }); diff --git a/invokeai/frontend/web/src/features/canvas/util/colorToString.ts b/invokeai/frontend/web/src/features/canvas/util/colorToString.ts deleted file mode 100644 index 25d79fed5a..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/colorToString.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { RgbaColor, RgbColor } from 'react-colorful'; - -export const rgbaColorToString = (color: RgbaColor): string => { - const { r, g, b, a } = color; - return `rgba(${r}, ${g}, ${b}, ${a})`; -}; - -export const rgbColorToString = (color: RgbColor): string => { - const { r, g, b } = color; - return `rgba(${r}, ${g}, ${b})`; -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/constants.ts b/invokeai/frontend/web/src/features/canvas/util/constants.ts deleted file mode 100644 index 3291732ecc..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/constants.ts +++ /dev/null @@ -1,14 +0,0 @@ -// canvas wheel zoom exponential scale factor -export const CANVAS_SCALE_BY = 0.999; - -// minimum (furthest-zoomed-out) scale -export const MIN_CANVAS_SCALE = 0.1; - -// maximum (furthest-zoomed-in) scale -export const MAX_CANVAS_SCALE = 20; - -// padding given to initial image/bounding box when stage view is reset -export const STAGE_PADDING_PERCENTAGE = 0.95; - -export const COLOR_PICKER_SIZE = 30; -export const COLOR_PICKER_STROKE_RADIUS = 10; diff --git a/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts b/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts deleted file mode 100644 index d0a71dee40..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { CanvasMaskLine } from 'features/canvas/store/canvasTypes'; -import Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; - -/** - * Creates a stage from array of mask objects. - * We cannot just convert the mask layer to a blob because it uses a texture with transparent areas. - * So instead we create a new stage with the mask layer and composite it onto a white background. - */ -const createMaskStage = async ( - lines: CanvasMaskLine[], - boundingBox: IRect, - shouldInvertMask: boolean -): Promise => { - // create an offscreen canvas and add the mask to it - const { width, height } = boundingBox; - - const offscreenContainer = document.createElement('div'); - - const maskStage = new Konva.Stage({ - container: offscreenContainer, - width: width, - height: height, - }); - - const baseLayer = new Konva.Layer(); - const maskLayer = new Konva.Layer(); - - // composite the image onto the mask layer - baseLayer.add( - new Konva.Rect({ - ...boundingBox, - fill: shouldInvertMask ? 'black' : 'white', - }) - ); - - lines.forEach((line) => - maskLayer.add( - new Konva.Line({ - points: line.points, - stroke: shouldInvertMask ? 'white' : 'black', - strokeWidth: line.strokeWidth * 2, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - shadowForStrokeEnabled: false, - globalCompositeOperation: line.tool === 'brush' ? 'source-over' : 'destination-out', - }) - ) - ); - - maskStage.add(baseLayer); - maskStage.add(maskLayer); - - // you'd think we can't do this until we finish with the maskStage, but we can - offscreenContainer.remove(); - - return maskStage; -}; - -export default createMaskStage; diff --git a/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts b/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts deleted file mode 100644 index d19cbe4612..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Gets an ImageData object from an image dataURL by drawing it to a canvas. - */ -export const dataURLToImageData = async (dataURL: string, width: number, height: number): Promise => - new Promise((resolve, reject) => { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - const image = new Image(); - - if (!ctx) { - canvas.remove(); - reject('Unable to get context'); - return; - } - - image.onload = function () { - ctx.drawImage(image, 0, 0); - canvas.remove(); - resolve(ctx.getImageData(0, 0, width, height)); - }; - - image.src = dataURL; - }); diff --git a/invokeai/frontend/web/src/features/canvas/util/downloadBlob.ts b/invokeai/frontend/web/src/features/canvas/util/downloadBlob.ts deleted file mode 100644 index 837e76c998..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/downloadBlob.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** Download a blob as a file */ -export const downloadBlob = (blob: Blob, fileName: string) => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - a.remove(); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/floorCoordinates.ts b/invokeai/frontend/web/src/features/canvas/util/floorCoordinates.ts deleted file mode 100644 index 4908868332..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/floorCoordinates.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Vector2d } from 'konva/lib/types'; - -const floorCoordinates = (coord: Vector2d): Vector2d => { - return { - x: Math.floor(coord.x), - y: Math.floor(coord.y), - }; -}; - -export default floorCoordinates; diff --git a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts deleted file mode 100644 index d37d644008..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore'; - -import { konvaNodeToBlob } from './konvaNodeToBlob'; - -/** - * Get the canvas base layer blob, with or without bounding box according to `shouldCropToBoundingBoxOnSave` - */ -export const getBaseLayerBlob = async (state: RootState, alwaysUseBoundingBox: boolean = false) => { - const canvasBaseLayer = $canvasBaseLayer.get(); - - if (!canvasBaseLayer) { - throw new Error('Problem getting base layer blob'); - } - - const { shouldCropToBoundingBoxOnSave, boundingBoxCoordinates, boundingBoxDimensions } = state.canvas; - - const clonedBaseLayer = canvasBaseLayer.clone(); - - clonedBaseLayer.scale({ x: 1, y: 1 }); - - const absPos = clonedBaseLayer.getAbsolutePosition(); - - const boundingBox = - shouldCropToBoundingBoxOnSave || alwaysUseBoundingBox - ? { - x: boundingBoxCoordinates.x + absPos.x, - y: boundingBoxCoordinates.y + absPos.y, - width: boundingBoxDimensions.width, - height: boundingBoxDimensions.height, - } - : clonedBaseLayer.getClientRect(); - - return konvaNodeToBlob(clonedBaseLayer, boundingBox); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts b/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts deleted file mode 100644 index d17096cb71..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { logger } from 'app/logging/logger'; -import { $canvasBaseLayer, $canvasStage } from 'features/canvas/store/canvasNanostore'; -import type { CanvasLayerState, Dimensions } from 'features/canvas/store/canvasTypes'; -import { isCanvasMaskLine } from 'features/canvas/store/canvasTypes'; -import { konvaNodeToImageData } from 'features/canvas/util/konvaNodeToImageData'; -import type { Vector2d } from 'konva/lib/types'; - -import createMaskStage from './createMaskStage'; -import { konvaNodeToBlob } from './konvaNodeToBlob'; - -/** - * Gets Blob and ImageData objects for the base and mask layers - */ -export const getCanvasData = async ( - layerState: CanvasLayerState, - boundingBoxCoordinates: Vector2d, - boundingBoxDimensions: Dimensions, - isMaskEnabled: boolean, - shouldPreserveMaskedArea: boolean -) => { - const log = logger('canvas'); - - const canvasBaseLayer = $canvasBaseLayer.get(); - const canvasStage = $canvasStage.get(); - - if (!canvasBaseLayer || !canvasStage) { - log.error('Unable to find canvas / stage'); - return; - } - - const boundingBox = { - ...boundingBoxCoordinates, - ...boundingBoxDimensions, - }; - - // Clone the base layer so we don't affect the visible base layer - const clonedBaseLayer = canvasBaseLayer.clone(); - - // Scale it to 100% so we get full resolution - clonedBaseLayer.scale({ x: 1, y: 1 }); - - // absolute position is needed to get the bounding box coords relative to the base layer - const absPos = clonedBaseLayer.getAbsolutePosition(); - - const offsetBoundingBox = { - x: boundingBox.x + absPos.x, - y: boundingBox.y + absPos.y, - width: boundingBox.width, - height: boundingBox.height, - }; - - // For the base layer, use the offset boundingBox - const baseBlob = await konvaNodeToBlob(clonedBaseLayer, offsetBoundingBox); - const baseImageData = await konvaNodeToImageData(clonedBaseLayer, offsetBoundingBox); - - // For the mask layer, use the normal boundingBox - const maskStage = await createMaskStage( - isMaskEnabled ? layerState.objects.filter(isCanvasMaskLine) : [], // only include mask lines, and only if mask is enabled - boundingBox, - shouldPreserveMaskedArea - ); - const maskBlob = await konvaNodeToBlob(maskStage, boundingBox); - const maskImageData = await konvaNodeToImageData(maskStage, boundingBox); - - return { - baseBlob, - baseImageData, - maskBlob, - maskImageData, - }; -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getCanvasGenerationMode.ts b/invokeai/frontend/web/src/features/canvas/util/getCanvasGenerationMode.ts deleted file mode 100644 index f0a3464986..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getCanvasGenerationMode.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { areAnyPixelsBlack, getImageDataTransparency } from 'common/util/arrayBuffer'; -import type { GenerationMode } from 'features/canvas/store/canvasTypes'; - -export const getCanvasGenerationMode = (baseImageData: ImageData, maskImageData: ImageData): GenerationMode => { - const { isPartiallyTransparent: baseIsPartiallyTransparent, isFullyTransparent: baseIsFullyTransparent } = - getImageDataTransparency(baseImageData.data); - - // check mask for black - const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data); - - if (baseIsPartiallyTransparent) { - if (baseIsFullyTransparent) { - return 'txt2img'; - } - - return 'outpaint'; - } else { - if (doesMaskHaveBlackPixels) { - return 'inpaint'; - } - - return 'img2img'; - } -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getColoredMaskSVG.ts b/invokeai/frontend/web/src/features/canvas/util/getColoredMaskSVG.ts deleted file mode 100644 index 47e1100447..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getColoredMaskSVG.ts +++ /dev/null @@ -1,81 +0,0 @@ -export const getColoredMaskSVG = (color: string) => { - return `data:image/svg+xml;utf8, - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`.replaceAll('black', color); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts deleted file mode 100644 index a5fbc99922..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore'; - -import { konvaNodeToBlob } from './konvaNodeToBlob'; - -/** - * Gets the canvas base layer blob, without bounding box - */ -export const getFullBaseLayerBlob = async () => { - const canvasBaseLayer = $canvasBaseLayer.get(); - - if (!canvasBaseLayer) { - return; - } - - const clonedBaseLayer = canvasBaseLayer.clone(); - - clonedBaseLayer.scale({ x: 1, y: 1 }); - - return konvaNodeToBlob(clonedBaseLayer, clonedBaseLayer.getClientRect()); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts b/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts deleted file mode 100644 index 1250f66d52..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Stage } from 'konva/lib/Stage'; - -const getScaledCursorPosition = (stage: Stage) => { - const pointerPosition = stage.getPointerPosition(); - - const stageTransform = stage.getAbsoluteTransform().copy(); - - if (!pointerPosition || !stageTransform) { - return; - } - - const scaledCursorPosition = stageTransform.invert().point(pointerPosition); - - return { - x: scaledCursorPosition.x, - y: scaledCursorPosition.y, - }; -}; - -export default getScaledCursorPosition; diff --git a/invokeai/frontend/web/src/features/canvas/util/isInteractiveTarget.ts b/invokeai/frontend/web/src/features/canvas/util/isInteractiveTarget.ts deleted file mode 100644 index 74a09aa8e4..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/isInteractiveTarget.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isInputElement } from 'common/util/isInputElement'; - -export const isInteractiveTarget = (target: EventTarget | null) => { - if (target instanceof HTMLElement) { - return ( - target.tabIndex > -1 || - isInputElement(target) || - ['dialog', 'alertdialog'].includes(target.getAttribute('role') ?? '') - ); - } - - return false; -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToBlob.ts b/invokeai/frontend/web/src/features/canvas/util/konvaNodeToBlob.ts deleted file mode 100644 index e16988ea23..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToBlob.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; - -import { canvasToBlob } from './canvasToBlob'; - -/** - * Converts a Konva node to a Blob - * @param node - The Konva node to convert to a Blob - * @param boundingBox - The bounding box to crop to - * @returns A Promise that resolves with Blob of the node cropped to the bounding box - */ -export const konvaNodeToBlob = async (node: Konva.Node, boundingBox: IRect): Promise => { - return await canvasToBlob(node.toCanvas(boundingBox)); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToImageData.ts b/invokeai/frontend/web/src/features/canvas/util/konvaNodeToImageData.ts deleted file mode 100644 index 3b5780ae16..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToImageData.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; - -import { dataURLToImageData } from './dataURLToImageData'; - -/** - * Converts a Konva node to an ImageData object - * @param node - The Konva node to convert to an ImageData object - * @param boundingBox - The bounding box to crop to - * @returns A Promise that resolves with ImageData object of the node cropped to the bounding box - */ -export const konvaNodeToImageData = async (node: Konva.Node, boundingBox: IRect): Promise => { - // get a dataURL of the bbox'd region - const dataURL = node.toDataURL(boundingBox); - - return await dataURLToImageData(dataURL, boundingBox.width, boundingBox.height); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/roundToHundreth.ts b/invokeai/frontend/web/src/features/canvas/util/roundToHundreth.ts deleted file mode 100644 index 1b05e7f64d..0000000000 --- a/invokeai/frontend/web/src/features/canvas/util/roundToHundreth.ts +++ /dev/null @@ -1,5 +0,0 @@ -const roundToHundreth = (val: number): number => { - return Math.round(val * 100) / 100; -}; - -export default roundToHundreth; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 9b9f957dfc..4b4a9842a2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -10,8 +10,8 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setShouldInvertBrushSizeScrollDirection } from 'features/canvas/store/canvasSlice'; -import { RGGlobalOpacity } from 'features/controlLayers/components/RGGlobalOpacity'; +import { MaskOpacity } from 'features/controlLayers/components/MaskOpacity'; +import { invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -22,7 +22,7 @@ const ControlLayersSettingsPopover = () => { const dispatch = useAppDispatch(); const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); const onChangeInvertScroll = useCallback( - (e: ChangeEvent) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)), + (e: ChangeEvent) => dispatch(invertScrollChanged(e.target.checked)), [dispatch] ); return ( @@ -33,7 +33,7 @@ const ControlLayersSettingsPopover = () => { - + {t('unifiedCanvas.invertBrushSizeScrollDirection')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index f5ab9d1ba5..d99bffcc61 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -8,12 +8,17 @@ import { memo } from 'react'; export const HeadsUpDisplay = memo(() => { const stageAttrs = useStore($stageAttrs); const bbox = useAppSelector((s) => s.canvasV2.bbox); + const document = useAppSelector((s) => s.canvasV2.document); return ( + - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx similarity index 77% rename from invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx index e20026f3dd..58c659c65b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx @@ -1,19 +1,19 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgGlobalOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { maskOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const RGGlobalOpacity = memo(() => { +export const MaskOpacity = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const opacity = useAppSelector((s) => Math.round(s.canvasV2.maskFillOpacity * 100)); + const opacity = useAppSelector((s) => Math.round(s.canvasV2.settings.maskOpacity * 100)); const onChange = useCallback( (v: number) => { - dispatch(rgGlobalOpacityChanged({ opacity: v / 100 })); + dispatch(maskOpacityChanged(v / 100)); }, [dispatch] ); @@ -46,4 +46,4 @@ export const RGGlobalOpacity = memo(() => { ); }); -RGGlobalOpacity.displayName = 'RGGlobalOpacity'; +MaskOpacity.displayName = 'MaskOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx index 26b5cf624c..12297ab5a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx @@ -1,6 +1,6 @@ import { useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgbColorToString } from 'features/canvas/util/colorToString'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader'; import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx index 24df9dc558..471febea28 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx @@ -1,8 +1,8 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; -import { rgbColorToString } from 'features/canvas/util/colorToString'; import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 055070717d..a1c67ad86c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,10 +1,10 @@ -import { $alt, $ctrl, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-library'; +import { $alt, $ctrl, $meta, $shift, Flex, Heading } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { logger } from 'app/logging/logger'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; -import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; +import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; import { $bbox, @@ -70,7 +70,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const regions = useAppSelector((s) => s.canvasV2.regions); const tool = useAppSelector((s) => s.canvasV2.tool); const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier); - const maskFillOpacity = useAppSelector((s) => s.canvasV2.maskFillOpacity); + const maskOpacity = useAppSelector((s) => s.canvasV2.settings.maskOpacity); const bbox = useAppSelector((s) => s.canvasV2.bbox); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); @@ -95,10 +95,10 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const currentFill = useMemo(() => { if (selectedEntity && selectedEntity.type === 'regional_guidance') { - return { ...selectedEntity.fill, a: maskFillOpacity }; + return { ...selectedEntity.fill, a: maskOpacity }; } return tool.fill; - }, [maskFillOpacity, selectedEntity, tool.fill]); + }, [maskOpacity, selectedEntity, tool.fill]); const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); const dpr = useDevicePixelRatio({ round: false }); @@ -106,7 +106,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, useLayoutEffect(() => { $toolState.set(tool); $selectedEntity.set(selectedEntity); - $bbox.set(bbox); + $bbox.set({ x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }); $currentFill.set(currentFill); }, [selectedEntity, tool, bbox, currentFill]); @@ -291,6 +291,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const resizeObserver = new ResizeObserver(fitStageToContainer); resizeObserver.observe(container); fitStageToContainer(); + renderBackgroundLayer(stage); return () => { resizeObserver.disconnect(); @@ -352,23 +353,13 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, layers, controlAdapters, regions, - maskFillOpacity, + maskOpacity, tool.selected, selectedEntity, getImageDTO, onPosChanged ); - }, [ - controlAdapters, - layers, - maskFillOpacity, - onPosChanged, - regions, - renderers, - selectedEntity, - stage, - tool.selected, - ]); + }, [controlAdapters, layers, maskOpacity, onPosChanged, regions, renderers, selectedEntity, stage, tool.selected]); // useLayoutEffect(() => { // if (asPreview) { @@ -414,15 +405,6 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { return ( - {!asPreview && } { ref={containerRef} tabIndex={-1} borderRadius="base" + border={1} + borderStyle="solid" + borderColor="base.700" overflow="hidden" data-testid="control-layers-canvas" /> diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts index 9fd691bbda..e7a3e35d2b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts @@ -23,7 +23,7 @@ export const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)'; /** * The target spacing of individual points of brush strokes, as a percentage of the brush size. */ -export const BRUSH_SPACING_PCT = 10; +export const BRUSH_SPACING_TARGET_SCALE = 0.1; /** * The minimum brush spacing in pixels. @@ -54,3 +54,13 @@ export const MIN_CANVAS_SCALE = 0.1; * Maximum (furthest-zoomed-in) scale */ export const MAX_CANVAS_SCALE = 20; + +/** + * The fine grid size of the canvas + */ +export const CANVAS_GRID_SIZE_FINE = 8; + +/** + * The coarse grid size of the canvas + */ +export const CANVAS_GRID_SIZE_COARSE = 64; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index bc8f94d31e..849a164bb7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,5 +1,4 @@ -import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; -import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; +import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { BrushLineAddedArg, @@ -16,6 +15,14 @@ import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; +import { + BRUSH_SPACING_TARGET_SCALE, + CANVAS_SCALE_BY, + MAX_BRUSH_SPACING_PX, + MAX_CANVAS_SCALE, + MIN_BRUSH_SPACING_PX, + MIN_CANVAS_SCALE, +} from './constants'; import { PREVIEW_TOOL_GROUP_ID } from './naming'; type Arg = { @@ -60,6 +67,18 @@ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastC return pos; }; +const calculateNewBrushSize = (brushSize: number, delta: number) => { + // This equation was derived by fitting a curve to the desired brush sizes and deltas + // see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565 + const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize); + // This needs to be clamped to prevent the delta from getting too large + const finalDelta = clamp(targetDelta, -20, 20); + // The new brush size is also clamped to prevent it from getting too large or small + const newBrushSize = clamp(brushSize + finalDelta, 1, 500); + + return newBrushSize; +}; + /** * Adds the next point to a line if the cursor has moved far enough from the last point. * @param layerId The layer to (maybe) add the point to @@ -82,7 +101,13 @@ const maybeAddNextPoint = ( // 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; + const minSpacingPx = clamp( + toolState.selected === 'brush' + ? toolState.brush.width * BRUSH_SPACING_TARGET_SCALE + : toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE, + MIN_BRUSH_SPACING_PX, + MAX_BRUSH_SPACING_PX + ); 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) < minSpacingPx) { @@ -354,6 +379,7 @@ export const setStageEventHandlers = ({ } }); + //#region wheel stage.on('wheel', (e) => { e.evt.preventDefault(); @@ -393,9 +419,11 @@ export const setStageEventHandlers = ({ stage.scaleY(newScale); stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); + renderBackgroundLayer(stage); } }); + //#region dragmove stage.on('dragmove', () => { setStageAttrs({ x: stage.x(), @@ -404,21 +432,22 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); + renderBackgroundLayer(stage); }); + //#region dragend stage.on('dragend', () => { // Stage position should always be an integer, else we get fractional pixels which are blurry - stage.x(Math.floor(stage.x())); - stage.y(Math.floor(stage.y())); setStageAttrs({ - x: stage.x(), - y: stage.y(), + x: Math.floor(stage.x()), + y: Math.floor(stage.y()), width: stage.width(), height: stage.height(), scale: stage.scaleX(), }); }); + //#region key const onKeyDown = (e: KeyboardEvent) => { if (e.repeat) { return; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts new file mode 100644 index 0000000000..4d898f31bd --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -0,0 +1,115 @@ +import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; +import Konva from 'konva'; + +const baseGridLineColor = getArbitraryBaseColor(27); +const fineGridLineColor = getArbitraryBaseColor(18); + +const getGridSpacing = (scale: number): number => { + if (scale >= 2) { + return 8; + } + if (scale >= 1 && scale < 2) { + return 16; + } + if (scale >= 0.5 && scale < 1) { + return 32; + } + if (scale >= 0.25 && scale < 0.5) { + return 64; + } + if (scale >= 0.125 && scale < 0.25) { + return 128; + } + return 256; +}; + +const getBackgroundLayer = (stage: Konva.Stage): Konva.Layer => { + let background = stage.findOne('#background'); + if (background) { + return background; + } + + background = new Konva.Layer({ id: 'background' }); + stage.add(background); + return background; +}; + +export const renderBackgroundLayer = (stage: Konva.Stage): void => { + const background = getBackgroundLayer(stage); + background.zIndex(0); + const scale = stage.scaleX(); + const gridSpacing = getGridSpacing(scale); + const x = stage.x(); + const y = stage.y(); + const width = stage.width(); + const height = stage.height(); + const stageRect = { + x1: 0, + y1: 0, + x2: width, + y2: height, + offset: { + x: x / scale, + y: y / scale, + }, + }; + + const gridOffset = { + x: Math.ceil(x / scale / gridSpacing) * gridSpacing, + y: Math.ceil(y / scale / gridSpacing) * gridSpacing, + }; + + const gridRect = { + x1: -gridOffset.x, + y1: -gridOffset.y, + x2: width / scale - gridOffset.x + gridSpacing, + y2: height / scale - gridOffset.y + gridSpacing, + }; + + const gridFullRect = { + x1: Math.min(stageRect.x1, gridRect.x1), + y1: Math.min(stageRect.y1, gridRect.y1), + x2: Math.max(stageRect.x2, gridRect.x2), + y2: Math.max(stageRect.y2, gridRect.y2), + }; + + const // find the x & y size of the grid + xSize = gridFullRect.x2 - gridFullRect.x1; + const ySize = gridFullRect.y2 - gridFullRect.y1; + // compute the number of steps required on each axis. + const xSteps = Math.round(xSize / gridSpacing) + 1; + const ySteps = Math.round(ySize / gridSpacing) + 1; + + const strokeWidth = 1 / scale; + let _x = 0; + let _y = 0; + + background.destroyChildren(); + + for (let i = 0; i < xSteps; i++) { + _x = gridFullRect.x1 + i * gridSpacing; + background.add( + new Konva.Line({ + x: _x, + y: gridFullRect.y1, + points: [0, 0, 0, ySize], + stroke: _x % 64 ? fineGridLineColor : baseGridLineColor, + strokeWidth, + listening: false, + }) + ); + } + for (let i = 0; i < ySteps; i++) { + _y = gridFullRect.y1 + i * gridSpacing; + background.add( + new Konva.Line({ + x: gridFullRect.x1, + y: _y, + points: [0, 0, xSize, 0], + stroke: _y % 64 ? fineGridLineColor : baseGridLineColor, + strokeWidth, + listening: false, + }) + ); + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index 5b3df88aea..a006f481c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -1,5 +1,4 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL'; import { CA_LAYER_IMAGE_NAME, LAYER_BBOX_NAME, @@ -7,6 +6,7 @@ import { RG_LAYER_OBJECT_GROUP_NAME, } from 'features/controlLayers/konva/naming'; import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; +import { imageDataToDataURL } from "features/controlLayers/konva/util"; import type { ControlAdapterData, LayerData, RegionalGuidanceData } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; 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 d5dc53f3be..43db3554ee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -50,7 +50,7 @@ const renderLayers = ( } } // We'll need to ensure the tool preview layer is on top of the rest of the layers - let zIndex = 0; + let zIndex = 1; for (const layer of layers) { renderRasterLayer(stage, layer, tool, zIndex, onPosChanged); zIndex++; 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 9074f24046..dd4ba645e2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,17 +1,11 @@ -import { rgbaColorToString } from 'features/canvas/util/colorToString'; +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { getLayerBboxId, getObjectGroupId, LAYER_BBOX_NAME, PREVIEW_GENERATION_BBOX_DUMMY_RECT, } from 'features/controlLayers/konva/naming'; -import type { - BrushLine, - CanvasEntity, - EraserLine, - ImageObject, - 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'; 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 4ee22d71a6..2ca1573041 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -1,10 +1,6 @@ +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { - BBOX_SELECTED_STROKE, - BRUSH_BORDER_INNER_COLOR, - BRUSH_BORDER_OUTER_COLOR, -} from 'features/controlLayers/konva/constants'; +import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants'; import { PREVIEW_BRUSH_BORDER_INNER_ID, PREVIEW_BRUSH_BORDER_OUTER_ID, @@ -206,9 +202,10 @@ export const getBboxPreviewGroup = ( height, }; - // Here we _could_ go ahead and update the bboxRect's attrs directly with the new transform, and reset its scale to 1. - // However, we have another function that renders the bbox when its internal state changes, so we will rely on that - // to set the new attrs. + // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. + // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. + // Gotta be a way to avoid setting it twice... + bboxRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); // Update the bbox in internal state. onBboxTransformed(bbox); @@ -281,7 +278,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { if (toolPreviewGroup) { return toolPreviewGroup; } - + const scale = stage.scaleX(); toolPreviewGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); // Create the brush preview group & circles @@ -296,7 +293,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { id: PREVIEW_BRUSH_BORDER_INNER_ID, listening: false, stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: 1, + strokeWidth: 1 / scale, strokeEnabled: true, }); brushPreviewGroup.add(brushPreviewBorderInner); @@ -304,7 +301,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { id: PREVIEW_BRUSH_BORDER_OUTER_ID, listening: false, stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: 1, + strokeWidth: 1 / scale, strokeEnabled: true, }); brushPreviewGroup.add(brushPreviewBorderOuter); @@ -313,8 +310,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { const rectPreview = new Konva.Rect({ id: PREVIEW_RECT_ID, listening: false, - stroke: BBOX_SELECTED_STROKE, - strokeWidth: 1, + strokeEnabled: false, }); toolPreviewGroup.add(rectPreview); 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 2f7f259baa..b6033416cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -1,4 +1,4 @@ -import { rgbColorToString } from 'features/canvas/util/colorToString'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants'; import { COMPOSITING_RECT_NAME, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 1e28392f2a..ba0f3a3343 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -12,11 +12,11 @@ import { RG_LAYER_NAME, RG_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; -import type Konva from 'konva'; +import type { RgbaColor } from 'features/controlLayers/store/types'; +import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; -import type { Vector2d } from 'konva/lib/types'; +import type { IRect, Vector2d } from 'konva/lib/types'; -//#region getScaledFlooredCursorPosition /** * Gets the scaled and floored cursor position on the stage. If the cursor is not currently over the stage, returns null. * @param stage The konva stage @@ -33,9 +33,7 @@ export const getScaledFlooredCursorPosition = (stage: Konva.Stage): Vector2d | n y: Math.floor(scaledCursorPosition.y), }; }; -//#endregion -//#region snapPosToStage /** * Snaps a position to the edge of the stage if within a threshold of the edge * @param pos The position to snap @@ -62,25 +60,19 @@ export const snapPosToStage = (pos: Vector2d, stage: Konva.Stage, snapPx = 10): } return snappedPos; }; -//#endregion -//#region getIsMouseDown /** * Checks if the left mouse button is currently pressed * @param e The konva event */ export const getIsMouseDown = (e: KonvaEventObject): boolean => e.evt.buttons === 1; -//#endregion -//#region getIsFocused /** * Checks if the stage is currently focused * @param stage The konva stage */ export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().contains(document.activeElement); -//#endregion -//#region mapId /** * Simple util to map an object to its id property. Serves as a minor optimization to avoid recreating a map callback * every time we need to map an object to its id, which happens very often. @@ -88,9 +80,7 @@ export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().c * @returns The object's id property */ export const mapId = (object: { id: string }): string => object.id; -//#endregion -//#region konva selector callbacks /** * Konva selection callback to select all renderable layers. This includes RG, CA II and Raster layers. * This can be provided to the `find` or `findOne` konva node methods. @@ -120,4 +110,141 @@ export const selectRasterObjects = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_ERASER_LINE_NAME || node.name() === RASTER_LAYER_RECT_SHAPE_NAME || node.name() === RASTER_LAYER_IMAGE_NAME; -//#endregion + +/** + * Convert a Blob to a data URL. + */ +export const blobToDataURL = (blob: Blob): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (_e) => resolve(reader.result as string); + reader.onerror = (_e) => reject(reader.error); + reader.onabort = (_e) => reject(new Error('Read aborted')); + reader.readAsDataURL(blob); + }); +}; + +/** + * Convert an ImageData object to a data URL. + */ +export function imageDataToDataURL(imageData: ImageData): string { + const { width, height } = imageData; + + // Create a canvas to transfer the ImageData to + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + + // Draw the ImageData onto the canvas + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Unable to get canvas context'); + } + ctx.putImageData(imageData, 0, 0); + + // Convert the canvas to a data URL (base64) + return canvas.toDataURL(); +} + +/** + * Download a Blob as a file + */ +export const downloadBlob = (blob: Blob, fileName: string) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + a.remove(); +}; + +/** + * Gets a Blob from a HTMLCanvasElement. + */ +export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise => { + return new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + return; + } + reject('Unable to create Blob'); + }); + }); +}; + +/** + * Gets an ImageData object from an image dataURL by drawing it to a canvas. + */ +export const dataURLToImageData = async (dataURL: string, width: number, height: number): Promise => { + return new Promise((resolve, reject) => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + const image = new Image(); + + if (!ctx) { + canvas.remove(); + reject('Unable to get context'); + return; + } + + image.onload = function () { + ctx.drawImage(image, 0, 0); + canvas.remove(); + resolve(ctx.getImageData(0, 0, width, height)); + }; + + image.src = dataURL; + }); +}; + +/** + * Converts a Konva node to a Blob + * @param node - The Konva node to convert to a Blob + * @param boundingBox - The bounding box to crop to + * @returns A Promise that resolves with Blob of the node cropped to the bounding box + */ +export const konvaNodeToBlob = async (node: Konva.Node, boundingBox: IRect): Promise => { + return await canvasToBlob(node.toCanvas(boundingBox)); +}; + +/** + * Converts a Konva node to an ImageData object + * @param node - The Konva node to convert to an ImageData object + * @param boundingBox - The bounding box to crop to + * @returns A Promise that resolves with ImageData object of the node cropped to the bounding box + */ +export const konvaNodeToImageData = async (node: Konva.Node, boundingBox: IRect): Promise => { + // get a dataURL of the bbox'd region + const dataURL = node.toDataURL(boundingBox); + + return await dataURLToImageData(dataURL, boundingBox.width, boundingBox.height); +}; + +/** + * Gets the pixel under the cursor on the stage, or null if the cursor is not over the stage. + * @param stage The konva stage + */ +export const getPixelUnderCursor = (stage: Konva.Stage): RgbaColor | null => { + const cursorPos = stage.getPointerPosition(); + const pixelRatio = Konva.pixelRatio; + if (!cursorPos) { + return null; + } + const ctx = stage.toCanvas().getContext('2d'); + + if (!ctx) { + return null; + } + const [r, g, b, a] = ctx.getImageData(cursorPos.x * pixelRatio, cursorPos.y * pixelRatio, 1, 1).data; + + if (r === undefined || g === undefined || b === undefined || a === undefined) { + return null; + } + + return { r, g, b, a }; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts new file mode 100644 index 0000000000..dc75e2e8e6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -0,0 +1,39 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { BoundingBoxScaleMethod, CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; +import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; +import type { IRect } from 'konva/lib/types'; + +export const bboxReducers = { + scaledBboxChanged: (state, action: PayloadAction>) => { + const { width, height } = action.payload; + state.bbox.scaledWidth = width ?? state.bbox.scaledWidth; + state.bbox.scaledHeight = height ?? state.bbox.scaledHeight; + }, + bboxScaleMethodChanged: (state, action: PayloadAction) => { + state.bbox.scaleMethod = action.payload; + + if (action.payload === 'auto') { + const bboxDims = { width: state.bbox.width, height: state.bbox.height }; + const optimalDimension = getOptimalDimension(state.params.model); + const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); + state.bbox.scaledWidth = scaledBboxDims.width; + state.bbox.scaledHeight = scaledBboxDims.height; + } + }, + bboxChanged: (state, action: PayloadAction) => { + const { x, y, width, height } = action.payload; + state.bbox.x = x; + state.bbox.y = y; + state.bbox.width = width; + state.bbox.height = height; + + if (state.bbox.scaleMethod === 'auto') { + const bboxDims = { width: state.bbox.width, height: state.bbox.height }; + const optimalDimension = getOptimalDimension(state.params.model); + const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); + state.bbox.scaledWidth = scaledBboxDims.width; + state.bbox.scaledHeight = scaledBboxDims.height; + } + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 60abd47769..4f86bb1fb4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -3,23 +3,30 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; +import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; import { paramsReducers } from 'features/controlLayers/store/paramsReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; +import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; +import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; -import type { CanvasEntity, CanvasEntityIdentifier, CanvasV2State, RgbaColor, StageAttrs, Tool } from './types'; +import type { CanvasEntity, CanvasEntityIdentifier, CanvasV2State, RgbaColor, StageAttrs } from './types'; import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: null, + layers: [], + controlAdapters: [], + ipAdapters: [], + regions: [], tool: { selected: 'bbox', selectedBuffer: null, @@ -42,17 +49,20 @@ const initialState: CanvasV2State = { y: 0, width: 512, height: 512, - }, - scaledBbox: { - width: 512, - height: 512, scaleMethod: 'auto', + scaledWidth: 512, + scaledHeight: 512, + }, + settings: { + maskOpacity: 0.3, + // TODO(psyche): These are copied from old canvas state, need to be implemented + autoSave: false, + imageSmoothing: true, + preserveMaskedArea: false, + showHUD: true, + clipToBbox: false, + cropToBboxOnSave: false, }, - controlAdapters: [], - ipAdapters: [], - regions: [], - layers: [], - maskFillOpacity: 0.3, compositing: { maskBlur: 16, maskBlurMethod: 'box', @@ -105,6 +115,9 @@ export const canvasV2Slice = createSlice({ ...regionsReducers, ...paramsReducers, ...compositingReducers, + ...settingsReducers, + ...toolReducers, + ...bboxReducers, widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; @@ -126,30 +139,6 @@ export const canvasV2Slice = createSlice({ aspectRatioChanged: (state, action: PayloadAction) => { state.document.aspectRatio = action.payload; }, - bboxChanged: (state, action: PayloadAction) => { - state.bbox = action.payload; - }, - brushWidthChanged: (state, action: PayloadAction) => { - state.tool.brush.width = Math.round(action.payload); - }, - eraserWidthChanged: (state, action: PayloadAction) => { - state.tool.eraser.width = Math.round(action.payload); - }, - fillChanged: (state, action: PayloadAction) => { - state.tool.fill = action.payload; - }, - 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; - }, - maskFillOpacityChanged: (state, action: PayloadAction) => { - state.maskFillOpacity = action.payload; - }, entitySelected: (state, action: PayloadAction) => { state.selectedEntityIdentifier = action.payload; }, @@ -173,9 +162,11 @@ export const { invertScrollChanged, toolChanged, toolBufferChanged, - maskFillOpacityChanged, + maskOpacityChanged, entitySelected, allEntitiesDeleted, + scaledBboxChanged, + bboxScaleMethodChanged, // layers layerAdded, layerRecalled, @@ -238,7 +229,6 @@ export const { rgBboxChanged, rgDeleted, rgAllDeleted, - rgGlobalOpacityChanged, rgMovedForwardOne, rgMovedToFront, rgMovedBackwardOne, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts index 2020a1b732..ba36deaa32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts @@ -1,6 +1,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions'; import type { CanvasV2State } from 'features/controlLayers/store/types'; +import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; import type { @@ -67,10 +67,10 @@ export const paramsReducers = { state.bbox.width = bboxDims.width; state.bbox.height = bboxDims.height; - if (state.scaledBbox.scaleMethod === 'auto') { + if (state.bbox.scaleMethod === 'auto') { const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); - state.scaledBbox.width = scaledBboxDims.width; - state.scaledBbox.height = scaledBboxDims.height; + state.bbox.scaledWidth = scaledBboxDims.width; + state.bbox.scaledHeight = scaledBboxDims.height; } } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 35b584adb7..9d75e0e93f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -120,10 +120,6 @@ export const regionsReducers = { rgAllDeleted: (state) => { state.regions = []; }, - rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => { - const { opacity } = action.payload; - state.maskFillOpacity = opacity; - }, rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; const rg = selectRG(state, id); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index d57fee3d49..ebd59de480 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -1,13 +1,13 @@ import { createSelector } from '@reduxjs/toolkit'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; -export const selectEntityCount = createSelector(selectCanvasSlice, (canvasV2) => { +export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => { return ( canvasV2.regions.length + canvasV2.controlAdapters.length + canvasV2.ipAdapters.length + canvasV2.layers.length ); }); -export const selectOptimalDimension = createSelector(selectCanvasSlice, (canvasV2) => { +export const selectOptimalDimension = createSelector(selectCanvasV2Slice, (canvasV2) => { return getOptimalDimension(canvasV2.params.model); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts new file mode 100644 index 0000000000..d3b7dd40d9 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts @@ -0,0 +1,8 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; + +export const settingsReducers = { + maskOpacityChanged: (state, action: PayloadAction) => { + state.settings.maskOpacity = action.payload; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/test.ts b/invokeai/frontend/web/src/features/controlLayers/store/test.ts deleted file mode 100644 index 2426f9af2c..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { ActionReducerMapBuilder, PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; - -type MySlice = { - flavour: 'vanilla' | 'chocolate' | 'strawberry'; - sprinkles: boolean; - customers: { id: string; name: string }[]; -}; -const initialStateMySlice: MySlice = { flavour: 'vanilla', sprinkles: false, customers: [] }; - -const reducersInAnotherFile: SliceCaseReducers = { - sprinklesToggled: (state) => { - state.sprinkles = !state.sprinkles; - }, - customerAdded: { - reducer: (state, action: PayloadAction<{ id: string; name: string }>) => { - state.customers.push(action.payload); - }, - prepare: (name: string) => ({ payload: { name, id: crypto.randomUUID() } }), - }, -}; - -const extraReducersInAnotherFile = (builder: ActionReducerMapBuilder) => { - builder.addCase(otherSlice.actions.fooChanged, (state, action) => { - if (action.payload === 'bar') { - state.flavour = 'vanilla'; - } - }); -}; - -export const mySlice = createSlice({ - name: 'mySlice', - initialState: initialStateMySlice, - reducers: { - ...reducersInAnotherFile, - flavourChanged: (state, action: PayloadAction) => { - state.flavour = action.payload; - }, - }, - extraReducers: extraReducersInAnotherFile, -}); - -type OtherSlice = { - something: string; -}; - -const initialStateOtherSlice: OtherSlice = { something: 'foo' }; - -export const otherSlice = createSlice({ - name: 'otherSlice', - initialState: initialStateOtherSlice, - reducers: { - fooChanged: (state, action: PayloadAction) => { - state.something = action.payload; - }, - }, -}); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts new file mode 100644 index 0000000000..c1f14d7df4 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts @@ -0,0 +1,23 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { CanvasV2State, RgbaColor, Tool } from 'features/controlLayers/store/types'; + +export const toolReducers = { + brushWidthChanged: (state, action: PayloadAction) => { + state.tool.brush.width = Math.round(action.payload); + }, + eraserWidthChanged: (state, action: PayloadAction) => { + state.tool.eraser.width = Math.round(action.payload); + }, + fillChanged: (state, action: PayloadAction) => { + state.tool.fill = action.payload; + }, + 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; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index bce9d98fdf..14d4725fb6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -802,6 +802,10 @@ export type Dimensions = { export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; + layers: LayerData[]; + controlAdapters: ControlAdapterData[]; + ipAdapters: IPAdapterData[]; + regions: RegionalGuidanceData[]; tool: { selected: Tool; selectedBuffer: Tool | null; @@ -815,17 +819,24 @@ export type CanvasV2State = { height: ParameterHeight; aspectRatio: AspectRatioState; }; - bbox: IRect; - scaledBbox: { - scaleMethod: BoundingBoxScaleMethod; + settings: { + imageSmoothing: boolean; + maskOpacity: number; + showHUD: boolean; + autoSave: boolean; + preserveMaskedArea: boolean; + cropToBboxOnSave: boolean; + clipToBbox: boolean; + }; + bbox: { + x: number; + y: number; width: ParameterWidth; height: ParameterHeight; + scaleMethod: BoundingBoxScaleMethod; + scaledWidth: ParameterWidth; + scaledHeight: ParameterHeight; }; - layers: LayerData[]; - controlAdapters: ControlAdapterData[]; - ipAdapters: IPAdapterData[]; - regions: RegionalGuidanceData[]; - maskFillOpacity: number; compositing: { maskBlur: number; maskBlurMethod: ParameterMaskBlurMethod; diff --git a/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts similarity index 64% rename from invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts rename to invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts index de38d12cf5..d98d03f33e 100644 --- a/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts @@ -1,8 +1,14 @@ import { roundToMultiple } from 'common/util/roundDownToMultiple'; -import type { Dimensions } from 'features/canvas/store/canvasTypes'; -import { CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/constants'; +import { CANVAS_GRID_SIZE_FINE } from 'features/controlLayers/konva/constants'; +import type { Dimensions } from 'features/controlLayers/store/types'; -const getScaledBoundingBoxDimensions = (dimensions: Dimensions, optimalDimension: number) => { +/** + * Scales the bounding box dimensions to the optimal dimension. The optimal dimensions should be the trained dimension + * for the model. For example, 1024 for SDXL or 512 for SD1.5. + * @param dimensions The un-scaled bbox dimensions + * @param optimalDimension The optimal dimension to scale the bbox to + */ +export const getScaledBoundingBoxDimensions = (dimensions: Dimensions, optimalDimension: number): Dimensions => { const { width, height } = dimensions; const scaledDimensions = { width, height }; @@ -30,5 +36,3 @@ const getScaledBoundingBoxDimensions = (dimensions: Dimensions, optimalDimension return scaledDimensions; }; - -export default getScaledBoundingBoxDimensions; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 06eaf0be12..b1037f0b1f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,7 +1,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; +import { blobToDataURL } from "features/controlLayers/konva/util"; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; import { renderers } from 'features/controlLayers/konva/renderers/layers'; import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx index 5273b8c508..1a0d95d57a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx @@ -2,17 +2,15 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setBoundingBoxScaleMethod } from 'features/canvas/store/canvasSlice'; -import { isBoundingBoxScaleMethod } from 'features/canvas/store/canvasTypes'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { bboxScaleMethodChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { isBoundingBoxScaleMethod } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamScaleBeforeProcessing = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const boundingBoxScaleMethod = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod); - const optimalDimension = useAppSelector(selectOptimalDimension); + const scaleMethod = useAppSelector((s) => s.canvasV2.bbox.scaleMethod); const OPTIONS: ComboboxOption[] = useMemo( () => [ @@ -28,15 +26,12 @@ const ParamScaleBeforeProcessing = () => { if (!isBoundingBoxScaleMethod(v?.value)) { return; } - dispatch(setBoundingBoxScaleMethod(v.value, optimalDimension)); + dispatch(bboxScaleMethodChanged(v.value)); }, - [dispatch, optimalDimension] + [dispatch] ); - const value = useMemo( - () => OPTIONS.find((o) => o.value === boundingBoxScaleMethod), - [boundingBoxScaleMethod, OPTIONS] - ); + const value = useMemo(() => OPTIONS.find((o) => o.value === scaleMethod), [scaleMethod, OPTIONS]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx index 0c6aa502ce..71a5dc28c9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx @@ -1,6 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setScaledBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { scaledBboxChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,8 +9,8 @@ const ParamScaledHeight = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const isManual = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod === 'manual'); - const height = useAppSelector((s) => s.canvasV2.scaledBbox.height); + const isManual = useAppSelector((s) => s.canvasV2.bbox.scaleMethod === 'manual'); + const height = useAppSelector((s) => s.canvasV2.bbox.scaledHeight); const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.numberInputMin); @@ -20,7 +20,7 @@ const ParamScaledHeight = () => { const onChange = useCallback( (height: number) => { - dispatch(setScaledBoundingBoxDimensions({ height })); + dispatch(scaledBboxChanged({ height })); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx index 52bc567c18..ed09e4599a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx @@ -1,6 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setScaledBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { scaledBboxChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,8 +9,8 @@ const ParamScaledWidth = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const isManual = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod === 'manual'); - const width = useAppSelector((s) => s.canvasV2.scaledBbox.width); + const isManual = useAppSelector((s) => s.canvasV2.bbox.scaleMethod === 'manual'); + const width = useAppSelector((s) => s.canvasV2.bbox.scaledWidth); const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.numberInputMin); @@ -19,7 +19,7 @@ const ParamScaledWidth = () => { const fineStep = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.fineStep); const onChange = useCallback( (width: number) => { - dispatch(setScaledBoundingBoxDimensions({ width })); + dispatch(scaledBboxChanged({ width })); }, [dispatch] );