perf(ui): memoize & otherwise optimize regional prompts ui

This commit is contained in:
psychedelicious 2024-04-12 20:14:33 +10:00 committed by Kent Keirsey
parent 944fa1a847
commit d1db6198b5
15 changed files with 144 additions and 111 deletions

View File

@ -1,10 +1,10 @@
import { Button } from '@invoke-ai/ui-library'; import { Button } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { layerAdded } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { layerAdded } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const AddLayerButton = () => { export const AddLayerButton = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const onClick = useCallback(() => { const onClick = useCallback(() => {
@ -12,4 +12,6 @@ export const AddLayerButton = () => {
}, [dispatch]); }, [dispatch]);
return <Button onClick={onClick}>{t('regionalPrompts.addLayer')}</Button>; return <Button onClick={onClick}>{t('regionalPrompts.addLayer')}</Button>;
}; });
AddLayerButton.displayName = 'AddLayerButton';

View File

@ -1,49 +1,38 @@
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { rgbColorToString } from 'features/canvas/util/colorToString'; import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { $cursorPosition } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { $cursorPosition } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo } from 'react';
import { Circle, Group } from 'react-konva'; import { Circle, Group } from 'react-konva';
const useBrushData = () => { export const BrushPreviewOutline = memo(() => {
const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize); const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize);
const tool = useAppSelector((s) => s.regionalPrompts.tool); const tool = useAppSelector((s) => s.regionalPrompts.tool);
const a = useAppSelector((s) => s.regionalPrompts.promptLayerOpacity);
const color = useAppSelector((s) => { const color = useAppSelector((s) => {
const _color = s.regionalPrompts.layers.find((l) => l.id === s.regionalPrompts.selectedLayer)?.color; const _color = s.regionalPrompts.layers.find((l) => l.id === s.regionalPrompts.selectedLayer)?.color;
if (!_color) { if (!_color) {
return null; return null;
} }
return rgbColorToString(_color); return rgbaColorToString({ ..._color, a });
}); });
const pos = useStore($cursorPosition); const pos = useStore($cursorPosition);
return { brushSize, tool, color, pos };
};
export const BrushPreviewFill = () => {
const { brushSize, tool, color, pos } = useBrushData();
if (!brushSize || !color || !pos || tool === 'move') { if (!brushSize || !color || !pos || tool === 'move') {
return null; return null;
} }
return ( return (
<Circle <Group listening={false}>
x={pos.x} <Circle
y={pos.y} x={pos.x}
radius={brushSize / 2} y={pos.y}
fill={color} radius={brushSize / 2}
globalCompositeOperation={tool === 'brush' ? 'source-over' : 'destination-out'} fill={color}
/> globalCompositeOperation={tool === 'brush' ? 'source-over' : 'destination-out'}
); listening={false}
}; />
export const BrushPreviewOutline = () => {
const { brushSize, tool, color, pos } = useBrushData();
if (!brushSize || !color || !pos || tool === 'move') {
return null;
}
return (
<Group>
<Circle <Circle
x={pos.x} x={pos.x}
y={pos.y} y={pos.y}
@ -64,4 +53,6 @@ export const BrushPreviewOutline = () => {
/> />
</Group> </Group>
); );
}; });
BrushPreviewOutline.displayName = 'BrushPreviewOutline';

View File

@ -1,10 +1,10 @@
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 { brushSizeChanged } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { brushSizeChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const BrushSize = () => { export const BrushSize = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize); const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize);
@ -21,4 +21,6 @@ export const BrushSize = () => {
<CompositeNumberInput min={1} max={500} value={brushSize} onChange={onChange} /> <CompositeNumberInput min={1} max={500} value={brushSize} onChange={onChange} />
</FormControl> </FormControl>
); );
}; });
BrushSize.displayName = 'BrushSize';

View File

