mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
refactor(ui): port remaining canvasV1 rendering logic to V2, remove old code
This commit is contained in:
parent
7ef4553fc9
commit
5184d05bc2
@ -4,9 +4,9 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
|
|||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||||
import { parseify } from 'common/util/serialize';
|
import { parseify } from 'common/util/serialize';
|
||||||
import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice';
|
import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice';
|
||||||
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
|
||||||
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
||||||
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
||||||
|
import { blobToDataURL } from "features/controlLayers/konva/util";
|
||||||
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
||||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||||
import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph';
|
import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph';
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import type { AppDispatch, RootState } from 'app/store/store';
|
import type { AppDispatch, RootState } from 'app/store/store';
|
||||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
|
||||||
import {
|
import {
|
||||||
caImageChanged,
|
caImageChanged,
|
||||||
caProcessedImageChanged,
|
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
|
// 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) => {
|
imageDTOs.forEach((imageDTO) => {
|
||||||
deleteNodesImages(state, dispatch, imageDTO);
|
deleteNodesImages(state, dispatch, imageDTO);
|
||||||
deleteControlAdapterImages(state, dispatch, imageDTO);
|
deleteControlAdapterImages(state, dispatch, imageDTO);
|
||||||
|
@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger';
|
|||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { parseify } from 'common/util/serialize';
|
import { parseify } from 'common/util/serialize';
|
||||||
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
|
||||||
import {
|
import {
|
||||||
boardIdSelected,
|
boardIdSelected,
|
||||||
galleryViewChanged,
|
galleryViewChanged,
|
||||||
@ -12,7 +11,6 @@ import {
|
|||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants';
|
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { getCategories, getListImagesUrl } from 'services/api/util';
|
import { getCategories, getListImagesUrl } from 'services/api/util';
|
||||||
@ -47,11 +45,10 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
|
|||||||
imageDTORequest.unsubscribe();
|
imageDTORequest.unsubscribe();
|
||||||
|
|
||||||
// Add canvas images to the staging area
|
// 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 (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) {
|
if (!imageDTO.is_intermediate) {
|
||||||
// update the total images for the board
|
// update the total images for the board
|
||||||
|
@ -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 (
|
|
||||||
<>
|
|
||||||
<Button onClick={onOpen} size="sm" leftIcon={<PiTrashSimpleFill />} isDisabled={isStaging}>
|
|
||||||
{t('unifiedCanvas.clearCanvasHistory')}
|
|
||||||
</Button>
|
|
||||||
<ConfirmationAlertDialog
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
title={t('unifiedCanvas.clearCanvasHistory')}
|
|
||||||
acceptCallback={acceptCallback}
|
|
||||||
acceptButtonText={t('unifiedCanvas.clearHistory')}
|
|
||||||
>
|
|
||||||
<p>{t('unifiedCanvas.clearCanvasHistoryMessage')}</p>
|
|
||||||
<br />
|
|
||||||
<p>{t('unifiedCanvas.clearCanvasHistoryConfirm')}</p>
|
|
||||||
</ConfirmationAlertDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default memo(ClearCanvasHistoryButtonModal);
|
|
@ -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<HTMLDivElement>(null);
|
|
||||||
const stageRef = useRef<Konva.Stage | null>(null);
|
|
||||||
const canvasBaseLayerRef = useRef<Konva.Layer | null>(null);
|
|
||||||
const isModifyingBoundingBox = useStore($isModifyingBoundingBox);
|
|
||||||
const isMovingStage = useStore($isMovingStage);
|
|
||||||
const isTransformingBoundingBox = useStore($isTransformingBoundingBox);
|
|
||||||
const isMouseOverBoundingBox = useStore($isMouseOverBoundingBox);
|
|
||||||
const 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<Vector2d>({ x: 0, y: 0 });
|
|
||||||
|
|
||||||
// Use refs for values that do not affect rendering, other values in redux
|
|
||||||
const didMouseMoveRef = useRef<boolean>(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<MouseEvent>) => 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 (
|
|
||||||
<Flex id="canvas-container" ref={containerRef} position="relative" height="100%" width="100%" borderRadius="base">
|
|
||||||
<Box position="absolute">
|
|
||||||
<ChakraStage
|
|
||||||
tabIndex={-1}
|
|
||||||
ref={canvasStageRefCallback}
|
|
||||||
sx={stageStyles}
|
|
||||||
x={stageCoordinates.x}
|
|
||||||
y={stageCoordinates.y}
|
|
||||||
width={stageDimensions.width}
|
|
||||||
height={stageDimensions.height}
|
|
||||||
scale={scale}
|
|
||||||
onTouchStart={handleMouseDown}
|
|
||||||
onTouchMove={handleMouseMove}
|
|
||||||
onTouchEnd={handleMouseUp}
|
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
onMouseLeave={handleMouseOut}
|
|
||||||
onMouseMove={handleMouseMove}
|
|
||||||
onMouseUp={handleMouseUp}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragMove={handleDragMove}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
onWheel={handleWheel}
|
|
||||||
draggable={(tool === 'move' || isStaging) && !isModifyingBoundingBox}
|
|
||||||
>
|
|
||||||
<Layer id="grid" visible={shouldShowGrid} listening={false}>
|
|
||||||
<IAICanvasGrid />
|
|
||||||
</Layer>
|
|
||||||
|
|
||||||
<Layer id="base" ref={canvasBaseLayerRefCallback} listening={false} imageSmoothingEnabled={shouldAntialias}>
|
|
||||||
<IAICanvasObjectRenderer />
|
|
||||||
</Layer>
|
|
||||||
<Layer id="mask" visible={isMaskEnabled && !isStaging} listening={false}>
|
|
||||||
<IAICanvasMaskLines visible={true} listening={false} />
|
|
||||||
<IAICanvasMaskCompositor listening={false} />
|
|
||||||
</Layer>
|
|
||||||
<Layer listening={false}>
|
|
||||||
<IAICanvasBoundingBoxOverlay />
|
|
||||||
</Layer>
|
|
||||||
<Layer id="preview" imageSmoothingEnabled={shouldAntialias}>
|
|
||||||
{!isStaging && <IAICanvasToolPreview visible={tool !== 'move'} listening={false} />}
|
|
||||||
<IAICanvasStagingArea listening={false} visible={isStaging} />
|
|
||||||
{shouldShowIntermediates && <IAICanvasIntermediateImage />}
|
|
||||||
<IAICanvasBoundingBox visible={shouldShowBoundingBox && !isStaging} />
|
|
||||||
</Layer>
|
|
||||||
</ChakraStage>
|
|
||||||
</Box>
|
|
||||||
<IAICanvasStatusText />
|
|
||||||
<IAICanvasStagingAreaToolbar />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvas);
|
|
@ -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 (
|
|
||||||
<Group listening={false}>
|
|
||||||
<Rect
|
|
||||||
offsetX={stageCoordinates.x / stageScale}
|
|
||||||
offsetY={stageCoordinates.y / stageScale}
|
|
||||||
height={stageDimensions.height / stageScale}
|
|
||||||
width={stageDimensions.width / stageScale}
|
|
||||||
fill="rgba(0,0,0,0.4)"
|
|
||||||
listening={false}
|
|
||||||
visible={shouldDarkenOutsideBoundingBox}
|
|
||||||
/>
|
|
||||||
<Rect
|
|
||||||
x={boundingBoxCoordinates.x}
|
|
||||||
y={boundingBoxCoordinates.y}
|
|
||||||
width={boundingBoxDimensions.width}
|
|
||||||
height={boundingBoxDimensions.height}
|
|
||||||
fill="rgb(255,255,255)"
|
|
||||||
listening={false}
|
|
||||||
visible={shouldDarkenOutsideBoundingBox}
|
|
||||||
globalCompositeOperation="destination-out"
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasBoundingBoxOverlay);
|
|
@ -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(
|
|
||||||
<KonvaLine
|
|
||||||
key={`x_${i}`}
|
|
||||||
x={_x}
|
|
||||||
y={gridFullRect.y1}
|
|
||||||
points={[0, 0, 0, ySize]}
|
|
||||||
stroke={_x % 64 ? fineGridLineColor : baseGridLineColor}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < ySteps; i++) {
|
|
||||||
_y = gridFullRect.y1 + i * gridSpacing;
|
|
||||||
gridLines.push(
|
|
||||||
<KonvaLine
|
|
||||||
key={`y_${i}`}
|
|
||||||
x={gridFullRect.x1}
|
|
||||||
y={_y}
|
|
||||||
points={[0, 0, xSize, 0]}
|
|
||||||
stroke={_y % 64 ? fineGridLineColor : baseGridLineColor}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return gridLines;
|
|
||||||
}, [stageDimensions, stageCoordinates, unscale, gridSpacing]);
|
|
||||||
|
|
||||||
return <Group listening={false}>{gridLines}</Group>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasGrid);
|
|
@ -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 <IAICanvasImageErrorFallback canvasImage={props.canvasImage} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Image x={x} y={y} image={image} listening={false} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasImage);
|
|
@ -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 (
|
|
||||||
<Group listening={false}>
|
|
||||||
<Rect
|
|
||||||
x={canvasImage.x}
|
|
||||||
y={canvasImage.y}
|
|
||||||
width={canvasImage.width}
|
|
||||||
height={canvasImage.height}
|
|
||||||
fill={rectFill}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
x={canvasImage.x}
|
|
||||||
y={canvasImage.y}
|
|
||||||
width={canvasImage.width}
|
|
||||||
height={canvasImage.height}
|
|
||||||
align="center"
|
|
||||||
verticalAlign="middle"
|
|
||||||
fontFamily='"Inter Variable", sans-serif'
|
|
||||||
fontSize={canvasImage.width / 16}
|
|
||||||
fontStyle="600"
|
|
||||||
text={t('common.imageFailedToLoad')}
|
|
||||||
fill={textFill}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasImageErrorFallback);
|
|
@ -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<HTMLImageElement | null>(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 (
|
|
||||||
<KonvaImage
|
|
||||||
x={boundingBox.x}
|
|
||||||
y={boundingBox.y}
|
|
||||||
width={boundingBox.width}
|
|
||||||
height={boundingBox.height}
|
|
||||||
image={loadedImageElement}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasIntermediateImage);
|
|
@ -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<HTMLImageElement | null>(null);
|
|
||||||
|
|
||||||
const [offset, setOffset] = useState<number>(0);
|
|
||||||
|
|
||||||
const rectRef = useRef<Konva.Rect>(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 (
|
|
||||||
<Rect
|
|
||||||
ref={rectRef}
|
|
||||||
offsetX={stageCoordinates.x / stageScale}
|
|
||||||
offsetY={stageCoordinates.y / stageScale}
|
|
||||||
height={stageDimensions.height / stageScale}
|
|
||||||
width={stageDimensions.width / stageScale}
|
|
||||||
fillPatternImage={fillPatternImage}
|
|
||||||
fillPatternOffsetY={!isNumber(offset) ? 0 : offset}
|
|
||||||
fillPatternRepeat="repeat"
|
|
||||||
fillPatternScale={fillPatternScale}
|
|
||||||
listening={true}
|
|
||||||
globalCompositeOperation="source-in"
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasMaskCompositor);
|
|
@ -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 (
|
|
||||||
<Group listening={false} {...props}>
|
|
||||||
{objects.filter(isCanvasMaskLine).map((line, i) => (
|
|
||||||
<Line
|
|
||||||
key={i}
|
|
||||||
points={line.points}
|
|
||||||
stroke="rgb(0,0,0)" // The lines can be any color, just need alpha > 0
|
|
||||||
strokeWidth={line.strokeWidth * 2}
|
|
||||||
tension={0}
|
|
||||||
lineCap="round"
|
|
||||||
lineJoin="round"
|
|
||||||
shadowForStrokeEnabled={false}
|
|
||||||
listening={false}
|
|
||||||
globalCompositeOperation={line.tool === 'brush' ? 'source-over' : 'destination-out'}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasLines);
|
|
@ -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 (
|
|
||||||
<Group name="outpainting-objects" listening={false}>
|
|
||||||
{objects.map((obj, i) => {
|
|
||||||
if (isCanvasBaseImage(obj)) {
|
|
||||||
return <IAICanvasImage key={i} canvasImage={obj} />;
|
|
||||||
} else if (isCanvasBaseLine(obj)) {
|
|
||||||
const line = (
|
|
||||||
<Line
|
|
||||||
key={i}
|
|
||||||
points={obj.points}
|
|
||||||
stroke={obj.color ? rgbaColorToString(obj.color) : 'rgb(0,0,0)'} // The lines can be any color, just need alpha > 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 (
|
|
||||||
<Group
|
|
||||||
key={i}
|
|
||||||
clipX={obj.clip.x}
|
|
||||||
clipY={obj.clip.y}
|
|
||||||
clipWidth={obj.clip.width}
|
|
||||||
clipHeight={obj.clip.height}
|
|
||||||
listening={false}
|
|
||||||
>
|
|
||||||
{line}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
} else if (isCanvasFillRect(obj)) {
|
|
||||||
return (
|
|
||||||
<Rect
|
|
||||||
key={i}
|
|
||||||
x={obj.x}
|
|
||||||
y={obj.y}
|
|
||||||
width={obj.width}
|
|
||||||
height={obj.height}
|
|
||||||
fill={rgbaColorToString(obj.color)}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (isCanvasEraseRect(obj)) {
|
|
||||||
return (
|
|
||||||
<Rect
|
|
||||||
key={i}
|
|
||||||
x={obj.x}
|
|
||||||
y={obj.y}
|
|
||||||
width={obj.width}
|
|
||||||
height={obj.height}
|
|
||||||
fill="rgb(255, 255, 255)"
|
|
||||||
globalCompositeOperation="destination-out"
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasObjectRenderer);
|
|
@ -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 (
|
|
||||||
<Group {...props}>
|
|
||||||
{shouldShowStagingImage && currentStagingAreaImage && <IAICanvasImage canvasImage={currentStagingAreaImage} />}
|
|
||||||
{shouldShowStagingOutline && (
|
|
||||||
<Group listening={false}>
|
|
||||||
<Rect
|
|
||||||
x={x}
|
|
||||||
y={y}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
strokeWidth={1}
|
|
||||||
stroke="white"
|
|
||||||
strokeScaleEnabled={false}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
<Rect
|
|
||||||
x={x}
|
|
||||||
y={y}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
dash={dash}
|
|
||||||
strokeWidth={1}
|
|
||||||
stroke="black"
|
|
||||||
strokeScaleEnabled={false}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasStagingArea);
|
|
@ -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 (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
tooltip={`${t('unifiedCanvas.discardCurrent')}`}
|
|
||||||
aria-label={t('unifiedCanvas.discardCurrent')}
|
|
||||||
icon={<PiXBold />}
|
|
||||||
onClick={handleDiscardStagingImage}
|
|
||||||
colorScheme="invokeBlue"
|
|
||||||
fontSize={16}
|
|
||||||
isDisabled={totalStagedImages <= 1}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
tooltip={`${t('unifiedCanvas.discardAll')} (Esc)`}
|
|
||||||
aria-label={t('unifiedCanvas.discardAll')}
|
|
||||||
icon={<PiTrashSimpleBold />}
|
|
||||||
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 (
|
|
||||||
<Flex
|
|
||||||
pos="absolute"
|
|
||||||
bottom={4}
|
|
||||||
gap={2}
|
|
||||||
w="100%"
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
onMouseEnter={handleMouseOver}
|
|
||||||
onMouseLeave={handleMouseOut}
|
|
||||||
>
|
|
||||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
|
||||||
<IconButton
|
|
||||||
tooltip={`${t('unifiedCanvas.previous')} (Left)`}
|
|
||||||
aria-label={`${t('unifiedCanvas.previous')} (Left)`}
|
|
||||||
icon={<PiArrowLeftBold />}
|
|
||||||
onClick={handlePrevImage}
|
|
||||||
colorScheme="invokeBlue"
|
|
||||||
isDisabled={!shouldShowStagingImage}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
colorScheme="base"
|
|
||||||
pointerEvents="none"
|
|
||||||
isDisabled={!shouldShowStagingImage}
|
|
||||||
minW={20}
|
|
||||||
>{`${currentIndex + 1}/${total}`}</Button>
|
|
||||||
<IconButton
|
|
||||||
tooltip={`${t('unifiedCanvas.next')} (Right)`}
|
|
||||||
aria-label={`${t('unifiedCanvas.next')} (Right)`}
|
|
||||||
icon={<PiArrowRightBold />}
|
|
||||||
onClick={handleNextImage}
|
|
||||||
colorScheme="invokeBlue"
|
|
||||||
isDisabled={!shouldShowStagingImage}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
|
||||||
<IconButton
|
|
||||||
tooltip={`${t('unifiedCanvas.accept')} (Enter)`}
|
|
||||||
aria-label={`${t('unifiedCanvas.accept')} (Enter)`}
|
|
||||||
icon={<PiCheckBold />}
|
|
||||||
onClick={handleAccept}
|
|
||||||
colorScheme="invokeBlue"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
tooltip={shouldShowStagingImage ? t('unifiedCanvas.showResultsOn') : t('unifiedCanvas.showResultsOff')}
|
|
||||||
aria-label={shouldShowStagingImage ? t('unifiedCanvas.showResultsOn') : t('unifiedCanvas.showResultsOff')}
|
|
||||||
data-alert={!shouldShowStagingImage}
|
|
||||||
icon={shouldShowStagingImage ? <PiEyeBold /> : <PiEyeSlashBold />}
|
|
||||||
onClick={handleToggleShouldShowStagingImage}
|
|
||||||
colorScheme="invokeBlue"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
|
||||||
aria-label={t('unifiedCanvas.saveToGallery')}
|
|
||||||
isDisabled={!imageDTO || !imageDTO.is_intermediate}
|
|
||||||
icon={<PiFloppyDiskBold />}
|
|
||||||
onClick={handleSaveToGallery}
|
|
||||||
colorScheme="invokeBlue"
|
|
||||||
/>
|
|
||||||
<ClearStagingIntermediatesIconButton />
|
|
||||||
</ButtonGroup>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasStagingAreaToolbar);
|
|
@ -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 (
|
|
||||||
<Flex
|
|
||||||
flexDirection="column"
|
|
||||||
position="absolute"
|
|
||||||
top={0}
|
|
||||||
insetInlineStart={0}
|
|
||||||
opacity={0.65}
|
|
||||||
display="flex"
|
|
||||||
fontSize="sm"
|
|
||||||
padding={1}
|
|
||||||
px={2}
|
|
||||||
minWidth={48}
|
|
||||||
margin={1}
|
|
||||||
borderRadius="base"
|
|
||||||
pointerEvents="none"
|
|
||||||
bg="base.800"
|
|
||||||
>
|
|
||||||
<GenerationModeStatusText />
|
|
||||||
<Box color={activeLayerColor}>{`${t('unifiedCanvas.activeLayer')}: ${t(`unifiedCanvas.${layer}`)}`}</Box>
|
|
||||||
<Box>{`${t('unifiedCanvas.canvasScale')}: ${canvasScaleString}%`}</Box>
|
|
||||||
{shouldPreserveMaskedArea && (
|
|
||||||
<Box color={warningColor}>
|
|
||||||
{t('unifiedCanvas.preserveMaskedArea')}: {t('common.on')}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{shouldShowBoundingBox && (
|
|
||||||
<Box color={boundingBoxColor}>{`${t('unifiedCanvas.boundingBox')}: ${boundingBoxDimensionsString}`}</Box>
|
|
||||||
)}
|
|
||||||
{shouldShowScaledBoundingBox && (
|
|
||||||
<Box color={boundingBoxColor}>{`${t(
|
|
||||||
'unifiedCanvas.scaledBoundingBox'
|
|
||||||
)}: ${scaledBoundingBoxDimensionsString}`}</Box>
|
|
||||||
)}
|
|
||||||
{shouldShowCanvasDebugInfo && (
|
|
||||||
<>
|
|
||||||
<Box>{`${t('unifiedCanvas.boundingBoxPosition')}: ${boundingBoxCoordinatesString}`}</Box>
|
|
||||||
<Box>{`${t('unifiedCanvas.canvasDimensions')}: ${canvasDimensionsString}`}</Box>
|
|
||||||
<Box>{`${t('unifiedCanvas.canvasPosition')}: ${canvasCoordinatesString}`}</Box>
|
|
||||||
<IAICanvasStatusTextCursorPos />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasStatusText);
|
|
@ -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 <Box>{`${t('unifiedCanvas.cursorPosition')}: ${cursorCoordinatesString}`}</Box>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasStatusTextCursorPos);
|
|
@ -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<number>(
|
|
||||||
() => (cursorPosition ? cursorPosition.x : stageDimensions.width / 2),
|
|
||||||
[cursorPosition, stageDimensions]
|
|
||||||
);
|
|
||||||
const brushY = useMemo<number>(
|
|
||||||
() => (cursorPosition ? cursorPosition.y : stageDimensions.height / 2),
|
|
||||||
[cursorPosition, stageDimensions]
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldDrawBrushPreview = useMemo(
|
|
||||||
() => !(isMovingBoundingBox || isTransformingBoundingBox || !cursorPosition),
|
|
||||||
[cursorPosition, isMovingBoundingBox, isTransformingBoundingBox]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!shouldDrawBrushPreview) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group listening={false} {...clip} {...props}>
|
|
||||||
{tool === 'colorPicker' ? (
|
|
||||||
<>
|
|
||||||
<Circle
|
|
||||||
x={brushX}
|
|
||||||
y={brushY}
|
|
||||||
radius={colorPickerOuterRadius}
|
|
||||||
stroke={brushColorString}
|
|
||||||
strokeWidth={COLOR_PICKER_STROKE_RADIUS}
|
|
||||||
strokeScaleEnabled={false}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
<Circle
|
|
||||||
x={brushX}
|
|
||||||
y={brushY}
|
|
||||||
radius={colorPickerInnerRadius}
|
|
||||||
stroke={colorPickerColorString}
|
|
||||||
strokeWidth={COLOR_PICKER_STROKE_RADIUS}
|
|
||||||
strokeScaleEnabled={false}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Circle
|
|
||||||
x={brushX}
|
|
||||||
y={brushY}
|
|
||||||
radius={radius}
|
|
||||||
fill={layer === 'mask' ? maskColorString : brushColorString}
|
|
||||||
globalCompositeOperation={tool === 'eraser' ? 'destination-out' : 'source-out'}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
<Circle
|
|
||||||
x={brushX}
|
|
||||||
y={brushY}
|
|
||||||
radius={radius}
|
|
||||||
stroke="rgba(255,255,255,0.4)"
|
|
||||||
strokeWidth={strokeWidth * 2}
|
|
||||||
strokeEnabled={true}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
<Circle
|
|
||||||
x={brushX}
|
|
||||||
y={brushY}
|
|
||||||
radius={radius}
|
|
||||||
stroke="rgba(0,0,0,1)"
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
strokeEnabled={true}
|
|
||||||
listening={false}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Circle x={brushX} y={brushY} radius={dotRadius * 2} fill="rgba(255,255,255,0.4)" listening={false} />
|
|
||||||
<Circle x={brushX} y={brushY} radius={dotRadius} fill="rgba(0,0,0,1)" listening={false} />
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasToolPreview);
|
|
@ -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<Konva.Transformer>(null);
|
|
||||||
const shapeRef = useRef<Konva.Rect>(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<DragEvent>) => {
|
|
||||||
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<Event>) => {
|
|
||||||
/**
|
|
||||||
* 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 (
|
|
||||||
<Group {...rest}>
|
|
||||||
<Rect
|
|
||||||
height={boundingBoxDimensions.height}
|
|
||||||
width={boundingBoxDimensions.width}
|
|
||||||
x={boundingBoxCoordinates.x}
|
|
||||||
y={boundingBoxCoordinates.y}
|
|
||||||
onMouseEnter={handleMouseEnterBoundingBox}
|
|
||||||
onMouseOver={handleMouseEnterBoundingBox}
|
|
||||||
onMouseLeave={handleMouseLeaveBoundingBox}
|
|
||||||
onMouseOut={handleMouseLeaveBoundingBox}
|
|
||||||
/>
|
|
||||||
<Rect
|
|
||||||
draggable={true}
|
|
||||||
fillEnabled={false}
|
|
||||||
height={boundingBoxDimensions.height}
|
|
||||||
hitStrokeWidth={hitStrokeWidth}
|
|
||||||
listening={!isDrawing && tool === 'move'}
|
|
||||||
onDragStart={handleStartedMoving}
|
|
||||||
onDragEnd={handleEndedModifying}
|
|
||||||
onDragMove={handleOnDragMove}
|
|
||||||
onMouseDown={handleStartedMoving}
|
|
||||||
onMouseOut={handleMouseOut}
|
|
||||||
onMouseOver={handleMouseOver}
|
|
||||||
onMouseEnter={handleMouseOver}
|
|
||||||
onMouseUp={handleEndedModifying}
|
|
||||||
onTransform={handleOnTransform}
|
|
||||||
onTransformEnd={handleEndedTransforming}
|
|
||||||
ref={shapeRef}
|
|
||||||
stroke={stroke}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
width={boundingBoxDimensions.width}
|
|
||||||
x={boundingBoxCoordinates.x}
|
|
||||||
y={boundingBoxCoordinates.y}
|
|
||||||
/>
|
|
||||||
<Transformer
|
|
||||||
anchorCornerRadius={3}
|
|
||||||
anchorDragBoundFunc={anchorDragBoundFunc}
|
|
||||||
anchorFill="rgba(212,216,234,1)"
|
|
||||||
anchorSize={15}
|
|
||||||
anchorStroke="rgb(42,42,42)"
|
|
||||||
borderDash={borderDash}
|
|
||||||
borderEnabled={true}
|
|
||||||
borderStroke="black"
|
|
||||||
draggable={false}
|
|
||||||
enabledAnchors={enabledAnchors}
|
|
||||||
flipEnabled={false}
|
|
||||||
ignoreStroke={true}
|
|
||||||
keepRatio={false}
|
|
||||||
listening={!isDrawing && tool === 'move'}
|
|
||||||
onDragStart={handleStartedMoving}
|
|
||||||
onDragEnd={handleEndedModifying}
|
|
||||||
onMouseDown={handleStartedTransforming}
|
|
||||||
onMouseUp={handleEndedTransforming}
|
|
||||||
onTransformEnd={handleEndedTransforming}
|
|
||||||
ref={transformerRef}
|
|
||||||
rotateEnabled={false}
|
|
||||||
shiftBehavior="none"
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasBoundingBox);
|
|
||||||
|
|
||||||
const emptyArray: string[] = [];
|
|
@ -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<HTMLInputElement>) => {
|
|
||||||
dispatch(setShouldPreserveMaskedArea(e.target.checked));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeMaskColor = useCallback(
|
|
||||||
(newColor: RgbaColor) => {
|
|
||||||
dispatch(setMaskColor(newColor));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover isLazy>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<IconButton
|
|
||||||
aria-label={t('unifiedCanvas.maskingOptions')}
|
|
||||||
tooltip={t('unifiedCanvas.maskingOptions')}
|
|
||||||
icon={<PiExcludeBold />}
|
|
||||||
isChecked={layer === 'mask'}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent>
|
|
||||||
<PopoverBody>
|
|
||||||
<Flex direction="column" gap={2}>
|
|
||||||
<FormControlGroup formLabelProps={formLabelProps}>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{`${t('unifiedCanvas.enableMask')} (H)`}</FormLabel>
|
|
||||||
<Checkbox isChecked={isMaskEnabled} onChange={handleToggleEnableMask} />
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.preserveMaskedArea')}</FormLabel>
|
|
||||||
<Checkbox isChecked={shouldPreserveMaskedArea} onChange={handleChangePreserveMaskedArea} />
|
|
||||||
</FormControl>
|
|
||||||
</FormControlGroup>
|
|
||||||
<Box pt={2} pb={2}>
|
|
||||||
<IAIColorPicker color={maskColor} onChange={handleChangeMaskColor} />
|
|
||||||
</Box>
|
|
||||||
<ButtonGroup isAttached={false}>
|
|
||||||
<Button size="sm" leftIcon={<PiFloppyDiskBackFill />} onClick={handleSaveMask}>
|
|
||||||
{t('unifiedCanvas.saveMask')}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" leftIcon={<PiTrashSimpleFill />} onClick={handleClearMask}>
|
|
||||||
{t('unifiedCanvas.clearMask')}
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Flex>
|
|
||||||
</PopoverBody>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasMaskOptions);
|
|
@ -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 (
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.redo')} (Ctrl+Shift+Z)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.redo')} (Ctrl+Shift+Z)`}
|
|
||||||
icon={<PiArrowClockwiseBold />}
|
|
||||||
onClick={handleRedo}
|
|
||||||
isDisabled={!canRedo}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasRedoButton);
|
|
@ -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<HTMLInputElement>) => dispatch(setShouldSnapToGrid(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeShouldShowIntermediates = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldShowIntermediates(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
const handleChangeShouldShowGrid = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldShowGrid(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
const handleChangeShouldDarkenOutsideBoundingBox = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
const handleChangeShouldInvertBrushSizeScrollDirection = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
const handleChangeShouldAutoSave = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldAutoSave(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
const handleChangeShouldCropToBoundingBoxOnSave = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
const handleChangeShouldRestrictStrokesToBox = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldRestrictStrokesToBox(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
const handleChangeShouldShowCanvasDebugInfo = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldShowCanvasDebugInfo(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
const handleChangeShouldAntialias = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldAntialias(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
const handleChangeShouldFitImageSize = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldFitImageSize(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<IconButton
|
|
||||||
tooltip={t('unifiedCanvas.canvasSettings')}
|
|
||||||
aria-label={t('unifiedCanvas.canvasSettings')}
|
|
||||||
icon={<PiGearSixBold />}
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent>
|
|
||||||
<PopoverBody>
|
|
||||||
<Flex direction="column" gap={2}>
|
|
||||||
<FormControlGroup formLabelProps={formLabelProps}>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.showIntermediates')}</FormLabel>
|
|
||||||
<Checkbox isChecked={shouldShowIntermediates} onChange={handleChangeShouldShowIntermediates} />
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.showGrid')}</FormLabel>
|
|
||||||
<Checkbox isChecked={shouldShowGrid} onChange={handleChangeShouldShowGrid} />
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.snapToGrid')}</FormLabel>
|
|
||||||
<Checkbox isChecked={shouldSnapToGrid} onChange={handleChangeShouldSnapToGrid} />
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.darkenOutsideSelection')}</FormLabel>
|
|
||||||
<Checkbox
|
|
||||||
isChecked={shouldDarkenOutsideBoundingBox}
|
|
||||||
onChange={handleChangeShouldDarkenOutsideBoundingBox}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.autoSaveToGallery')}</FormLabel>
|
|
||||||
<Checkbox isChecked={shouldAutoSave} onChange={handleChangeShouldAutoSave} />
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.saveBoxRegionOnly')}</FormLabel>
|
|
||||||
<Checkbox
|
|
||||||
isChecked={shouldCropToBoundingBoxOnSave}
|
|
||||||
onChange={handleChangeShouldCropToBoundingBoxOnSave}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.limitStrokesToBox')}</FormLabel>
|
|
||||||
<Checkbox isChecked={shouldRestrictStrokesToBox} onChange={handleChangeShouldRestrictStrokesToBox} />
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
|
|
||||||
<Checkbox
|
|
||||||
isChecked={shouldInvertBrushSizeScrollDirection}
|
|
||||||
onChange={handleChangeShouldInvertBrushSizeScrollDirection}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.showCanvasDebugInfo')}</FormLabel>
|
|
||||||
<Checkbox isChecked={shouldShowCanvasDebugInfo} onChange={handleChangeShouldShowCanvasDebugInfo} />
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.antialiasing')}</FormLabel>
|
|
||||||
<Checkbox isChecked={shouldAntialias} onChange={handleChangeShouldAntialias} />
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.initialFitImageSize')}</FormLabel>
|
|
||||||
<Checkbox isChecked={shouldFitImageSize} onChange={handleChangeShouldFitImageSize} />
|
|
||||||
</FormControl>
|
|
||||||
</FormControlGroup>
|
|
||||||
<ClearCanvasHistoryButtonModal />
|
|
||||||
</Flex>
|
|
||||||
</PopoverBody>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasSettingsButtonPopover);
|
|
@ -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 (
|
|
||||||
<ButtonGroup>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.brush')} (B)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.brush')} (B)`}
|
|
||||||
icon={<PiPaintBrushBold />}
|
|
||||||
isChecked={tool === 'brush' && !isStaging}
|
|
||||||
onClick={handleSelectBrushTool}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.eraser')} (E)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.eraser')} (E)`}
|
|
||||||
icon={<PiEraserBold />}
|
|
||||||
isChecked={tool === 'eraser' && !isStaging}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
onClick={handleSelectEraserTool}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.fillBoundingBox')} (Shift+F)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.fillBoundingBox')} (Shift+F)`}
|
|
||||||
icon={<PiPaintBucketBold />}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
onClick={handleFillRect}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.eraseBoundingBox')} (Del/Backspace)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.eraseBoundingBox')} (Del/Backspace)`}
|
|
||||||
icon={<PiXBold />}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
onClick={handleEraseBoundingBox}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.colorPicker')} (C)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.colorPicker')} (C)`}
|
|
||||||
icon={<PiEyedropperBold />}
|
|
||||||
isChecked={tool === 'colorPicker' && !isStaging}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
onClick={handleSelectColorPickerTool}
|
|
||||||
/>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<IconButton
|
|
||||||
aria-label={t('unifiedCanvas.brushOptions')}
|
|
||||||
tooltip={t('unifiedCanvas.brushOptions')}
|
|
||||||
icon={<PiSlidersHorizontalBold />}
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent>
|
|
||||||
<PopoverBody>
|
|
||||||
<Flex minWidth={60} direction="column" gap={4} width="100%">
|
|
||||||
<Flex gap={4} justifyContent="space-between">
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('unifiedCanvas.brushSize')}</FormLabel>
|
|
||||||
<CompositeSlider
|
|
||||||
value={brushSize}
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
onChange={handleChangeBrushSize}
|
|
||||||
marks={marks}
|
|
||||||
defaultValue={50}
|
|
||||||
/>
|
|
||||||
<CompositeNumberInput
|
|
||||||
value={brushSize}
|
|
||||||
min={1}
|
|
||||||
max={500}
|
|
||||||
step={1}
|
|
||||||
onChange={handleChangeBrushSize}
|
|
||||||
defaultValue={50}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</Flex>
|
|
||||||
<Box w="full" pt={2} pb={2}>
|
|
||||||
<IAIColorPicker color={brushColor} onChange={handleChangeBrushColor} withNumberInput />
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
</PopoverBody>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</ButtonGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasToolChooserOptions);
|
|
@ -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<ComboboxOnChange>(
|
|
||||||
(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 (
|
|
||||||
<Flex alignItems="center" gap={2} flexWrap="wrap">
|
|
||||||
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
|
|
||||||
<FormControl isDisabled={isStaging} w="5rem">
|
|
||||||
<Combobox value={layerValue} options={layerOptions} onChange={handleChangeLayer} />
|
|
||||||
</FormControl>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<IAICanvasMaskOptions />
|
|
||||||
<IAICanvasToolChooserOptions />
|
|
||||||
|
|
||||||
<ButtonGroup>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.move')} (V)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.move')} (V)`}
|
|
||||||
icon={<PiHandGrabbingBold />}
|
|
||||||
isChecked={tool === 'move' || isStaging}
|
|
||||||
onClick={handleSelectMoveTool}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
|
||||||
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
|
||||||
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
|
|
||||||
onClick={handleSetShouldShowBoundingBox}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
|
|
||||||
icon={<PiCrosshairSimpleBold />}
|
|
||||||
onClick={handleClickResetCanvasView}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
<ButtonGroup>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
|
||||||
icon={<PiStackBold />}
|
|
||||||
onClick={handleMergeVisible}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
|
||||||
icon={<PiFloppyDiskBold />}
|
|
||||||
onClick={handleSaveToGallery}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
{isClipboardAPIAvailable && (
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
|
||||||
icon={<PiCopyBold />}
|
|
||||||
onClick={handleCopyImageToClipboard}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
|
||||||
icon={<PiDownloadSimpleBold />}
|
|
||||||
onClick={handleDownloadAsImage}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
<ButtonGroup>
|
|
||||||
<IAICanvasUndoButton />
|
|
||||||
<IAICanvasRedoButton />
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
<ButtonGroup>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('common.upload')}`}
|
|
||||||
tooltip={`${t('common.upload')}`}
|
|
||||||
icon={<PiUploadSimpleBold />}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
{...getUploadButtonProps()}
|
|
||||||
/>
|
|
||||||
<input {...getUploadInputProps()} />
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
|
||||||
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
|
||||||
icon={<PiTrashSimpleBold />}
|
|
||||||
onClick={handleResetCanvas}
|
|
||||||
colorScheme="error"
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
<ButtonGroup>
|
|
||||||
<IAICanvasSettingsButtonPopover />
|
|
||||||
</ButtonGroup>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasToolbar);
|
|
@ -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 (
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.undo')} (Ctrl+Z)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.undo')} (Ctrl+Z)`}
|
|
||||||
icon={<PiArrowCounterClockwiseBold />}
|
|
||||||
onClick={handleUndo}
|
|
||||||
isDisabled={!canUndo}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasUndoButton);
|
|
@ -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<MouseEvent>) => {
|
|
||||||
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;
|
|
@ -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<GenerationMode | undefined>();
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
@ -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;
|
|
@ -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<Konva.Stage | null>) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const isStaging = useAppSelector(isStagingSelector);
|
|
||||||
const { commitColorUnderCursor } = useColorPicker();
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
|
||||||
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;
|
|
@ -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<Konva.Stage | null>,
|
|
||||||
didMouseMoveRef: MutableRefObject<boolean>,
|
|
||||||
lastCursorPositionRef: MutableRefObject<Vector2d>
|
|
||||||
) => {
|
|
||||||
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;
|
|
@ -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;
|
|
@ -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<Konva.Stage | null>,
|
|
||||||
didMouseMoveRef: MutableRefObject<boolean>
|
|
||||||
) => {
|
|
||||||
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;
|
|
@ -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<Konva.Stage | null>) => {
|
|
||||||
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<WheelEvent>) => {
|
|
||||||
// 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;
|
|
@ -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;
|
|
@ -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');
|
|
@ -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<Vector2d | null>(null);
|
|
||||||
export const $tool = atom<CanvasTool>('move');
|
|
||||||
export const $toolStash = atom<CanvasTool | null>(null);
|
|
||||||
export const $isDrawing = atom<boolean>(false);
|
|
||||||
export const $isMouseOverBoundingBox = atom<boolean>(false);
|
|
||||||
const $isMoveBoundingBoxKeyHeld = atom<boolean>(false);
|
|
||||||
export const $isMoveStageKeyHeld = atom<boolean>(false);
|
|
||||||
export const $isMovingBoundingBox = atom<boolean>(false);
|
|
||||||
export const $isMovingStage = atom<boolean>(false);
|
|
||||||
export const $isTransformingBoundingBox = atom<boolean>(false);
|
|
||||||
export const $isMouseOverBoundingBoxOutline = atom<boolean>(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<Konva.Layer | null>(null);
|
|
||||||
export const $canvasStage = atom<Konva.Stage | null>(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
|
|
||||||
);
|
|
@ -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<Dimensions>,
|
|
||||||
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<CanvasLayer>) => {
|
|
||||||
state.layer = action.payload;
|
|
||||||
},
|
|
||||||
setMaskColor: (state, action: PayloadAction<RgbaColor>) => {
|
|
||||||
state.maskColor = action.payload;
|
|
||||||
},
|
|
||||||
setBrushColor: (state, action: PayloadAction<RgbaColor>) => {
|
|
||||||
state.brushColor = action.payload;
|
|
||||||
},
|
|
||||||
setBrushSize: (state, action: PayloadAction<number>) => {
|
|
||||||
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<boolean>) => {
|
|
||||||
state.shouldPreserveMaskedArea = action.payload;
|
|
||||||
},
|
|
||||||
setIsMaskEnabled: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.isMaskEnabled = action.payload;
|
|
||||||
state.layer = action.payload ? 'mask' : 'base';
|
|
||||||
},
|
|
||||||
setInitialCanvasImage: {
|
|
||||||
reducer: (state, action: PayloadActionWithOptimalDimension<ImageDTO>) => {
|
|
||||||
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<Vector2d>) => {
|
|
||||||
state.boundingBoxCoordinates = floorCoordinates(action.payload);
|
|
||||||
},
|
|
||||||
setStageCoordinates: (state, action: PayloadAction<Vector2d>) => {
|
|
||||||
state.stageCoordinates = action.payload;
|
|
||||||
},
|
|
||||||
setStageScale: (state, action: PayloadAction<number>) => {
|
|
||||||
state.stageScale = action.payload;
|
|
||||||
},
|
|
||||||
setShouldDarkenOutsideBoundingBox: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldDarkenOutsideBoundingBox = action.payload;
|
|
||||||
},
|
|
||||||
setShouldInvertBrushSizeScrollDirection: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldInvertBrushSizeScrollDirection = action.payload;
|
|
||||||
},
|
|
||||||
clearCanvasHistory: (state) => {
|
|
||||||
state.pastLayerStates = [];
|
|
||||||
state.futureLayerStates = [];
|
|
||||||
},
|
|
||||||
setShouldLockBoundingBox: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldLockBoundingBox = action.payload;
|
|
||||||
},
|
|
||||||
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldShowBoundingBox = action.payload;
|
|
||||||
},
|
|
||||||
canvasBatchIdAdded: (state, action: PayloadAction<string>) => {
|
|
||||||
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<ImageDTO>) => {
|
|
||||||
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<number[]>) => {
|
|
||||||
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<boolean>) => {
|
|
||||||
state.shouldShowGrid = action.payload;
|
|
||||||
},
|
|
||||||
setShouldSnapToGrid: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldSnapToGrid = action.payload;
|
|
||||||
},
|
|
||||||
setShouldAutoSave: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldAutoSave = action.payload;
|
|
||||||
},
|
|
||||||
setShouldShowIntermediates: (state, action: PayloadAction<boolean>) => {
|
|
||||||
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<BoundingBoxScaleMethod>) => {
|
|
||||||
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<Partial<Dimensions>>) => {
|
|
||||||
state.scaledBoundingBoxDimensions = {
|
|
||||||
...state.scaledBoundingBoxDimensions,
|
|
||||||
...action.payload,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
setBoundingBoxDimensions: {
|
|
||||||
reducer: (state, action: PayloadActionWithOptimalDimension<Partial<Dimensions>>) => {
|
|
||||||
setBoundingBoxDimensionsReducer(state, action.payload, action.meta.optimalDimension);
|
|
||||||
},
|
|
||||||
prepare: (payload: Partial<Dimensions>, optimalDimension: number) => ({
|
|
||||||
payload,
|
|
||||||
meta: {
|
|
||||||
optimalDimension,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
setShouldShowStagingImage: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldShowStagingImage = action.payload;
|
|
||||||
},
|
|
||||||
setShouldShowStagingOutline: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldShowStagingOutline = action.payload;
|
|
||||||
},
|
|
||||||
setShouldShowCanvasDebugInfo: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldShowCanvasDebugInfo = action.payload;
|
|
||||||
},
|
|
||||||
setShouldRestrictStrokesToBox: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldRestrictStrokesToBox = action.payload;
|
|
||||||
},
|
|
||||||
setShouldAntialias: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldAntialias = action.payload;
|
|
||||||
},
|
|
||||||
setShouldFitImageSize: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldFitImageSize = action.payload;
|
|
||||||
},
|
|
||||||
setShouldCropToBoundingBoxOnSave: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldCropToBoundingBoxOnSave = action.payload;
|
|
||||||
},
|
|
||||||
setColorPickerColor: (state, action: PayloadAction<RgbaColor>) => {
|
|
||||||
state.colorPickerColor = action.payload;
|
|
||||||
},
|
|
||||||
commitColorPickerColor: (state) => {
|
|
||||||
state.brushColor = {
|
|
||||||
...state.colorPickerColor,
|
|
||||||
a: state.brushColor.a,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
setMergedCanvas: (state, action: PayloadAction<CanvasImage>) => {
|
|
||||||
pushToPrevLayerStates(state);
|
|
||||||
|
|
||||||
state.futureLayerStates = [];
|
|
||||||
|
|
||||||
state.layerState.objects = [action.payload];
|
|
||||||
},
|
|
||||||
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
|
|
||||||
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<CanvasState> = {
|
|
||||||
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;
|
|
||||||
};
|
|
@ -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<typeof zBoundingBoxScaleMethod>;
|
|
||||||
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';
|
|
@ -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';
|
|
@ -1,28 +0,0 @@
|
|||||||
export const blobToDataURL = (blob: Blob): Promise<string> => {
|
|
||||||
return new Promise<string>((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();
|
|
||||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* Gets a Blob from a canvas.
|
|
||||||
*/
|
|
||||||
export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob> =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
canvas.toBlob((blob) => {
|
|
||||||
if (blob) {
|
|
||||||
resolve(blob);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reject('Unable to create Blob');
|
|
||||||
});
|
|
||||||
});
|
|
@ -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})`;
|
|
||||||
};
|
|
@ -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;
|
|
@ -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<Konva.Stage> => {
|
|
||||||
// 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;
|
|
@ -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<ImageData> =>
|
|
||||||
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;
|
|
||||||
});
|
|
@ -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();
|
|
||||||
};
|
|
@ -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;
|
|
@ -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);
|
|
||||||
};
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
@ -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';
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,81 +0,0 @@
|
|||||||
export const getColoredMaskSVG = (color: string) => {
|
|
||||||
return `data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="60px" height="60px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,0)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,2.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,7.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,10)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,12.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,15)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,17.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,20)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,22.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,25)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,27.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,30)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-2.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-7.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-10)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-12.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-15)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-17.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-20)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-22.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-25)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-27.5)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.5,0,0,0.5,0,-30)">
|
|
||||||
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
|
||||||
</g>
|
|
||||||
</svg>`.replaceAll('black', color);
|
|
||||||
};
|
|
@ -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());
|
|
||||||
};
|
|
@ -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;
|
|
@ -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;
|
|
||||||
};
|
|
@ -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<Blob> => {
|
|
||||||
return await canvasToBlob(node.toCanvas(boundingBox));
|
|
||||||
};
|
|
@ -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<ImageData> => {
|
|
||||||
// get a dataURL of the bbox'd region
|
|
||||||
const dataURL = node.toDataURL(boundingBox);
|
|
||||||
|
|
||||||
return await dataURLToImageData(dataURL, boundingBox.width, boundingBox.height);
|
|
||||||
};
|
|
@ -1,5 +0,0 @@
|
|||||||
const roundToHundreth = (val: number): number => {
|
|
||||||
return Math.round(val * 100) / 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default roundToHundreth;
|
|
@ -10,8 +10,8 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { setShouldInvertBrushSizeScrollDirection } from 'features/canvas/store/canvasSlice';
|
import { MaskOpacity } from 'features/controlLayers/components/MaskOpacity';
|
||||||
import { RGGlobalOpacity } from 'features/controlLayers/components/RGGlobalOpacity';
|
import { invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -22,7 +22,7 @@ const ControlLayersSettingsPopover = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll);
|
const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll);
|
||||||
const onChangeInvertScroll = useCallback(
|
const onChangeInvertScroll = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)),
|
(e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)),
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
@ -33,7 +33,7 @@ const ControlLayersSettingsPopover = () => {
|
|||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<PopoverBody>
|
<PopoverBody>
|
||||||
<Flex direction="column" gap={2}>
|
<Flex direction="column" gap={2}>
|
||||||
<RGGlobalOpacity />
|
<MaskOpacity />
|
||||||
<FormControl w="full">
|
<FormControl w="full">
|
||||||
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
|
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
|
||||||
<Checkbox isChecked={invertScroll} onChange={onChangeInvertScroll} />
|
<Checkbox isChecked={invertScroll} onChange={onChangeInvertScroll} />
|
||||||
|
@ -8,12 +8,17 @@ import { memo } from 'react';
|
|||||||
export const HeadsUpDisplay = memo(() => {
|
export const HeadsUpDisplay = memo(() => {
|
||||||
const stageAttrs = useStore($stageAttrs);
|
const stageAttrs = useStore($stageAttrs);
|
||||||
const bbox = useAppSelector((s) => s.canvasV2.bbox);
|
const bbox = useAppSelector((s) => s.canvasV2.bbox);
|
||||||
|
const document = useAppSelector((s) => s.canvasV2.document);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" bg="blackAlpha.400" borderBottomEndRadius="base" p={2} minW={64} gap={2}>
|
<Flex flexDir="column" bg="blackAlpha.400" borderBottomEndRadius="base" p={2} minW={64} gap={2}>
|
||||||
<HUDItem label="Scale" value={round(stageAttrs.scale, 3)} />
|
<HUDItem label="Scale" value={round(stageAttrs.scale, 3)} />
|
||||||
|
<HUDItem label="Document Size" value={`${document.width}×${document.height}`} />
|
||||||
<HUDItem label="Stage Pos" value={`${round(stageAttrs.x, 3)}, ${round(stageAttrs.y, 3)}`} />
|
<HUDItem label="Stage Pos" value={`${round(stageAttrs.x, 3)}, ${round(stageAttrs.y, 3)}`} />
|
||||||
<HUDItem label="Stage Size" value={`${round(stageAttrs.width, 3)}, ${round(stageAttrs.height, 3)}`} />
|
<HUDItem
|
||||||
|
label="Stage Size"
|
||||||
|
value={`${round(stageAttrs.width / stageAttrs.scale, 3)}, ${round(stageAttrs.height / stageAttrs.scale, 3)}`}
|
||||||
|
/>
|
||||||
<HUDItem label="BBox Size" value={`${bbox.width}×${bbox.height}`} />
|
<HUDItem label="BBox Size" value={`${bbox.width}×${bbox.height}`} />
|
||||||
<HUDItem label="BBox Position" value={`${bbox.x}, ${bbox.y}`} />
|
<HUDItem label="BBox Position" value={`${bbox.x}, ${bbox.y}`} />
|
||||||
<HUDItem label="BBox Width % 8" value={round(bbox.width % 8, 3)} />
|
<HUDItem label="BBox Width % 8" value={round(bbox.width % 8, 3)} />
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
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 { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const marks = [0, 25, 50, 75, 100];
|
const marks = [0, 25, 50, 75, 100];
|
||||||
const formatPct = (v: number | string) => `${v} %`;
|
const formatPct = (v: number | string) => `${v} %`;
|
||||||
|
|
||||||
export const RGGlobalOpacity = memo(() => {
|
export const MaskOpacity = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
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(
|
const onChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(rgGlobalOpacityChanged({ opacity: v / 100 }));
|
dispatch(maskOpacityChanged(v / 100));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@ -46,4 +46,4 @@ export const RGGlobalOpacity = memo(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RGGlobalOpacity.displayName = 'RGGlobalOpacity';
|
MaskOpacity.displayName = 'MaskOpacity';
|
@ -1,6 +1,6 @@
|
|||||||
import { useDisclosure } from '@invoke-ai/ui-library';
|
import { useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
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 { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||||
import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader';
|
import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader';
|
||||||
import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings';
|
import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings';
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
|
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import RgbColorPicker from 'common/components/RgbColorPicker';
|
import RgbColorPicker from 'common/components/RgbColorPicker';
|
||||||
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { stopPropagation } from 'common/util/stopPropagation';
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
|
||||||
import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice';
|
import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
|
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
|
@ -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 { useStore } from '@nanostores/react';
|
||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
||||||
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
|
||||||
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
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 { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers';
|
||||||
import {
|
import {
|
||||||
$bbox,
|
$bbox,
|
||||||
@ -70,7 +70,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
|
|||||||
const regions = useAppSelector((s) => s.canvasV2.regions);
|
const regions = useAppSelector((s) => s.canvasV2.regions);
|
||||||
const tool = useAppSelector((s) => s.canvasV2.tool);
|
const tool = useAppSelector((s) => s.canvasV2.tool);
|
||||||
const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier);
|
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 bbox = useAppSelector((s) => s.canvasV2.bbox);
|
||||||
const lastCursorPos = useStore($lastCursorPos);
|
const lastCursorPos = useStore($lastCursorPos);
|
||||||
const lastMouseDownPos = useStore($lastMouseDownPos);
|
const lastMouseDownPos = useStore($lastMouseDownPos);
|
||||||
@ -95,10 +95,10 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
|
|||||||
|
|
||||||
const currentFill = useMemo(() => {
|
const currentFill = useMemo(() => {
|
||||||
if (selectedEntity && selectedEntity.type === 'regional_guidance') {
|
if (selectedEntity && selectedEntity.type === 'regional_guidance') {
|
||||||
return { ...selectedEntity.fill, a: maskFillOpacity };
|
return { ...selectedEntity.fill, a: maskOpacity };
|
||||||
}
|
}
|
||||||
return tool.fill;
|
return tool.fill;
|
||||||
}, [maskFillOpacity, selectedEntity, tool.fill]);
|
}, [maskOpacity, selectedEntity, tool.fill]);
|
||||||
|
|
||||||
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
|
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
|
||||||
const dpr = useDevicePixelRatio({ round: false });
|
const dpr = useDevicePixelRatio({ round: false });
|
||||||
@ -106,7 +106,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
$toolState.set(tool);
|
$toolState.set(tool);
|
||||||
$selectedEntity.set(selectedEntity);
|
$selectedEntity.set(selectedEntity);
|
||||||
$bbox.set(bbox);
|
$bbox.set({ x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height });
|
||||||
$currentFill.set(currentFill);
|
$currentFill.set(currentFill);
|
||||||
}, [selectedEntity, tool, bbox, currentFill]);
|
}, [selectedEntity, tool, bbox, currentFill]);
|
||||||
|
|
||||||
@ -291,6 +291,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
|
|||||||
const resizeObserver = new ResizeObserver(fitStageToContainer);
|
const resizeObserver = new ResizeObserver(fitStageToContainer);
|
||||||
resizeObserver.observe(container);
|
resizeObserver.observe(container);
|
||||||
fitStageToContainer();
|
fitStageToContainer();
|
||||||
|
renderBackgroundLayer(stage);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
@ -352,23 +353,13 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
|
|||||||
layers,
|
layers,
|
||||||
controlAdapters,
|
controlAdapters,
|
||||||
regions,
|
regions,
|
||||||
maskFillOpacity,
|
maskOpacity,
|
||||||
tool.selected,
|
tool.selected,
|
||||||
selectedEntity,
|
selectedEntity,
|
||||||
getImageDTO,
|
getImageDTO,
|
||||||
onPosChanged
|
onPosChanged
|
||||||
);
|
);
|
||||||
}, [
|
}, [controlAdapters, layers, maskOpacity, onPosChanged, regions, renderers, selectedEntity, stage, tool.selected]);
|
||||||
controlAdapters,
|
|
||||||
layers,
|
|
||||||
maskFillOpacity,
|
|
||||||
onPosChanged,
|
|
||||||
regions,
|
|
||||||
renderers,
|
|
||||||
selectedEntity,
|
|
||||||
stage,
|
|
||||||
tool.selected,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// useLayoutEffect(() => {
|
// useLayoutEffect(() => {
|
||||||
// if (asPreview) {
|
// if (asPreview) {
|
||||||
@ -414,15 +405,6 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex position="relative" w="full" h="full">
|
<Flex position="relative" w="full" h="full">
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
w="full"
|
|
||||||
h="full"
|
|
||||||
borderRadius="base"
|
|
||||||
backgroundImage={TRANSPARENCY_CHECKER_PATTERN}
|
|
||||||
backgroundRepeat="repeat"
|
|
||||||
opacity={0.2}
|
|
||||||
/>
|
|
||||||
{!asPreview && <NoEntitiesFallback />}
|
{!asPreview && <NoEntitiesFallback />}
|
||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
@ -433,6 +415,9 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
|
border={1}
|
||||||
|
borderStyle="solid"
|
||||||
|
borderColor="base.700"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
data-testid="control-layers-canvas"
|
data-testid="control-layers-canvas"
|
||||||
/>
|
/>
|
||||||
|
@ -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.
|
* 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.
|
* The minimum brush spacing in pixels.
|
||||||
@ -54,3 +54,13 @@ export const MIN_CANVAS_SCALE = 0.1;
|
|||||||
* Maximum (furthest-zoomed-in) scale
|
* Maximum (furthest-zoomed-in) scale
|
||||||
*/
|
*/
|
||||||
export const MAX_CANVAS_SCALE = 20;
|
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;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom';
|
import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background';
|
||||||
import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants';
|
|
||||||
import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
|
import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
|
||||||
import type {
|
import type {
|
||||||
BrushLineAddedArg,
|
BrushLineAddedArg,
|
||||||
@ -16,6 +15,14 @@ import type Konva from 'konva';
|
|||||||
import type { Vector2d } from 'konva/lib/types';
|
import type { Vector2d } from 'konva/lib/types';
|
||||||
import { clamp } from 'lodash-es';
|
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';
|
import { PREVIEW_TOOL_GROUP_ID } from './naming';
|
||||||
|
|
||||||
type Arg = {
|
type Arg = {
|
||||||
@ -60,6 +67,18 @@ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastC
|
|||||||
return pos;
|
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.
|
* 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
|
* @param layerId The layer to (maybe) add the point to
|
||||||
@ -82,7 +101,13 @@ const maybeAddNextPoint = (
|
|||||||
// Continue the last line
|
// Continue the last line
|
||||||
const lastAddedPoint = getLastAddedPoint();
|
const lastAddedPoint = getLastAddedPoint();
|
||||||
const toolState = getToolState();
|
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) {
|
if (lastAddedPoint) {
|
||||||
// Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number
|
// 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) {
|
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) => {
|
stage.on('wheel', (e) => {
|
||||||
e.evt.preventDefault();
|
e.evt.preventDefault();
|
||||||
|
|
||||||
@ -393,9 +419,11 @@ export const setStageEventHandlers = ({
|
|||||||
stage.scaleY(newScale);
|
stage.scaleY(newScale);
|
||||||
stage.position(newPos);
|
stage.position(newPos);
|
||||||
setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale });
|
setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale });
|
||||||
|
renderBackgroundLayer(stage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//#region dragmove
|
||||||
stage.on('dragmove', () => {
|
stage.on('dragmove', () => {
|
||||||
setStageAttrs({
|
setStageAttrs({
|
||||||
x: stage.x(),
|
x: stage.x(),
|
||||||
@ -404,21 +432,22 @@ export const setStageEventHandlers = ({
|
|||||||
height: stage.height(),
|
height: stage.height(),
|
||||||
scale: stage.scaleX(),
|
scale: stage.scaleX(),
|
||||||
});
|
});
|
||||||
|
renderBackgroundLayer(stage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//#region dragend
|
||||||
stage.on('dragend', () => {
|
stage.on('dragend', () => {
|
||||||
// Stage position should always be an integer, else we get fractional pixels which are blurry
|
// 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({
|
setStageAttrs({
|
||||||
x: stage.x(),
|
x: Math.floor(stage.x()),
|
||||||
y: stage.y(),
|
y: Math.floor(stage.y()),
|
||||||
width: stage.width(),
|
width: stage.width(),
|
||||||
height: stage.height(),
|
height: stage.height(),
|
||||||
scale: stage.scaleX(),
|
scale: stage.scaleX(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//#region key
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.repeat) {
|
if (e.repeat) {
|
||||||
return;
|
return;
|
||||||
|
@ -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<Konva.Layer>('#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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -1,5 +1,4 @@
|
|||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||||
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
|
||||||
import {
|
import {
|
||||||
CA_LAYER_IMAGE_NAME,
|
CA_LAYER_IMAGE_NAME,
|
||||||
LAYER_BBOX_NAME,
|
LAYER_BBOX_NAME,
|
||||||
@ -7,6 +6,7 @@ import {
|
|||||||
RG_LAYER_OBJECT_GROUP_NAME,
|
RG_LAYER_OBJECT_GROUP_NAME,
|
||||||
} from 'features/controlLayers/konva/naming';
|
} from 'features/controlLayers/konva/naming';
|
||||||
import { createBboxRect } from 'features/controlLayers/konva/renderers/objects';
|
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 type { ControlAdapterData, LayerData, RegionalGuidanceData } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
|
@ -50,7 +50,7 @@ const renderLayers = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// We'll need to ensure the tool preview layer is on top of the rest of the layers
|
// 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) {
|
for (const layer of layers) {
|
||||||
renderRasterLayer(stage, layer, tool, zIndex, onPosChanged);
|
renderRasterLayer(stage, layer, tool, zIndex, onPosChanged);
|
||||||
zIndex++;
|
zIndex++;
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import {
|
import {
|
||||||
getLayerBboxId,
|
getLayerBboxId,
|
||||||
getObjectGroupId,
|
getObjectGroupId,
|
||||||
LAYER_BBOX_NAME,
|
LAYER_BBOX_NAME,
|
||||||
PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
||||||
} from 'features/controlLayers/konva/naming';
|
} from 'features/controlLayers/konva/naming';
|
||||||
import type {
|
import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types';
|
||||||
BrushLine,
|
|
||||||
CanvasEntity,
|
|
||||||
EraserLine,
|
|
||||||
ImageObject,
|
|
||||||
RectShape,
|
|
||||||
} from 'features/controlLayers/store/types';
|
|
||||||
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
|
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
|
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple';
|
import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple';
|
||||||
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants';
|
||||||
import {
|
|
||||||
BBOX_SELECTED_STROKE,
|
|
||||||
BRUSH_BORDER_INNER_COLOR,
|
|
||||||
BRUSH_BORDER_OUTER_COLOR,
|
|
||||||
} from 'features/controlLayers/konva/constants';
|
|
||||||
import {
|
import {
|
||||||
PREVIEW_BRUSH_BORDER_INNER_ID,
|
PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||||
PREVIEW_BRUSH_BORDER_OUTER_ID,
|
PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||||
@ -206,9 +202,10 @@ export const getBboxPreviewGroup = (
|
|||||||
height,
|
height,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Here we _could_ go ahead and update the bboxRect's attrs directly with the new transform, and reset its scale to 1.
|
// 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
|
// TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly.
|
||||||
// to set the new attrs.
|
// Gotta be a way to avoid setting it twice...
|
||||||
|
bboxRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 });
|
||||||
|
|
||||||
// Update the bbox in internal state.
|
// Update the bbox in internal state.
|
||||||
onBboxTransformed(bbox);
|
onBboxTransformed(bbox);
|
||||||
@ -281,7 +278,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => {
|
|||||||
if (toolPreviewGroup) {
|
if (toolPreviewGroup) {
|
||||||
return toolPreviewGroup;
|
return toolPreviewGroup;
|
||||||
}
|
}
|
||||||
|
const scale = stage.scaleX();
|
||||||
toolPreviewGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID });
|
toolPreviewGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID });
|
||||||
|
|
||||||
// Create the brush preview group & circles
|
// Create the brush preview group & circles
|
||||||
@ -296,7 +293,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => {
|
|||||||
id: PREVIEW_BRUSH_BORDER_INNER_ID,
|
id: PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||||
listening: false,
|
listening: false,
|
||||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||||
strokeWidth: 1,
|
strokeWidth: 1 / scale,
|
||||||
strokeEnabled: true,
|
strokeEnabled: true,
|
||||||
});
|
});
|
||||||
brushPreviewGroup.add(brushPreviewBorderInner);
|
brushPreviewGroup.add(brushPreviewBorderInner);
|
||||||
@ -304,7 +301,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => {
|
|||||||
id: PREVIEW_BRUSH_BORDER_OUTER_ID,
|
id: PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||||
listening: false,
|
listening: false,
|
||||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||||
strokeWidth: 1,
|
strokeWidth: 1 / scale,
|
||||||
strokeEnabled: true,
|
strokeEnabled: true,
|
||||||
});
|
});
|
||||||
brushPreviewGroup.add(brushPreviewBorderOuter);
|
brushPreviewGroup.add(brushPreviewBorderOuter);
|
||||||
@ -313,8 +310,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => {
|
|||||||
const rectPreview = new Konva.Rect({
|
const rectPreview = new Konva.Rect({
|
||||||
id: PREVIEW_RECT_ID,
|
id: PREVIEW_RECT_ID,
|
||||||
listening: false,
|
listening: false,
|
||||||
stroke: BBOX_SELECTED_STROKE,
|
strokeEnabled: false,
|
||||||
strokeWidth: 1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toolPreviewGroup.add(rectPreview);
|
toolPreviewGroup.add(rectPreview);
|
||||||
|
@ -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 { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants';
|
||||||
import {
|
import {
|
||||||
COMPOSITING_RECT_NAME,
|
COMPOSITING_RECT_NAME,
|
||||||
|
@ -12,11 +12,11 @@ import {
|
|||||||
RG_LAYER_NAME,
|
RG_LAYER_NAME,
|
||||||
RG_LAYER_RECT_SHAPE_NAME,
|
RG_LAYER_RECT_SHAPE_NAME,
|
||||||
} from 'features/controlLayers/konva/naming';
|
} 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 { 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.
|
* 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
|
* @param stage The konva stage
|
||||||
@ -33,9 +33,7 @@ export const getScaledFlooredCursorPosition = (stage: Konva.Stage): Vector2d | n
|
|||||||
y: Math.floor(scaledCursorPosition.y),
|
y: Math.floor(scaledCursorPosition.y),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region snapPosToStage
|
|
||||||
/**
|
/**
|
||||||
* Snaps a position to the edge of the stage if within a threshold of the edge
|
* Snaps a position to the edge of the stage if within a threshold of the edge
|
||||||
* @param pos The position to snap
|
* @param pos The position to snap
|
||||||
@ -62,25 +60,19 @@ export const snapPosToStage = (pos: Vector2d, stage: Konva.Stage, snapPx = 10):
|
|||||||
}
|
}
|
||||||
return snappedPos;
|
return snappedPos;
|
||||||
};
|
};
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region getIsMouseDown
|
|
||||||
/**
|
/**
|
||||||
* Checks if the left mouse button is currently pressed
|
* Checks if the left mouse button is currently pressed
|
||||||
* @param e The konva event
|
* @param e The konva event
|
||||||
*/
|
*/
|
||||||
export const getIsMouseDown = (e: KonvaEventObject<MouseEvent>): boolean => e.evt.buttons === 1;
|
export const getIsMouseDown = (e: KonvaEventObject<MouseEvent>): boolean => e.evt.buttons === 1;
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region getIsFocused
|
|
||||||
/**
|
/**
|
||||||
* Checks if the stage is currently focused
|
* Checks if the stage is currently focused
|
||||||
* @param stage The konva stage
|
* @param stage The konva stage
|
||||||
*/
|
*/
|
||||||
export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().contains(document.activeElement);
|
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
|
* 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.
|
* 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
|
* @returns The object's id property
|
||||||
*/
|
*/
|
||||||
export const mapId = (object: { id: string }): string => object.id;
|
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.
|
* 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.
|
* 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_ERASER_LINE_NAME ||
|
||||||
node.name() === RASTER_LAYER_RECT_SHAPE_NAME ||
|
node.name() === RASTER_LAYER_RECT_SHAPE_NAME ||
|
||||||
node.name() === RASTER_LAYER_IMAGE_NAME;
|
node.name() === RASTER_LAYER_IMAGE_NAME;
|
||||||
//#endregion
|
|
||||||
|
/**
|
||||||
|
* Convert a Blob to a data URL.
|
||||||
|
*/
|
||||||
|
export const blobToDataURL = (blob: Blob): Promise<string> => {
|
||||||
|
return new Promise<string>((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<Blob> => {
|
||||||
|
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<ImageData> => {
|
||||||
|
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<Blob> => {
|
||||||
|
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<ImageData> => {
|
||||||
|
// 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 };
|
||||||
|
};
|
||||||
|
@ -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<Partial<Dimensions>>) => {
|
||||||
|
const { width, height } = action.payload;
|
||||||
|
state.bbox.scaledWidth = width ?? state.bbox.scaledWidth;
|
||||||
|
state.bbox.scaledHeight = height ?? state.bbox.scaledHeight;
|
||||||
|
},
|
||||||
|
bboxScaleMethodChanged: (state, action: PayloadAction<BoundingBoxScaleMethod>) => {
|
||||||
|
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<IRect>) => {
|
||||||
|
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<CanvasV2State>;
|
@ -3,23 +3,30 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
|
import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
|
||||||
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
|
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
|
||||||
import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers';
|
import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers';
|
||||||
import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers';
|
import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers';
|
||||||
import { layersReducers } from 'features/controlLayers/store/layersReducers';
|
import { layersReducers } from 'features/controlLayers/store/layersReducers';
|
||||||
import { paramsReducers } from 'features/controlLayers/store/paramsReducers';
|
import { paramsReducers } from 'features/controlLayers/store/paramsReducers';
|
||||||
import { regionsReducers } from 'features/controlLayers/store/regionsReducers';
|
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 { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
|
||||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import { atom } from 'nanostores';
|
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';
|
import { DEFAULT_RGBA_COLOR } from './types';
|
||||||
|
|
||||||
const initialState: CanvasV2State = {
|
const initialState: CanvasV2State = {
|
||||||
_version: 3,
|
_version: 3,
|
||||||
selectedEntityIdentifier: null,
|
selectedEntityIdentifier: null,
|
||||||
|
layers: [],
|
||||||
|
controlAdapters: [],
|
||||||
|
ipAdapters: [],
|
||||||
|
regions: [],
|
||||||
tool: {
|
tool: {
|
||||||
selected: 'bbox',
|
selected: 'bbox',
|
||||||
selectedBuffer: null,
|
selectedBuffer: null,
|
||||||
@ -42,17 +49,20 @@ const initialState: CanvasV2State = {
|
|||||||
y: 0,
|
y: 0,
|
||||||
width: 512,
|
width: 512,
|
||||||
height: 512,
|
height: 512,
|
||||||
},
|
|
||||||
scaledBbox: {
|
|
||||||
width: 512,
|
|
||||||
height: 512,
|
|
||||||
scaleMethod: 'auto',
|
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: {
|
compositing: {
|
||||||
maskBlur: 16,
|
maskBlur: 16,
|
||||||
maskBlurMethod: 'box',
|
maskBlurMethod: 'box',
|
||||||
@ -105,6 +115,9 @@ export const canvasV2Slice = createSlice({
|
|||||||
...regionsReducers,
|
...regionsReducers,
|
||||||
...paramsReducers,
|
...paramsReducers,
|
||||||
...compositingReducers,
|
...compositingReducers,
|
||||||
|
...settingsReducers,
|
||||||
|
...toolReducers,
|
||||||
|
...bboxReducers,
|
||||||
widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => {
|
widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => {
|
||||||
const { width, updateAspectRatio, clamp } = action.payload;
|
const { width, updateAspectRatio, clamp } = action.payload;
|
||||||
state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width;
|
state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width;
|
||||||
@ -126,30 +139,6 @@ export const canvasV2Slice = createSlice({
|
|||||||
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
|
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
|
||||||
state.document.aspectRatio = action.payload;
|
state.document.aspectRatio = action.payload;
|
||||||
},
|
},
|
||||||
bboxChanged: (state, action: PayloadAction<IRect>) => {
|
|
||||||
state.bbox = action.payload;
|
|
||||||
},
|
|
||||||
brushWidthChanged: (state, action: PayloadAction<number>) => {
|
|
||||||
state.tool.brush.width = Math.round(action.payload);
|
|
||||||
},
|
|
||||||
eraserWidthChanged: (state, action: PayloadAction<number>) => {
|
|
||||||
state.tool.eraser.width = Math.round(action.payload);
|
|
||||||
},
|
|
||||||
fillChanged: (state, action: PayloadAction<RgbaColor>) => {
|
|
||||||
state.tool.fill = action.payload;
|
|
||||||
},
|
|
||||||
invertScrollChanged: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.tool.invertScroll = action.payload;
|
|
||||||
},
|
|
||||||
toolChanged: (state, action: PayloadAction<Tool>) => {
|
|
||||||
state.tool.selected = action.payload;
|
|
||||||
},
|
|
||||||
toolBufferChanged: (state, action: PayloadAction<Tool | null>) => {
|
|
||||||
state.tool.selectedBuffer = action.payload;
|
|
||||||
},
|
|
||||||
maskFillOpacityChanged: (state, action: PayloadAction<number>) => {
|
|
||||||
state.maskFillOpacity = action.payload;
|
|
||||||
},
|
|
||||||
entitySelected: (state, action: PayloadAction<CanvasEntityIdentifier>) => {
|
entitySelected: (state, action: PayloadAction<CanvasEntityIdentifier>) => {
|
||||||
state.selectedEntityIdentifier = action.payload;
|
state.selectedEntityIdentifier = action.payload;
|
||||||
},
|
},
|
||||||
@ -173,9 +162,11 @@ export const {
|
|||||||
invertScrollChanged,
|
invertScrollChanged,
|
||||||
toolChanged,
|
toolChanged,
|
||||||
toolBufferChanged,
|
toolBufferChanged,
|
||||||
maskFillOpacityChanged,
|
maskOpacityChanged,
|
||||||
entitySelected,
|
entitySelected,
|
||||||
allEntitiesDeleted,
|
allEntitiesDeleted,
|
||||||
|
scaledBboxChanged,
|
||||||
|
bboxScaleMethodChanged,
|
||||||
// layers
|
// layers
|
||||||
layerAdded,
|
layerAdded,
|
||||||
layerRecalled,
|
layerRecalled,
|
||||||
@ -238,7 +229,6 @@ export const {
|
|||||||
rgBboxChanged,
|
rgBboxChanged,
|
||||||
rgDeleted,
|
rgDeleted,
|
||||||
rgAllDeleted,
|
rgAllDeleted,
|
||||||
rgGlobalOpacityChanged,
|
|
||||||
rgMovedForwardOne,
|
rgMovedForwardOne,
|
||||||
rgMovedToFront,
|
rgMovedToFront,
|
||||||
rgMovedBackwardOne,
|
rgMovedBackwardOne,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||||
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
|
|
||||||
import type { CanvasV2State } from 'features/controlLayers/store/types';
|
import type { CanvasV2State } from 'features/controlLayers/store/types';
|
||||||
|
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
||||||
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||||
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
||||||
import type {
|
import type {
|
||||||
@ -67,10 +67,10 @@ export const paramsReducers = {
|
|||||||
state.bbox.width = bboxDims.width;
|
state.bbox.width = bboxDims.width;
|
||||||
state.bbox.height = bboxDims.height;
|
state.bbox.height = bboxDims.height;
|
||||||
|
|
||||||
if (state.scaledBbox.scaleMethod === 'auto') {
|
if (state.bbox.scaleMethod === 'auto') {
|
||||||
const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension);
|
const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension);
|
||||||
state.scaledBbox.width = scaledBboxDims.width;
|
state.bbox.scaledWidth = scaledBboxDims.width;
|
||||||
state.scaledBbox.height = scaledBboxDims.height;
|
state.bbox.scaledHeight = scaledBboxDims.height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,10 +120,6 @@ export const regionsReducers = {
|
|||||||
rgAllDeleted: (state) => {
|
rgAllDeleted: (state) => {
|
||||||
state.regions = [];
|
state.regions = [];
|
||||||
},
|
},
|
||||||
rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => {
|
|
||||||
const { opacity } = action.payload;
|
|
||||||
state.maskFillOpacity = opacity;
|
|
||||||
},
|
|
||||||
rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
|
rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
|
||||||
const { id } = action.payload;
|
const { id } = action.payload;
|
||||||
const rg = selectRG(state, id);
|
const rg = selectRG(state, id);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
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';
|
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||||
|
|
||||||
export const selectEntityCount = createSelector(selectCanvasSlice, (canvasV2) => {
|
export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => {
|
||||||
return (
|
return (
|
||||||
canvasV2.regions.length + canvasV2.controlAdapters.length + canvasV2.ipAdapters.length + canvasV2.layers.length
|
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);
|
return getOptimalDimension(canvasV2.params.model);
|
||||||
});
|
});
|
||||||
|
@ -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<number>) => {
|
||||||
|
state.settings.maskOpacity = action.payload;
|
||||||
|
},
|
||||||
|
} satisfies SliceCaseReducers<CanvasV2State>;
|
@ -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<MySlice> = {
|
|
||||||
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<MySlice>) => {
|
|
||||||
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<MySlice['flavour']>) => {
|
|
||||||
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<string>) => {
|
|
||||||
state.something = action.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
@ -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<number>) => {
|
||||||
|
state.tool.brush.width = Math.round(action.payload);
|
||||||
|
},
|
||||||
|
eraserWidthChanged: (state, action: PayloadAction<number>) => {
|
||||||
|
state.tool.eraser.width = Math.round(action.payload);
|
||||||
|
},
|
||||||
|
fillChanged: (state, action: PayloadAction<RgbaColor>) => {
|
||||||
|
state.tool.fill = action.payload;
|
||||||
|
},
|
||||||
|
invertScrollChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.tool.invertScroll = action.payload;
|
||||||
|
},
|
||||||
|
toolChanged: (state, action: PayloadAction<Tool>) => {
|
||||||
|
state.tool.selected = action.payload;
|
||||||
|
},
|
||||||
|
toolBufferChanged: (state, action: PayloadAction<Tool | null>) => {
|
||||||
|
state.tool.selectedBuffer = action.payload;
|
||||||
|
},
|
||||||
|
} satisfies SliceCaseReducers<CanvasV2State>;
|
@ -802,6 +802,10 @@ export type Dimensions = {
|
|||||||
export type CanvasV2State = {
|
export type CanvasV2State = {
|
||||||
_version: 3;
|
_version: 3;
|
||||||
selectedEntityIdentifier: CanvasEntityIdentifier | null;
|
selectedEntityIdentifier: CanvasEntityIdentifier | null;
|
||||||
|
layers: LayerData[];
|
||||||
|
controlAdapters: ControlAdapterData[];
|
||||||
|
ipAdapters: IPAdapterData[];
|
||||||
|
regions: RegionalGuidanceData[];
|
||||||
tool: {
|
tool: {
|
||||||
selected: Tool;
|
selected: Tool;
|
||||||
selectedBuffer: Tool | null;
|
selectedBuffer: Tool | null;
|
||||||
@ -815,17 +819,24 @@ export type CanvasV2State = {
|
|||||||
height: ParameterHeight;
|
height: ParameterHeight;
|
||||||
aspectRatio: AspectRatioState;
|
aspectRatio: AspectRatioState;
|
||||||
};
|
};
|
||||||
bbox: IRect;
|
settings: {
|
||||||
scaledBbox: {
|
imageSmoothing: boolean;
|
||||||
scaleMethod: BoundingBoxScaleMethod;
|
maskOpacity: number;
|
||||||
|
showHUD: boolean;
|
||||||
|
autoSave: boolean;
|
||||||
|
preserveMaskedArea: boolean;
|
||||||
|
cropToBboxOnSave: boolean;
|
||||||
|
clipToBbox: boolean;
|
||||||
|
};
|
||||||
|
bbox: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
width: ParameterWidth;
|
width: ParameterWidth;
|
||||||
height: ParameterHeight;
|
height: ParameterHeight;
|
||||||
|
scaleMethod: BoundingBoxScaleMethod;
|
||||||
|
scaledWidth: ParameterWidth;
|
||||||
|
scaledHeight: ParameterHeight;
|
||||||
};
|
};
|
||||||
layers: LayerData[];
|
|
||||||
controlAdapters: ControlAdapterData[];
|
|
||||||
ipAdapters: IPAdapterData[];
|
|
||||||
regions: RegionalGuidanceData[];
|
|
||||||
maskFillOpacity: number;
|
|
||||||
compositing: {
|
compositing: {
|
||||||
maskBlur: number;
|
maskBlur: number;
|
||||||
maskBlurMethod: ParameterMaskBlurMethod;
|
maskBlurMethod: ParameterMaskBlurMethod;
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
import type { Dimensions } from 'features/canvas/store/canvasTypes';
|
import { CANVAS_GRID_SIZE_FINE } from 'features/controlLayers/konva/constants';
|
||||||
import { CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/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 { width, height } = dimensions;
|
||||||
|
|
||||||
const scaledDimensions = { width, height };
|
const scaledDimensions = { width, height };
|
||||||
@ -30,5 +36,3 @@ const getScaledBoundingBoxDimensions = (dimensions: Dimensions, optimalDimension
|
|||||||
|
|
||||||
return scaledDimensions;
|
return scaledDimensions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getScaledBoundingBoxDimensions;
|
|
@ -1,7 +1,7 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
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 { RG_LAYER_NAME } from 'features/controlLayers/konva/naming';
|
||||||
import { renderers } from 'features/controlLayers/konva/renderers/layers';
|
import { renderers } from 'features/controlLayers/konva/renderers/layers';
|
||||||
import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice';
|
import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
|
@ -2,17 +2,15 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
|||||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { setBoundingBoxScaleMethod } from 'features/canvas/store/canvasSlice';
|
import { bboxScaleMethodChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { isBoundingBoxScaleMethod } from 'features/canvas/store/canvasTypes';
|
import { isBoundingBoxScaleMethod } from 'features/controlLayers/store/types';
|
||||||
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const ParamScaleBeforeProcessing = () => {
|
const ParamScaleBeforeProcessing = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const boundingBoxScaleMethod = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod);
|
const scaleMethod = useAppSelector((s) => s.canvasV2.bbox.scaleMethod);
|
||||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
|
||||||
|
|
||||||
const OPTIONS: ComboboxOption[] = useMemo(
|
const OPTIONS: ComboboxOption[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -28,15 +26,12 @@ const ParamScaleBeforeProcessing = () => {
|
|||||||
if (!isBoundingBoxScaleMethod(v?.value)) {
|
if (!isBoundingBoxScaleMethod(v?.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(setBoundingBoxScaleMethod(v.value, optimalDimension));
|
dispatch(bboxScaleMethodChanged(v.value));
|
||||||
},
|
},
|
||||||
[dispatch, optimalDimension]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(() => OPTIONS.find((o) => o.value === scaleMethod), [scaleMethod, OPTIONS]);
|
||||||
() => OPTIONS.find((o) => o.value === boundingBoxScaleMethod),
|
|
||||||
[boundingBoxScaleMethod, OPTIONS]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
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 { selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -9,8 +9,8 @@ const ParamScaledHeight = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||||
const isManual = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod === 'manual');
|
const isManual = useAppSelector((s) => s.canvasV2.bbox.scaleMethod === 'manual');
|
||||||
const height = useAppSelector((s) => s.canvasV2.scaledBbox.height);
|
const height = useAppSelector((s) => s.canvasV2.bbox.scaledHeight);
|
||||||
const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMin);
|
const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMin);
|
||||||
const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMax);
|
const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMax);
|
||||||
const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.numberInputMin);
|
const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.numberInputMin);
|
||||||
@ -20,7 +20,7 @@ const ParamScaledHeight = () => {
|
|||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(height: number) => {
|
(height: number) => {
|
||||||
dispatch(setScaledBoundingBoxDimensions({ height }));
|
dispatch(scaledBboxChanged({ height }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
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 { selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -9,8 +9,8 @@ const ParamScaledWidth = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||||
const isManual = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod === 'manual');
|
const isManual = useAppSelector((s) => s.canvasV2.bbox.scaleMethod === 'manual');
|
||||||
const width = useAppSelector((s) => s.canvasV2.scaledBbox.width);
|
const width = useAppSelector((s) => s.canvasV2.bbox.scaledWidth);
|
||||||
const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMin);
|
const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMin);
|
||||||
const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMax);
|
const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMax);
|
||||||
const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.numberInputMin);
|
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 fineStep = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.fineStep);
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(width: number) => {
|
(width: number) => {
|
||||||
dispatch(setScaledBoundingBoxDimensions({ width }));
|
dispatch(scaledBboxChanged({ width }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user