feat(ui): clean up canvas selectors

Do not memoize unless absolutely necessary. Minor perf improvement
This commit is contained in:
psychedelicious 2024-01-05 21:18:12 +11:00
parent 2c049a3b94
commit 73481d4aec
14 changed files with 217 additions and 379 deletions

View File

@ -42,57 +42,34 @@ import IAICanvasStatusText from './IAICanvasStatusText';
import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox'; import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox';
import IAICanvasToolPreview from './IAICanvasToolPreview'; import IAICanvasToolPreview from './IAICanvasToolPreview';
const selector = createMemoizedSelector( const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
[selectCanvasSlice, isStagingSelector],
(canvas, isStaging) => {
const {
isMaskEnabled,
stageScale,
shouldShowBoundingBox,
stageDimensions,
stageCoordinates,
tool,
shouldShowIntermediates,
shouldRestrictStrokesToBox,
shouldShowGrid,
shouldAntialias,
} = canvas;
return { return {
isMaskEnabled, stageCoordinates: canvas.stageCoordinates,
shouldShowBoundingBox, stageDimensions: canvas.stageDimensions,
shouldShowGrid,
stageCoordinates,
stageDimensions,
stageScale,
tool,
isStaging,
shouldShowIntermediates,
shouldAntialias,
shouldRestrictStrokesToBox,
}; };
} });
);
const ChakraStage = chakra(Stage, { const ChakraStage = chakra(Stage, {
shouldForwardProp: (prop) => !['sx'].includes(prop), shouldForwardProp: (prop) => !['sx'].includes(prop),
}); });
const IAICanvas = () => { const IAICanvas = () => {
const { const isStaging = useAppSelector(isStagingSelector);
isMaskEnabled, const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
shouldShowBoundingBox, const shouldShowBoundingBox = useAppSelector(
shouldShowGrid, (s) => s.canvas.shouldShowBoundingBox
stageCoordinates, );
stageDimensions, const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid);
stageScale, const stageScale = useAppSelector((s) => s.canvas.stageScale);
tool, const tool = useAppSelector((s) => s.canvas.tool);
isStaging, const shouldShowIntermediates = useAppSelector(
shouldShowIntermediates, (s) => s.canvas.shouldShowIntermediates
shouldAntialias, );
shouldRestrictStrokesToBox, const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias);
} = useAppSelector(selector); const shouldRestrictStrokesToBox = useAppSelector(
useCanvasHotkeys(); (s) => s.canvas.shouldRestrictStrokesToBox
);
const { stageCoordinates, stageDimensions } = useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const stageRef = useRef<Konva.Stage | null>(null); const stageRef = useRef<Konva.Stage | null>(null);
@ -101,6 +78,7 @@ const IAICanvas = () => {
const isMovingStage = useStore($isMovingStage); const isMovingStage = useStore($isMovingStage);
const isTransformingBoundingBox = useStore($isTransformingBoundingBox); const isTransformingBoundingBox = useStore($isTransformingBoundingBox);
const isMouseOverBoundingBox = useStore($isMouseOverBoundingBox); const isMouseOverBoundingBox = useStore($isMouseOverBoundingBox);
useCanvasHotkeys();
const canvasStageRefCallback = useCallback((el: Konva.Stage) => { const canvasStageRefCallback = useCallback((el: Konva.Stage) => {
setCanvasStage(el as Konva.Stage); setCanvasStage(el as Konva.Stage);
stageRef.current = el; stageRef.current = el;

View File

@ -9,29 +9,28 @@ const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
boundingBoxCoordinates, boundingBoxCoordinates,
boundingBoxDimensions, boundingBoxDimensions,
stageDimensions, stageDimensions,
stageScale,
shouldDarkenOutsideBoundingBox,
stageCoordinates, stageCoordinates,
} = canvas; } = canvas;
return { return {
boundingBoxCoordinates, boundingBoxCoordinates,
boundingBoxDimensions, boundingBoxDimensions,
shouldDarkenOutsideBoundingBox,
stageCoordinates, stageCoordinates,
stageDimensions, stageDimensions,
stageScale,
}; };
}); });
const IAICanvasBoundingBoxOverlay = () => { const IAICanvasBoundingBoxOverlay = () => {
const { const {
boundingBoxCoordinates, boundingBoxCoordinates,
boundingBoxDimensions, boundingBoxDimensions,
shouldDarkenOutsideBoundingBox,
stageCoordinates, stageCoordinates,
stageDimensions, stageDimensions,
stageScale,
} = useAppSelector(selector); } = useAppSelector(selector);
const shouldDarkenOutsideBoundingBox = useAppSelector(
(s) => s.canvas.shouldDarkenOutsideBoundingBox
);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
return ( return (
<Group listening={false}> <Group listening={false}>

View File

@ -7,17 +7,19 @@ import { memo, useCallback, useMemo } from 'react';
import { Group, Line as KonvaLine } from 'react-konva'; import { Group, Line as KonvaLine } from 'react-konva';
import { getArbitraryBaseColor } from 'theme/colors'; import { getArbitraryBaseColor } from 'theme/colors';
const selector = createMemoizedSelector([selectCanvasSlice], (canvas) => { const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
const { stageScale, stageCoordinates, stageDimensions } = canvas; return {
return { stageScale, stageCoordinates, stageDimensions }; stageCoordinates: canvas.stageCoordinates,
stageDimensions: canvas.stageDimensions,
};
}); });
const baseGridLineColor = getArbitraryBaseColor(27); const baseGridLineColor = getArbitraryBaseColor(27);
const fineGridLineColor = getArbitraryBaseColor(18); const fineGridLineColor = getArbitraryBaseColor(18);
const IAICanvasGrid = () => { const IAICanvasGrid = () => {
const { stageScale, stageCoordinates, stageDimensions } = const { stageCoordinates, stageDimensions } = useAppSelector(selector);
useAppSelector(selector); const stageScale = useAppSelector((s) => s.canvas.stageScale);
const gridSpacing = useMemo(() => { const gridSpacing = useMemo(() => {
if (stageScale >= 2) { if (stageScale >= 2) {

View File

@ -1,33 +1,28 @@
import { import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
createLruSelector,
createMemoizedSelector,
} from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectSystemSlice } from 'features/system/store/systemSlice'; import { selectSystemSlice } from 'features/system/store/systemSlice';
import { memo, useEffect, useState } from 'react'; import { memo, useEffect, useState } from 'react';
import { Image as KonvaImage } from 'react-konva'; import { Image as KonvaImage } from 'react-konva';
const progressImageSelector = createLruSelector( const progressImageSelector = createMemoizedSelector(
[selectSystemSlice, selectCanvasSlice], [selectSystemSlice, selectCanvasSlice],
(system, canvas) => { (system, canvas) => {
const { denoiseProgress } = system; const { denoiseProgress } = system;
const { batchIds } = canvas; const { batchIds } = canvas;
return denoiseProgress && batchIds.includes(denoiseProgress.batch_id) return {
progressImage:
denoiseProgress && batchIds.includes(denoiseProgress.batch_id)
? denoiseProgress.progress_image ? denoiseProgress.progress_image
: undefined; : undefined,
boundingBox: canvas.layerState.stagingArea.boundingBox,
};
} }
); );
const boundingBoxSelector = createMemoizedSelector(
[selectCanvasSlice],
(canvas) => canvas.layerState.stagingArea.boundingBox
);
const IAICanvasIntermediateImage = () => { const IAICanvasIntermediateImage = () => {
const progressImage = useAppSelector(progressImageSelector); const { progressImage, boundingBox } = useAppSelector(progressImageSelector);
const boundingBox = useAppSelector(boundingBoxSelector);
const [loadedImageElement, setLoadedImageElement] = const [loadedImageElement, setLoadedImageElement] =
useState<HTMLImageElement | null>(null); useState<HTMLImageElement | null>(null);

View File

@ -2,6 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { getColoredMaskSVG } from 'features/canvas/util/getColoredMaskSVG';
import type Konva from 'konva'; import type Konva from 'konva';
import type { RectConfig } from 'konva/lib/shapes/Rect'; import type { RectConfig } from 'konva/lib/shapes/Rect';
import { isNumber } from 'lodash-es'; import { isNumber } from 'lodash-es';
@ -11,107 +12,25 @@ import { Rect } from 'react-konva';
export const canvasMaskCompositerSelector = createMemoizedSelector( export const canvasMaskCompositerSelector = createMemoizedSelector(
selectCanvasSlice, selectCanvasSlice,
(canvas) => { (canvas) => {
const { maskColor, stageCoordinates, stageDimensions, stageScale } = canvas;
return { return {
stageCoordinates, stageCoordinates: canvas.stageCoordinates,
stageDimensions, stageDimensions: canvas.stageDimensions,
stageScale,
maskColorString: rgbaColorToString(maskColor),
}; };
} }
); );
type IAICanvasMaskCompositerProps = RectConfig; type IAICanvasMaskCompositerProps = RectConfig;
const getColoredSVG = (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);
};
const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => { const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
const { ...rest } = props; const { ...rest } = props;
const { maskColorString, stageCoordinates, stageDimensions, stageScale } = const { stageCoordinates, stageDimensions } = useAppSelector(
useAppSelector(canvasMaskCompositerSelector); canvasMaskCompositerSelector
);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
const maskColorString = useAppSelector((s) =>
rgbaColorToString(s.canvas.maskColor)
);
const [fillPatternImage, setFillPatternImage] = const [fillPatternImage, setFillPatternImage] =
useState<HTMLImageElement | null>(null); useState<HTMLImageElement | null>(null);
@ -132,14 +51,14 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
image.onload = () => { image.onload = () => {
setFillPatternImage(image); setFillPatternImage(image);
}; };
image.src = getColoredSVG(maskColorString); image.src = getColoredMaskSVG(maskColorString);
}, [fillPatternImage, maskColorString]); }, [fillPatternImage, maskColorString]);
useEffect(() => { useEffect(() => {
if (!fillPatternImage) { if (!fillPatternImage) {
return; return;
} }
fillPatternImage.src = getColoredSVG(maskColorString); fillPatternImage.src = getColoredMaskSVG(maskColorString);
}, [fillPatternImage, maskColorString]); }, [fillPatternImage, maskColorString]);
useEffect(() => { useEffect(() => {

View File

@ -20,13 +20,6 @@ const canvasBrushPreviewSelector = createMemoizedSelector(
selectCanvasSlice, selectCanvasSlice,
(canvas) => { (canvas) => {
const { const {
brushSize,
colorPickerColor,
maskColor,
brushColor,
tool,
layer,
stageScale,
stageDimensions, stageDimensions,
boundingBoxCoordinates, boundingBoxCoordinates,
boundingBoxDimensions, boundingBoxDimensions,
@ -82,17 +75,6 @@ const canvasBrushPreviewSelector = createMemoizedSelector(
// : undefined; // : undefined;
return { return {
radius: brushSize / 2,
colorPickerOuterRadius: COLOR_PICKER_SIZE / stageScale,
colorPickerInnerRadius:
(COLOR_PICKER_SIZE - COLOR_PICKER_STROKE_RADIUS + 1) / stageScale,
maskColorString: rgbaColorToString({ ...maskColor, a: 0.5 }),
brushColorString: rgbaColorToString(brushColor),
colorPickerColorString: rgbaColorToString(colorPickerColor),
tool,
layer,
strokeWidth: 1.5 / stageScale,
dotRadius: 1.5 / stageScale,
clip, clip,
stageDimensions, stageDimensions,
}; };
@ -103,20 +85,28 @@ const canvasBrushPreviewSelector = createMemoizedSelector(
* Draws a black circle around the canvas brush preview. * Draws a black circle around the canvas brush preview.
*/ */
const IAICanvasToolPreview = (props: GroupConfig) => { const IAICanvasToolPreview = (props: GroupConfig) => {
const { const radius = useAppSelector((s) => s.canvas.brushSize / 2);
radius, const maskColorString = useAppSelector((s) =>
maskColorString, rgbaColorToString({ ...s.canvas.maskColor, a: 0.5 })
tool, );
layer, const tool = useAppSelector((s) => s.canvas.tool);
dotRadius, const layer = useAppSelector((s) => s.canvas.layer);
strokeWidth, const dotRadius = useAppSelector((s) => 1.5 / s.canvas.stageScale);
brushColorString, const strokeWidth = useAppSelector((s) => 1.5 / s.canvas.stageScale);
colorPickerColorString, const brushColorString = useAppSelector((s) =>
colorPickerInnerRadius, rgbaColorToString(s.canvas.brushColor)
colorPickerOuterRadius, );
clip, const colorPickerColorString = useAppSelector((s) =>
stageDimensions, rgbaColorToString(s.canvas.colorPickerColor)
} = useAppSelector(canvasBrushPreviewSelector); );
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 cursorPosition = useStore($cursorPosition);
const isMovingBoundingBox = useStore($isMovingBoundingBox); const isMovingBoundingBox = useStore($isMovingBoundingBox);

View File

@ -1,5 +1,4 @@
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { $shift } from 'common/hooks/useGlobalModifiers'; import { $shift } from 'common/hooks/useGlobalModifiers';
import { import {
@ -17,7 +16,6 @@ import {
} from 'features/canvas/store/canvasNanostore'; } from 'features/canvas/store/canvasNanostore';
import { import {
aspectRatioChanged, aspectRatioChanged,
selectCanvasSlice,
setBoundingBoxCoordinates, setBoundingBoxCoordinates,
setBoundingBoxDimensions, setBoundingBoxDimensions,
setShouldSnapToGrid, setShouldSnapToGrid,
@ -38,47 +36,23 @@ import { Group, Rect, Transformer } from 'react-konva';
const borderDash = [4, 4]; const borderDash = [4, 4];
const boundingBoxPreviewSelector = createMemoizedSelector(
[selectCanvasSlice, selectOptimalDimension],
(canvas, optimalDimension) => {
const {
boundingBoxCoordinates,
boundingBoxDimensions,
stageScale,
tool,
shouldSnapToGrid,
aspectRatio,
} = canvas;
return {
boundingBoxCoordinates,
boundingBoxDimensions,
stageScale,
shouldSnapToGrid,
tool,
hitStrokeWidth: 20 / stageScale,
aspectRatio,
optimalDimension,
};
}
);
type IAICanvasBoundingBoxPreviewProps = GroupConfig; type IAICanvasBoundingBoxPreviewProps = GroupConfig;
const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
const { ...rest } = props; const { ...rest } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { const boundingBoxCoordinates = useAppSelector(
boundingBoxCoordinates, (s) => s.canvas.boundingBoxCoordinates
boundingBoxDimensions, );
stageScale, const boundingBoxDimensions = useAppSelector(
shouldSnapToGrid, (s) => s.canvas.boundingBoxDimensions
tool, );
hitStrokeWidth, const stageScale = useAppSelector((s) => s.canvas.stageScale);
aspectRatio, const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid);
optimalDimension, const tool = useAppSelector((s) => s.canvas.tool);
} = useAppSelector(boundingBoxPreviewSelector); 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 transformerRef = useRef<Konva.Transformer>(null);
const shapeRef = useRef<Konva.Rect>(null); const shapeRef = useRef<Konva.Rect>(null);
const shift = useStore($shift); const shift = useStore($shift);

View File

@ -1,5 +1,4 @@
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker'; import IAIColorPicker from 'common/components/IAIColorPicker';
import { InvButton } from 'common/components/InvButton/InvButton'; import { InvButton } from 'common/components/InvButton/InvButton';
@ -16,13 +15,11 @@ import { canvasMaskSavedToGallery } from 'features/canvas/store/actions';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { import {
clearMask, clearMask,
selectCanvasSlice,
setIsMaskEnabled, setIsMaskEnabled,
setLayer, setLayer,
setMaskColor, setMaskColor,
setShouldPreserveMaskedArea, setShouldPreserveMaskedArea,
} from 'features/canvas/store/canvasSlice'; } from 'features/canvas/store/canvasSlice';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import type { RgbaColor } from 'react-colorful'; import type { RgbaColor } from 'react-colorful';
@ -30,33 +27,16 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaMask, FaSave, FaTrash } from 'react-icons/fa'; import { FaMask, FaSave, FaTrash } from 'react-icons/fa';
export const selector = createMemoizedSelector(
[selectCanvasSlice, isStagingSelector],
(canvas, isStaging) => {
const { maskColor, layer, isMaskEnabled, shouldPreserveMaskedArea } =
canvas;
return {
layer,
maskColor,
maskColorString: rgbaColorToString(maskColor),
isMaskEnabled,
shouldPreserveMaskedArea,
isStaging,
};
}
);
const IAICanvasMaskOptions = () => { const IAICanvasMaskOptions = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const layer = useAppSelector((s) => s.canvas.layer);
const { const maskColor = useAppSelector((s) => s.canvas.maskColor);
layer, const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
maskColor, const shouldPreserveMaskedArea = useAppSelector(
isMaskEnabled, (s) => s.canvas.shouldPreserveMaskedArea
shouldPreserveMaskedArea, );
isStaging, const isStaging = useAppSelector(isStagingSelector);
} = useAppSelector(selector);
useHotkeys( useHotkeys(
['q'], ['q'],

View File

@ -1,5 +1,4 @@
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvCheckbox } from 'common/components/InvCheckbox/wrapper'; import { InvCheckbox } from 'common/components/InvCheckbox/wrapper';
import { InvControl } from 'common/components/InvControl/InvControl'; import { InvControl } from 'common/components/InvControl/InvControl';
@ -10,7 +9,6 @@ import {
} from 'common/components/InvPopover/wrapper'; } from 'common/components/InvPopover/wrapper';
import ClearCanvasHistoryButtonModal from 'features/canvas/components/ClearCanvasHistoryButtonModal'; import ClearCanvasHistoryButtonModal from 'features/canvas/components/ClearCanvasHistoryButtonModal';
import { import {
selectCanvasSlice,
setShouldAntialias, setShouldAntialias,
setShouldAutoSave, setShouldAutoSave,
setShouldCropToBoundingBoxOnSave, setShouldCropToBoundingBoxOnSave,
@ -28,50 +26,28 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaWrench } from 'react-icons/fa'; import { FaWrench } from 'react-icons/fa';
export const canvasControlsSelector = createMemoizedSelector(
[selectCanvasSlice],
(canvas) => {
const {
shouldAutoSave,
shouldCropToBoundingBoxOnSave,
shouldDarkenOutsideBoundingBox,
shouldShowCanvasDebugInfo,
shouldShowGrid,
shouldShowIntermediates,
shouldSnapToGrid,
shouldRestrictStrokesToBox,
shouldAntialias,
} = canvas;
return {
shouldAutoSave,
shouldCropToBoundingBoxOnSave,
shouldDarkenOutsideBoundingBox,
shouldShowCanvasDebugInfo,
shouldShowGrid,
shouldShowIntermediates,
shouldSnapToGrid,
shouldRestrictStrokesToBox,
shouldAntialias,
};
}
);
const IAICanvasSettingsButtonPopover = () => { const IAICanvasSettingsButtonPopover = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const shouldAutoSave = useAppSelector((s) => s.canvas.shouldAutoSave);
const { const shouldCropToBoundingBoxOnSave = useAppSelector(
shouldAutoSave, (s) => s.canvas.shouldCropToBoundingBoxOnSave
shouldCropToBoundingBoxOnSave, );
shouldDarkenOutsideBoundingBox, const shouldDarkenOutsideBoundingBox = useAppSelector(
shouldShowCanvasDebugInfo, (s) => s.canvas.shouldDarkenOutsideBoundingBox
shouldShowGrid, );
shouldShowIntermediates, const shouldShowCanvasDebugInfo = useAppSelector(
shouldSnapToGrid, (s) => s.canvas.shouldShowCanvasDebugInfo
shouldRestrictStrokesToBox, );
shouldAntialias, const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid);
} = useAppSelector(canvasControlsSelector); 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);
useHotkeys( useHotkeys(
['n'], ['n'],

View File

@ -1,5 +1,4 @@
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup'; import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
import { InvControl } from 'common/components/InvControl/InvControl'; import { InvControl } from 'common/components/InvControl/InvControl';
@ -19,7 +18,6 @@ import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { import {
resetCanvas, resetCanvas,
resetCanvasView, resetCanvasView,
selectCanvasSlice,
setIsMaskEnabled, setIsMaskEnabled,
setLayer, setLayer,
setTool, setTool,
@ -48,27 +46,13 @@ import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions'; import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
import IAICanvasUndoButton from './IAICanvasUndoButton'; import IAICanvasUndoButton from './IAICanvasUndoButton';
export const selector = createMemoizedSelector(
[selectCanvasSlice, isStagingSelector],
(canvas, isStaging) => {
const { tool, shouldCropToBoundingBoxOnSave, layer, isMaskEnabled } =
canvas;
return {
isStaging,
isMaskEnabled,
tool,
layer,
shouldCropToBoundingBoxOnSave,
};
}
);
const IAICanvasToolbar = () => { const IAICanvasToolbar = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isStaging, isMaskEnabled, layer, tool } = useAppSelector(selector); const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
const layer = useAppSelector((s) => s.canvas.layer);
const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
const canvasBaseLayer = getCanvasBaseLayer(); const canvasBaseLayer = getCanvasBaseLayer();
const { t } = useTranslation(); const { t } = useTranslation();
const { isClipboardAPIAvailable } = useCopyImageToClipboard(); const { isClipboardAPIAvailable } = useCopyImageToClipboard();

View File

@ -1,4 +1,3 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { import {
resetCanvasInteractionState, resetCanvasInteractionState,
@ -7,7 +6,6 @@ import {
import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { import {
clearMask, clearMask,
selectCanvasSlice,
setIsMaskEnabled, setIsMaskEnabled,
setShouldShowBoundingBox, setShouldShowBoundingBox,
setShouldSnapToGrid, setShouldSnapToGrid,
@ -16,49 +14,24 @@ import {
import type { CanvasTool } from 'features/canvas/store/canvasTypes'; import type { CanvasTool } from 'features/canvas/store/canvasTypes';
import { getCanvasStage } from 'features/canvas/util/konvaInstanceProvider'; import { getCanvasStage } from 'features/canvas/util/konvaInstanceProvider';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useRef } from 'react'; import { useCallback, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
const selector = createMemoizedSelector(
[selectCanvasSlice, activeTabNameSelector, isStagingSelector],
(canvas, activeTabName, isStaging) => {
const {
shouldLockBoundingBox,
shouldShowBoundingBox,
tool,
isMaskEnabled,
shouldSnapToGrid,
} = canvas;
return {
activeTabName,
shouldLockBoundingBox,
shouldShowBoundingBox,
tool,
isStaging,
isMaskEnabled,
shouldSnapToGrid,
};
}
);
const useInpaintingCanvasHotkeys = () => { const useInpaintingCanvasHotkeys = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { const activeTabName = useAppSelector(activeTabNameSelector);
activeTabName, const shouldShowBoundingBox = useAppSelector(
shouldShowBoundingBox, (s) => s.canvas.shouldShowBoundingBox
tool, );
isStaging, const tool = useAppSelector((s) => s.canvas.tool);
isMaskEnabled, const isStaging = useAppSelector(isStagingSelector);
shouldSnapToGrid, const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
} = useAppSelector(selector); const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid);
const previousToolRef = useRef<CanvasTool | null>(null); const previousToolRef = useRef<CanvasTool | null>(null);
const canvasStage = getCanvasStage(); const canvasStage = getCanvasStage();
// Beta Keys // Beta Keys
const handleClearMask = () => dispatch(clearMask()); const handleClearMask = useCallback(() => dispatch(clearMask()), [dispatch]);
useHotkeys( useHotkeys(
['shift+c'], ['shift+c'],

View File

@ -1,5 +1,4 @@
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { import {
$isDrawing, $isDrawing,
@ -8,10 +7,8 @@ import {
import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { import {
addPointToCurrentLine, addPointToCurrentLine,
selectCanvasSlice,
} from 'features/canvas/store/canvasSlice'; } from 'features/canvas/store/canvasSlice';
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import type Konva from 'konva'; import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types'; import type { Vector2d } from 'konva/lib/types';
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
@ -19,17 +16,6 @@ import { useCallback } from 'react';
import useColorPicker from './useColorUnderCursor'; import useColorPicker from './useColorUnderCursor';
const selector = createMemoizedSelector(
[activeTabNameSelector, selectCanvasSlice, isStagingSelector],
(activeTabName, canvas, isStaging) => {
return {
tool: canvas.tool,
activeTabName,
isStaging,
};
}
);
const useCanvasMouseMove = ( const useCanvasMouseMove = (
stageRef: MutableRefObject<Konva.Stage | null>, stageRef: MutableRefObject<Konva.Stage | null>,
didMouseMoveRef: MutableRefObject<boolean>, didMouseMoveRef: MutableRefObject<boolean>,
@ -37,7 +23,8 @@ const useCanvasMouseMove = (
) => { ) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isDrawing = useStore($isDrawing); const isDrawing = useStore($isDrawing);
const { tool, isStaging } = useAppSelector(selector); const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
const { updateColorUnderCursor } = useColorPicker(); const { updateColorUnderCursor } = useColorPicker();
return useCallback(() => { return useCallback(() => {

View File

@ -0,0 +1,81 @@
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);
};

View File

@ -1,11 +1,11 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createSelector } from '@reduxjs/toolkit';
import { selectConfigSlice } from 'features/system/store/configSlice'; import { selectConfigSlice } from 'features/system/store/configSlice';
import { selectUiSlice } from 'features/ui/store/uiSlice'; import { selectUiSlice } from 'features/ui/store/uiSlice';
import { isString } from 'lodash-es'; import { isString } from 'lodash-es';
import { tabMap } from './tabMap'; import { tabMap } from './tabMap';
export const activeTabNameSelector = createMemoizedSelector( export const activeTabNameSelector = createSelector(
selectUiSlice, selectUiSlice,
/** /**
* Previously `activeTab` was an integer, but now it's a string. * Previously `activeTab` was an integer, but now it's a string.
@ -14,7 +14,7 @@ export const activeTabNameSelector = createMemoizedSelector(
(ui) => (isString(ui.activeTab) ? ui.activeTab : 'txt2img') (ui) => (isString(ui.activeTab) ? ui.activeTab : 'txt2img')
); );
export const activeTabIndexSelector = createMemoizedSelector( export const activeTabIndexSelector = createSelector(
selectUiSlice, selectUiSlice,
selectConfigSlice, selectConfigSlice,
(ui, config) => { (ui, config) => {