@ -1,14 +1,14 @@
import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { layerSelected, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { layerSelected, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { Rect as KonvaRect } from 'react-konva'; import { Rect as KonvaRect } from 'react-konva';
type Props = { type Props = {
layerId: string; layerId: string;
}; };
export const LayerBoundingBox = ({ layerId }: Props) => { export const LayerBoundingBox = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const tool = useAppSelector((s) => s.regionalPrompts.tool); const tool = useAppSelector((s) => s.regionalPrompts.tool);
const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer); const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer);
@ -19,7 +19,7 @@ export const LayerBoundingBox = ({ layerId }: Props) => {
const selectBbox = useMemo( const selectBbox = useMemo(
() => () =>
createSelector( createMemoizedSelector(
selectRegionalPromptsSlice, selectRegionalPromptsSlice,
(regionalPrompts) => regionalPrompts.layers.find((layer) => layer.id === layerId)?.bbox ?? null (regionalPrompts) => regionalPrompts.layers.find((layer) => layer.id === layerId)?.bbox ?? null
), ),
@ -33,6 +33,7 @@ export const LayerBoundingBox = ({ layerId }: Props) => {
return ( return (
<KonvaRect <KonvaRect
name="layer bbox"
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
stroke={selectedLayer === layerId ? 'rgba(153, 187, 189, 1)' : 'rgba(255, 255, 255, 0.149)'} stroke={selectedLayer === layerId ? 'rgba(153, 187, 189, 1)' : 'rgba(255, 255, 255, 0.149)'}
strokeWidth={1} strokeWidth={1}
@ -43,4 +44,6 @@ export const LayerBoundingBox = ({ layerId }: Props) => {
listening={tool === 'move'} listening={tool === 'move'}
/> />
); );
}; });
LayerBoundingBox.displayName = 'LayerBoundingBox';

View File

@ -1,18 +1,31 @@
import { IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; import { IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker'; import RgbColorPicker from 'common/components/RgbColorPicker';
import { useLayer } from 'features/regionalPrompts/hooks/layerStateHooks'; import {
import { promptRegionLayerColorChanged } from 'features/regionalPrompts/store/regionalPromptsSlice'; promptRegionLayerColorChanged,
import { useCallback } from 'react'; selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useMemo } from 'react';
import type { RgbColor } from 'react-colorful'; import type { RgbColor } from 'react-colorful';
import { PiEyedropperBold } from 'react-icons/pi'; import { PiEyedropperBold } from 'react-icons/pi';
import { assert } from 'tsafe';
type Props = { type Props = {
id: string; id: string;
}; };
export const LayerColorPicker = ({ id }: Props) => { export const LayerColorPicker = memo(({ id }: Props) => {
const layer = useLayer(id); const selectColor = useMemo(
() =>
createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
const layer = regionalPrompts.layers.find((l) => l.id === id);
assert(layer);
return layer.color;
}),
[id]
);
const color = useAppSelector(selectColor);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const onColorChange = useCallback( const onColorChange = useCallback(
(color: RgbColor) => { (color: RgbColor) => {
@ -27,9 +40,11 @@ export const LayerColorPicker = ({ id }: Props) => {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent> <PopoverContent>
<PopoverBody minH={64}> <PopoverBody minH={64}>
<RgbColorPicker color={layer.color} onChange={onColorChange} withNumberInput /> <RgbColorPicker color={color} onChange={onColorChange} withNumberInput />
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
); );
}; });
LayerColorPicker.displayName = 'LayerColorPicker';

View File

@ -1,6 +1,5 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import { BrushPreviewFill } from 'features/regionalPrompts/components/BrushPreview';
import { LayerBoundingBox } from 'features/regionalPrompts/components/LayerBoundingBox'; import { LayerBoundingBox } from 'features/regionalPrompts/components/LayerBoundingBox';
import { LineComponent } from 'features/regionalPrompts/components/LineComponent'; import { LineComponent } from 'features/regionalPrompts/components/LineComponent';
import { RectComponent } from 'features/regionalPrompts/components/RectComponent'; import { RectComponent } from 'features/regionalPrompts/components/RectComponent';
@ -17,8 +16,7 @@ import type { Group as KonvaGroupType } from 'konva/lib/Group';
import type { Layer as KonvaLayerType } from 'konva/lib/Layer'; import type { Layer as KonvaLayerType } from 'konva/lib/Layer';
import type { KonvaEventObject, Node as KonvaNodeType, NodeConfig as KonvaNodeConfigType } from 'konva/lib/Node'; import type { KonvaEventObject, Node as KonvaNodeType, NodeConfig as KonvaNodeConfigType } from 'konva/lib/Node';
import type { IRect, Vector2d } from 'konva/lib/types'; import type { IRect, Vector2d } from 'konva/lib/types';
import type React from 'react'; import { memo, useCallback, useEffect, useRef } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { Group as KonvaGroup, Layer as KonvaLayer } from 'react-konva'; import { Group as KonvaGroup, Layer as KonvaLayer } from 'react-konva';
type Props = { type Props = {
@ -28,15 +26,13 @@ type Props = {
export const selectPromptLayerObjectGroup = (item: KonvaNodeType<KonvaNodeConfigType>) => export const selectPromptLayerObjectGroup = (item: KonvaNodeType<KonvaNodeConfigType>) =>
item.name() !== REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME; item.name() !== REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME;
export const LayerComponent: React.FC<Props> = ({ id }) => { export const LayerComponent = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const layer = useLayer(id); const layer = useLayer(id);
const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer);
const promptLayerOpacity = useAppSelector((s) => s.regionalPrompts.promptLayerOpacity); const promptLayerOpacity = useAppSelector((s) => s.regionalPrompts.promptLayerOpacity);
const tool = useAppSelector((s) => s.regionalPrompts.tool); const tool = useAppSelector((s) => s.regionalPrompts.tool);
const layerRef = useRef<KonvaLayerType>(null); const layerRef = useRef<KonvaLayerType>(null);
const groupRef = useRef<KonvaGroupType>(null); const groupRef = useRef<KonvaGroupType>(null);
const onChangeBbox = useCallback( const onChangeBbox = useCallback(
(bbox: IRect | null) => { (bbox: IRect | null) => {
dispatch(layerBboxChanged({ layerId: layer.id, bbox })); dispatch(layerBboxChanged({ layerId: layer.id, bbox }));
@ -94,39 +90,39 @@ export const LayerComponent: React.FC<Props> = ({ id }) => {
} }
// Caching the group allows its opacity to apply to all shapes at once. We should cache only when the layer's // Caching the group allows its opacity to apply to all shapes at once. We should cache only when the layer's
// objects or attributes with a visual effect (e.g. color) change. // objects or attributes with a visual effect (e.g. color) change.
// TODO: Figure out a more efficient way to handle opacity - maybe a separate rect w/ globalCompositeOperation...
groupRef.current.cache(); groupRef.current.cache();
}, [layer.objects, layer.color, layer.isVisible]); }, [layer.objects, layer.color, layer.isVisible]);
return ( return (
<> <KonvaLayer
<KonvaLayer ref={layerRef}
ref={layerRef} id={layer.id}
id={layer.id} name={REGIONAL_PROMPT_LAYER_NAME}
name={REGIONAL_PROMPT_LAYER_NAME} onDragMove={onDragMove}
onDragMove={onDragMove} dragBoundFunc={dragBoundFunc}
dragBoundFunc={dragBoundFunc} visible={layer.isVisible}
visible={layer.isVisible} draggable
draggable >
<KonvaGroup
id={`layer-${layer.id}-group`}
name={REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}
ref={groupRef}
listening={false}
opacity={promptLayerOpacity}
> >
<KonvaGroup {layer.objects.map((obj) => {
id={`layer-${layer.id}-group`} if (obj.kind === 'line') {
name={REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME} return <LineComponent key={obj.id} line={obj} color={layer.color} layerId={layer.id} />;
ref={groupRef} }
listening={false} if (obj.kind === 'fillRect') {
opacity={promptLayerOpacity} return <RectComponent key={obj.id} rect={obj} color={layer.color} />;
> }
{layer.objects.map((obj) => { })}
if (obj.kind === 'line') { </KonvaGroup>
return <LineComponent key={obj.id} line={obj} color={layer.color} layerId={layer.id} />; <LayerBoundingBox layerId={layer.id} />
} </KonvaLayer>
if (obj.kind === 'fillRect') {
return <RectComponent key={obj.id} rect={obj} color={layer.color} />;
}
})}
</KonvaGroup>
<LayerBoundingBox layerId={layer.id} />
</KonvaLayer>
<KonvaLayer name="brushPreviewFill">{layer.id === selectedLayer && <BrushPreviewFill />}</KonvaLayer>
</>
); );
}; });
LayerComponent.displayName = 'LayerComponent';

View File

@ -6,14 +6,14 @@ import { LayerMenu } from 'features/regionalPrompts/components/LayerMenu';
import { LayerVisibilityToggle } from 'features/regionalPrompts/components/LayerVisibilityToggle'; import { LayerVisibilityToggle } from 'features/regionalPrompts/components/LayerVisibilityToggle';
import { RegionalPromptsPrompt } from 'features/regionalPrompts/components/RegionalPromptsPrompt'; import { RegionalPromptsPrompt } from 'features/regionalPrompts/components/RegionalPromptsPrompt';
import { layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
type Props = { type Props = {
id: string; id: string;
}; };
export const LayerListItem = ({ id }: Props) => { export const LayerListItem = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer); const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer);
@ -46,4 +46,6 @@ export const LayerListItem = ({ id }: Props) => {
</Flex> </Flex>
</Flex> </Flex>
); );
}; });
LayerListItem.displayName = 'LayerListItem';

View File

@ -1,5 +1,5 @@
import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { import {
layerDeleted, layerDeleted,
@ -10,8 +10,7 @@ import {
layerReset, layerReset,
selectRegionalPromptsSlice, selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice'; } from 'features/regionalPrompts/store/regionalPromptsSlice';
import type React from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
PiArrowCounterClockwiseBold, PiArrowCounterClockwiseBold,
@ -25,12 +24,12 @@ import {
type Props = { id: string }; type Props = { id: string };
export const LayerMenu: React.FC<Props> = ({ id }) => { export const LayerMenu = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const selectValidActions = useMemo( const selectValidActions = useMemo(
() => () =>
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => { createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
const layerIndex = regionalPrompts.layers.findIndex((l) => l.id === id); const layerIndex = regionalPrompts.layers.findIndex((l) => l.id === id);
const layerCount = regionalPrompts.layers.length; const layerCount = regionalPrompts.layers.length;
return { return {
@ -87,4 +86,6 @@ export const LayerMenu: React.FC<Props> = ({ id }) => {
</MenuList> </MenuList>
</Menu> </Menu>
); );
}; });
LayerMenu.displayName = 'LayerMenu';

View File

@ -2,14 +2,14 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { useLayerIsVisible } from 'features/regionalPrompts/hooks/layerStateHooks'; import { useLayerIsVisible } from 'features/regionalPrompts/hooks/layerStateHooks';
import { layerIsVisibleToggled } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { layerIsVisibleToggled } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { PiEyeBold, PiEyeClosedBold } from 'react-icons/pi'; import { PiEyeBold, PiEyeClosedBold } from 'react-icons/pi';
type Props = { type Props = {
id: string; id: string;
}; };
export const LayerVisibilityToggle = ({ id }: Props) => { export const LayerVisibilityToggle = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isVisible = useLayerIsVisible(id); const isVisible = useLayerIsVisible(id);
const onClick = useCallback(() => { const onClick = useCallback(() => {
@ -25,4 +25,6 @@ export const LayerVisibilityToggle = ({ id }: Props) => {
onClick={onClick} onClick={onClick}
/> />
); );
}; });
LayerVisibilityToggle.displayName = 'LayerVisibilityToggle';

View File

@ -1,5 +1,6 @@
import { rgbColorToString } from 'features/canvas/util/colorToString'; import { rgbColorToString } from 'features/canvas/util/colorToString';
import type { LineObject } from 'features/regionalPrompts/store/regionalPromptsSlice'; import type { LineObject } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo } from 'react';
import type { RgbColor } from 'react-colorful'; import type { RgbColor } from 'react-colorful';
import { Line } from 'react-konva'; import { Line } from 'react-konva';
@ -9,7 +10,7 @@ type Props = {
color: RgbColor; color: RgbColor;
}; };
export const LineComponent = ({ layerId, line, color }: Props) => { export const LineComponent = memo(({ layerId, line, color }: Props) => {
return ( return (
<Line <Line
id={`layer-${layerId}.line-${line.id}`} id={`layer-${layerId}.line-${line.id}`}
@ -25,4 +26,6 @@ export const LineComponent = ({ layerId, line, color }: Props) => {
listening={false} listening={false}
/> />
); );
}; });
LineComponent.displayName = 'LineComponent';

View File

@ -1,10 +1,10 @@
import { CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { promptLayerOpacityChanged } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { promptLayerOpacityChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const PromptLayerOpacity = () => { export const PromptLayerOpacity = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const promptLayerOpacity = useAppSelector((s) => s.regionalPrompts.promptLayerOpacity); const promptLayerOpacity = useAppSelector((s) => s.regionalPrompts.promptLayerOpacity);
@ -20,4 +20,6 @@ export const PromptLayerOpacity = () => {
<CompositeSlider min={0.25} max={1} step={0.01} value={promptLayerOpacity} onChange={onChange} /> <CompositeSlider min={0.25} max={1} step={0.01} value={promptLayerOpacity} onChange={onChange} />
</FormControl> </FormControl>
); );
}; });
PromptLayerOpacity.displayName = 'PromptLayerOpacity';

View File

@ -1,5 +1,6 @@
import { rgbColorToString } from 'features/canvas/util/colorToString'; import { rgbColorToString } from 'features/canvas/util/colorToString';
import type { FillRectObject } from 'features/regionalPrompts/store/regionalPromptsSlice'; import type { FillRectObject } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo } from 'react';
import type { RgbColor } from 'react-colorful'; import type { RgbColor } from 'react-colorful';
import { Rect } from 'react-konva'; import { Rect } from 'react-konva';
@ -8,8 +9,18 @@ type Props = {
color: RgbColor; color: RgbColor;
}; };
export const RectComponent = ({ rect, color }: Props) => { export const RectComponent = memo(({ rect, color }: Props) => {
return ( return (
<Rect key={rect.id} x={rect.x} y={rect.y} width={rect.width} height={rect.height} fill={rgbColorToString(color)} /> <Rect
key={rect.id}
x={rect.x}
y={rect.y}
width={rect.width}
height={rect.height}
fill={rgbColorToString(color)}
listening={false}
/>
); );
}; });
RectComponent.displayName = 'RectComponent';

View File

@ -1,6 +1,6 @@
/* eslint-disable i18next/no-literal-string */ /* eslint-disable i18next/no-literal-string */
import { Button, Flex } from '@invoke-ai/ui-library'; import { Button, Flex } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton'; import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton';
import { BrushSize } from 'features/regionalPrompts/components/BrushSize'; import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
@ -11,8 +11,9 @@ import { ToolChooser } from 'features/regionalPrompts/components/ToolChooser';
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { getRegionalPromptLayerBlobs } from 'features/regionalPrompts/util/getLayerBlobs'; import { getRegionalPromptLayerBlobs } from 'features/regionalPrompts/util/getLayerBlobs';
import { ImageSizeLinear } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear'; import { ImageSizeLinear } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear';
import { memo } from 'react';
const selectLayerIdsReversed = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => const selectLayerIdsReversed = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
regionalPrompts.layers.map((l) => l.id).reverse() regionalPrompts.layers.map((l) => l.id).reverse()
); );
@ -20,7 +21,7 @@ const debugBlobs = () => {
getRegionalPromptLayerBlobs(true); getRegionalPromptLayerBlobs(true);
}; };
export const RegionalPromptsEditor = () => { export const RegionalPromptsEditor = memo(() => {
const layerIdsReversed = useAppSelector(selectLayerIdsReversed); const layerIdsReversed = useAppSelector(selectLayerIdsReversed);
return ( return (
<Flex gap={4}> <Flex gap={4}>
@ -40,4 +41,6 @@ export const RegionalPromptsEditor = () => {
</Flex> </Flex>
</Flex> </Flex>
); );
}; });
RegionalPromptsEditor.displayName = 'RegionalPromptsEditor';

View File

@ -1,5 +1,5 @@
import { chakra } from '@invoke-ai/ui-library'; import { chakra } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { BrushPreviewOutline } from 'features/regionalPrompts/components/BrushPreview'; import { BrushPreviewOutline } from 'features/regionalPrompts/components/BrushPreview';
import { LayerComponent } from 'features/regionalPrompts/components/LayerComponent'; import { LayerComponent } from 'features/regionalPrompts/components/LayerComponent';
@ -15,7 +15,7 @@ import type Konva from 'konva';
import { memo, useCallback, useMemo, useRef } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react';
import { Layer, Stage } from 'react-konva'; import { Layer, Stage } from 'react-konva';
const selectLayerIds = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => const selectLayerIds = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
regionalPrompts.layers.map((l) => l.id) regionalPrompts.layers.map((l) => l.id)
); );

View File

@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store'; import type { PersistConfig, RootState } from 'app/store/store';
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
import type Konva from 'konva'; import type { Stage } from 'konva/lib/Stage';
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 { RgbColor } from 'react-colorful'; import type { RgbColor } from 'react-colorful';
@ -270,8 +270,8 @@ export const regionalPromptsPersistConfig: PersistConfig<RegionalPromptsState> =
export const $isMouseDown = atom(false); export const $isMouseDown = atom(false);
export const $isMouseOver = atom(false); export const $isMouseOver = atom(false);
export const $cursorPosition = atom<Vector2d | null>(null); export const $cursorPosition = atom<Vector2d | null>(null);
export const $stage = atom<Konva.Stage | null>(null); export const $stage = atom<Stage | null>(null);
export const getStage = (): Konva.Stage => { export const getStage = (): Stage => {
const stage = $stage.get(); const stage = $stage.get();
assert(stage); assert(stage);
return stage; return stage;