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

View File

@ -1,49 +1,38 @@
import { useStore } from '@nanostores/react';
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 { memo } from 'react';
import { Circle, Group } from 'react-konva';
const useBrushData = () => {
export const BrushPreviewOutline = memo(() => {
const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize);
const tool = useAppSelector((s) => s.regionalPrompts.tool);
const a = useAppSelector((s) => s.regionalPrompts.promptLayerOpacity);
const color = useAppSelector((s) => {
const _color = s.regionalPrompts.layers.find((l) => l.id === s.regionalPrompts.selectedLayer)?.color;
if (!_color) {
return null;
}
return rgbColorToString(_color);
return rgbaColorToString({ ..._color, a });
});
const pos = useStore($cursorPosition);
return { brushSize, tool, color, pos };
};
export const BrushPreviewFill = () => {
const { brushSize, tool, color, pos } = useBrushData();
if (!brushSize || !color || !pos || tool === 'move') {
return null;
}
return (
<Group listening={false}>
<Circle
x={pos.x}
y={pos.y}
radius={brushSize / 2}
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
x={pos.x}
y={pos.y}
@ -64,4 +53,6 @@ export const BrushPreviewOutline = () => {
/>
</Group>
);
};
});
BrushPreviewOutline.displayName = 'BrushPreviewOutline';

View File

@ -1,10 +1,10 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { brushSizeChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const BrushSize = () => {
export const BrushSize = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize);
@ -21,4 +21,6 @@ export const BrushSize = () => {
<CompositeNumberInput min={1} max={500} value={brushSize} onChange={onChange} />
</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 { 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';
type Props = {
layerId: string;
};
export const LayerBoundingBox = ({ layerId }: Props) => {
export const LayerBoundingBox = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const tool = useAppSelector((s) => s.regionalPrompts.tool);
const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer);
@ -19,7 +19,7 @@ export const LayerBoundingBox = ({ layerId }: Props) => {
const selectBbox = useMemo(
() =>
createSelector(
createMemoizedSelector(
selectRegionalPromptsSlice,
(regionalPrompts) => regionalPrompts.layers.find((layer) => layer.id === layerId)?.bbox ?? null
),
@ -33,6 +33,7 @@ export const LayerBoundingBox = ({ layerId }: Props) => {
return (
<KonvaRect
name="layer bbox"
onMouseDown={onMouseDown}
stroke={selectedLayer === layerId ? 'rgba(153, 187, 189, 1)' : 'rgba(255, 255, 255, 0.149)'}
strokeWidth={1}
@ -43,4 +44,6 @@ export const LayerBoundingBox = ({ layerId }: Props) => {
listening={tool === 'move'}
/>
);
};
});
LayerBoundingBox.displayName = 'LayerBoundingBox';

View File

@ -1,18 +1,31 @@
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 { useLayer } from 'features/regionalPrompts/hooks/layerStateHooks';
import { promptRegionLayerColorChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react';
import {
promptRegionLayerColorChanged,
selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useMemo } from 'react';
import type { RgbColor } from 'react-colorful';
import { PiEyedropperBold } from 'react-icons/pi';
import { assert } from 'tsafe';
type Props = {
id: string;
};
export const LayerColorPicker = ({ id }: Props) => {
const layer = useLayer(id);
export const LayerColorPicker = memo(({ id }: Props) => {
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 onColorChange = useCallback(
(color: RgbColor) => {
@ -27,9 +40,11 @@ export const LayerColorPicker = ({ id }: Props) => {
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<RgbColorPicker color={layer.color} onChange={onColorChange} withNumberInput />
<RgbColorPicker color={color} onChange={onColorChange} withNumberInput />
</PopoverBody>
</PopoverContent>
</Popover>
);
};
});
LayerColorPicker.displayName = 'LayerColorPicker';

View File

@ -1,6 +1,5 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import { BrushPreviewFill } from 'features/regionalPrompts/components/BrushPreview';
import { LayerBoundingBox } from 'features/regionalPrompts/components/LayerBoundingBox';
import { LineComponent } from 'features/regionalPrompts/components/LineComponent';
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 { KonvaEventObject, Node as KonvaNodeType, NodeConfig as KonvaNodeConfigType } from 'konva/lib/Node';
import type { IRect, Vector2d } from 'konva/lib/types';
import type React from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { memo, useCallback, useEffect, useRef } from 'react';
import { Group as KonvaGroup, Layer as KonvaLayer } from 'react-konva';
type Props = {
@ -28,15 +26,13 @@ type Props = {
export const selectPromptLayerObjectGroup = (item: KonvaNodeType<KonvaNodeConfigType>) =>
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 layer = useLayer(id);
const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer);
const promptLayerOpacity = useAppSelector((s) => s.regionalPrompts.promptLayerOpacity);
const tool = useAppSelector((s) => s.regionalPrompts.tool);
const layerRef = useRef<KonvaLayerType>(null);
const groupRef = useRef<KonvaGroupType>(null);
const onChangeBbox = useCallback(
(bbox: IRect | null) => {
dispatch(layerBboxChanged({ layerId: layer.id, bbox }));
@ -94,11 +90,11 @@ 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
// 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();
}, [layer.objects, layer.color, layer.isVisible]);
return (
<>
<KonvaLayer
ref={layerRef}
id={layer.id}
@ -126,7 +122,7 @@ export const LayerComponent: React.FC<Props> = ({ id }) => {
</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 { RegionalPromptsPrompt } from 'features/regionalPrompts/components/RegionalPromptsPrompt';
import { layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
id: string;
};
export const LayerListItem = ({ id }: Props) => {
export const LayerListItem = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer);
@ -46,4 +46,6 @@ export const LayerListItem = ({ id }: Props) => {
</Flex>
</Flex>
);
};
});
LayerListItem.displayName = 'LayerListItem';

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { promptLayerOpacityChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const PromptLayerOpacity = () => {
export const PromptLayerOpacity = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
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} />
</FormControl>
);
};
});
PromptLayerOpacity.displayName = 'PromptLayerOpacity';

View File

@ -1,5 +1,6 @@
import { rgbColorToString } from 'features/canvas/util/colorToString';
import type { FillRectObject } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo } from 'react';
import type { RgbColor } from 'react-colorful';
import { Rect } from 'react-konva';
@ -8,8 +9,18 @@ type Props = {
color: RgbColor;
};
export const RectComponent = ({ rect, color }: Props) => {
export const RectComponent = memo(({ rect, color }: Props) => {
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 */
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 { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton';
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 { getRegionalPromptLayerBlobs } from 'features/regionalPrompts/util/getLayerBlobs';
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()
);
@ -20,7 +21,7 @@ const debugBlobs = () => {
getRegionalPromptLayerBlobs(true);
};
export const RegionalPromptsEditor = () => {
export const RegionalPromptsEditor = memo(() => {
const layerIdsReversed = useAppSelector(selectLayerIdsReversed);
return (
<Flex gap={4}>
@ -40,4 +41,6 @@ export const RegionalPromptsEditor = () => {
</Flex>
</Flex>
);
};
});
RegionalPromptsEditor.displayName = 'RegionalPromptsEditor';

View File

@ -1,5 +1,5 @@
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 { BrushPreviewOutline } from 'features/regionalPrompts/components/BrushPreview';
import { LayerComponent } from 'features/regionalPrompts/components/LayerComponent';
@ -15,7 +15,7 @@ import type Konva from 'konva';
import { memo, useCallback, useMemo, useRef } from 'react';
import { Layer, Stage } from 'react-konva';
const selectLayerIds = createSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
const selectLayerIds = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
regionalPrompts.layers.map((l) => l.id)
);

View File

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