perf(ui): optimize all selectors 1

I learned that the inline selector syntax recreates the selector function on every render:

```ts
const val = useAppSelector((s) => s.slice.val)
```

Not good! Better is to create a selector outside the function and use it. Doing that for all selectors now, most of the way through now. Feels snappier.
This commit is contained in:
psychedelicious
2024-08-27 13:19:14 +10:00
parent 04f78a99ad
commit bac0ce1e69
92 changed files with 561 additions and 294 deletions

View File

@@ -8,7 +8,7 @@ import {
rasterLayerAdded,
rgAdded,
} from 'features/controlLayers/store/canvasSlice';
import { selectEntityCount } from 'features/controlLayers/store/selectors';
import { selectHasEntities } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi';
@@ -16,10 +16,7 @@ import { PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi';
export const CanvasEntityListMenuItems = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const hasEntities = useAppSelector((s) => {
const count = selectEntityCount(s);
return count > 0;
});
const hasEntities = useAppSelector(selectHasEntities);
const addInpaintMask = useCallback(() => {
dispatch(inpaintMaskAdded({ isSelected: true }));
}, [dispatch]);

View File

@@ -1,13 +1,16 @@
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice';
import { selectCanvasSessionSlice, sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectCanvasMode = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.mode);
export const CanvasModeSwitcher = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const mode = useAppSelector((s) => s.canvasSession.mode);
const mode = useAppSelector(selectCanvasMode);
const onClickGenerate = useCallback(() => dispatch(sessionModeChanged({ mode: 'generate' })), [dispatch]);
const onClickCompose = useCallback(() => dispatch(sessionModeChanged({ mode: 'compose' })), [dispatch]);

View File

@@ -4,11 +4,11 @@ import { CanvasAddEntityButtons } from 'features/controlLayers/components/Canvas
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectEntityCount } from 'features/controlLayers/store/selectors';
import { selectHasEntities } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
export const CanvasPanelContent = memo(() => {
const hasEntities = useAppSelector((s) => selectEntityCount(s) > 0);
const hasEntities = useAppSelector(selectHasEntities);
const renderMenu = useCallback(
() => (
<MenuList>

View File

@@ -3,6 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectBase } from 'features/controlLayers/store/paramsSlice';
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -18,7 +19,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const canvasManager = useCanvasManager();
const currentBaseModel = useAppSelector((s) => s.params.model?.base);
const currentBaseModel = useAppSelector(selectBase);
const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels();
const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]);

View File

@@ -1,3 +1,4 @@
import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
@@ -10,8 +11,12 @@ const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => {
return canvas.controlLayers.entities.map(mapId).reverse();
});
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
return selectedEntityIdentifier?.type === 'control_layer';
});
export const ControlLayerEntityList = memo(() => {
const isSelected = useAppSelector((s) => selectSelectedEntityIdentifier(s)?.type === 'control_layer');
const isSelected = useAppSelector(selectIsSelected);
const layerIds = useAppSelector(selectEntityIds);
if (layerIds.length === 0) {

View File

@@ -1,20 +1,17 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { FilterConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
import { configSelector } from 'features/system/store/configSelectors';
import { selectConfigSlice } from 'features/system/store/configSlice';
import { includes, map } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
const selectDisabledProcessors = createMemoizedSelector(
configSelector,
(config) => config.sd.disabledControlNetProcessors
);
const selectDisabledProcessors = createSelector(selectConfigSlice, (config) => config.sd.disabledControlNetProcessors);
type Props = {
filterType: FilterConfig['type'];

View File

@@ -2,6 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { selectBase } from 'features/controlLayers/store/paramsSlice';
import type { CLIPVisionModelV2 } from 'features/controlLayers/store/types';
import { isCLIPVisionModelV2 } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
@@ -24,7 +25,7 @@ type Props = {
export const IPAdapterModel = memo(({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => {
const { t } = useTranslation();
const currentBaseModel = useAppSelector((s) => s.params.model?.base);
const currentBaseModel = useAppSelector(selectBase);
const [modelConfigs, { isLoading }] = useIPAdapterModels();
const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]);

View File

@@ -1,13 +1,16 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { settingsAutoSaveToggled } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectCanvasSettingsSlice, settingsAutoSaveToggled } from 'features/controlLayers/store/canvasSettingsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectAutoSave = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.autoSave);
export const CanvasSettingsAutoSaveCheckbox = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const autoSave = useAppSelector((s) => s.canvasSettings.autoSave);
const autoSave = useAppSelector(selectAutoSave);
const onChange = useCallback(() => dispatch(settingsAutoSaveToggled()), [dispatch]);
return (
<FormControl w="full">

View File

@@ -1,14 +1,17 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { clipToBboxChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import { clipToBboxChanged, selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectClipToBbox = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.clipToBbox);
export const CanvasSettingsClipToBboxCheckbox = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const clipToBbox = useAppSelector((s) => s.canvasSettings.clipToBbox);
const clipToBbox = useAppSelector(selectClipToBbox);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => dispatch(clipToBboxChanged(e.target.checked)),
[dispatch]

View File

@@ -1,13 +1,19 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { settingsDynamicGridToggled } from 'features/controlLayers/store/canvasSettingsSlice';
import {
selectCanvasSettingsSlice,
settingsDynamicGridToggled,
} from 'features/controlLayers/store/canvasSettingsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
export const CanvasSettingsDynamicGridSwitch = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const dynamicGrid = useAppSelector((s) => s.canvasSettings.dynamicGrid);
const dynamicGrid = useAppSelector(selectDynamicGrid);
const onChange = useCallback(() => {
dispatch(settingsDynamicGridToggled());
}, [dispatch]);

View File

@@ -1,14 +1,17 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { invertScrollChanged } from 'features/controlLayers/store/toolSlice';
import { invertScrollChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectInvertScroll = createSelector(selectToolSlice, (tool) => tool.invertScroll);
export const CanvasSettingsInvertScrollCheckbox = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const invertScroll = useAppSelector((s) => s.tool.invertScroll);
const invertScroll = useAppSelector(selectInvertScroll);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)),
[dispatch]

View File

@@ -1,5 +1,6 @@
import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { $socket } from 'app/hooks/useSocketIO';
import { logger } from 'app/logging/logger';
import { useAppStore } from 'app/store/nanostores/store';
@@ -7,6 +8,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import Konva from 'konva';
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
@@ -51,8 +53,10 @@ type Props = {
asPreview?: boolean;
};
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
export const StageComponent = memo(({ asPreview = false }: Props) => {
const dynamicGrid = useAppSelector((s) => s.canvasSettings.dynamicGrid);
const dynamicGrid = useAppSelector(selectDynamicGrid);
const [stage] = useState(
() =>

View File

@@ -1,9 +1,10 @@
import { useAppSelector } from 'app/store/storeHooks';
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
export const StagingAreaIsStagingGate = memo((props: PropsWithChildren) => {
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
const isStaging = useAppSelector(selectIsStaging);
if (!isStaging) {
return null;

View File

@@ -1,9 +1,11 @@
import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { INTERACTION_SCOPES, useScopeOnMount } from 'common/hooks/interactionScopes';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import {
selectCanvasSessionSlice,
sessionNextStagedImageSelected,
sessionPrevStagedImageSelected,
sessionStagedImageDiscarded,
@@ -25,15 +27,25 @@ import {
} from 'react-icons/pi';
import { useChangeImageIsIntermediateMutation } from 'services/api/endpoints/images';
const selectStagedImageIndex = createSelector(
selectCanvasSessionSlice,
(canvasSession) => canvasSession.selectedStagedImageIndex
);
const selectSelectedImage = createSelector(
[selectCanvasSessionSlice, selectStagedImageIndex],
(canvasSession, index) => canvasSession.stagedImages[index] ?? null
);
const selectImageCount = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.stagedImages.length);
export const StagingAreaToolbar = memo(() => {
const dispatch = useAppDispatch();
const session = useAppSelector((s) => s.canvasSession);
const canvasManager = useCanvasManager();
const index = useAppSelector(selectStagedImageIndex);
const selectedImage = useAppSelector(selectSelectedImage);
const imageCount = useAppSelector(selectImageCount);
const shouldShowStagedImage = useStore(canvasManager.stateApi.$shouldShowStagedImage);
const images = useMemo(() => session.stagedImages, [session]);
const selectedImage = useMemo(() => {
return images[session.selectedStagedImageIndex] ?? null;
}, [images, session.selectedStagedImageIndex]);
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation();
useScopeOnMount('stagingArea');
@@ -52,19 +64,19 @@ export const StagingAreaToolbar = memo(() => {
if (!selectedImage) {
return;
}
dispatch(sessionStagingAreaImageAccepted({ index: session.selectedStagedImageIndex }));
}, [dispatch, selectedImage, session.selectedStagedImageIndex]);
dispatch(sessionStagingAreaImageAccepted({ index }));
}, [dispatch, index, selectedImage]);
const onDiscardOne = useCallback(() => {
if (!selectedImage) {
return;
}
if (images.length === 1) {
if (imageCount === 1) {
dispatch(sessionStagingAreaReset());
} else {
dispatch(sessionStagedImageDiscarded({ index: session.selectedStagedImageIndex }));
dispatch(sessionStagedImageDiscarded({ index }));
}
}, [selectedImage, images.length, dispatch, session.selectedStagedImageIndex]);
}, [selectedImage, imageCount, dispatch, index]);
const onDiscardAll = useCallback(() => {
dispatch(sessionStagingAreaReset());
@@ -112,12 +124,12 @@ export const StagingAreaToolbar = memo(() => {
);
const counterText = useMemo(() => {
if (images.length > 0) {
return `${(session.selectedStagedImageIndex ?? 0) + 1} of ${images.length}`;
if (imageCount > 0) {
return `${(index ?? 0) + 1} of ${imageCount}`;
} else {
return `0 of 0`;
}
}, [images.length, session.selectedStagedImageIndex]);
}, [imageCount, index]);
return (
<>
@@ -128,7 +140,7 @@ export const StagingAreaToolbar = memo(() => {
icon={<PiArrowLeftBold />}
onClick={onPrev}
colorScheme="invokeBlue"
isDisabled={images.length <= 1 || !shouldShowStagedImage}
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
/>
<Button colorScheme="base" pointerEvents="none" minW={28}>
{counterText}
@@ -139,7 +151,7 @@ export const StagingAreaToolbar = memo(() => {
icon={<PiArrowRightBold />}
onClick={onNext}
colorScheme="invokeBlue"
isDisabled={images.length <= 1 || !shouldShowStagedImage}
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
/>
</ButtonGroup>
<ButtonGroup borderRadius="base" shadow="dark-lg">

View File

@@ -3,6 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { memo, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@@ -14,7 +15,7 @@ export const ToolBboxButton = memo(() => {
const isSelected = useToolIsSelected('bbox');
const isFiltering = useIsFiltering();
const isTransforming = useIsTransforming();
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
const isStaging = useAppSelector(selectIsStaging);
const isDisabled = useMemo(() => {
return isTransforming || isFiltering || isStaging;
}, [isFiltering, isStaging, isTransforming]);

View File

@@ -9,18 +9,20 @@ import {
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { brushWidthChanged } from 'features/controlLayers/store/toolSlice';
import { brushWidthChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const marks = [0, 100, 200, 300];
const formatPx = (v: number | string) => `${v} px`;
const selectBrushWidth = createSelector(selectToolSlice, (tool) => tool.brush.width);
export const ToolBrushWidth = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const width = useAppSelector((s) => s.tool.brush.width);
const width = useAppSelector(selectBrushWidth);
const onChange = useCallback(
(v: number) => {
dispatch(brushWidthChanged(Math.round(v)));

View File

@@ -3,6 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { memo, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@@ -14,7 +15,7 @@ export const ToolColorPickerButton = memo(() => {
const isTransforming = useIsTransforming();
const selectColorPicker = useSelectTool('colorPicker');
const isSelected = useToolIsSelected('colorPicker');
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
const isStaging = useAppSelector(selectIsStaging);
const isDisabled = useMemo(() => {
return isTransforming || isFiltering || isStaging;

View File

@@ -9,18 +9,20 @@ import {
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { eraserWidthChanged } from 'features/controlLayers/store/toolSlice';
import { eraserWidthChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const marks = [0, 100, 200, 300];
const formatPx = (v: number | string) => `${v} px`;
const selectEraserWidth = createSelector(selectToolSlice, (tool) => tool.eraser.width);
export const ToolEraserWidth = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const width = useAppSelector((s) => s.tool.eraser.width);
const width = useAppSelector(selectEraserWidth);
const onChange = useCallback(
(v: number) => {
dispatch(eraserWidthChanged(Math.round(v)));

View File

@@ -1,15 +1,18 @@
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { fillChanged } from 'features/controlLayers/store/toolSlice';
import { fillChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectFill = createSelector(selectToolSlice, (tool) => tool.fill);
export const ToolFillColorPicker = memo(() => {
const { t } = useTranslation();
const fill = useAppSelector((s) => s.tool.fill);
const fill = useAppSelector(selectFill);
const dispatch = useAppDispatch();
const onChange = useCallback(
(color: RgbaColor) => {

View File

@@ -3,6 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { memo, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@@ -12,7 +13,7 @@ export const ToolViewButton = memo(() => {
const { t } = useTranslation();
const isTransforming = useIsTransforming();
const isFiltering = useIsFiltering();
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
const isStaging = useAppSelector(selectIsStaging);
const selectView = useSelectTool('view');
const isSelected = useToolIsSelected('view');
const isDisabled = useMemo(() => {

View File

@@ -13,6 +13,7 @@ import {
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { snapToNearest } from 'features/controlLayers/konva/util';
import { entityOpacityChanged } from 'features/controlLayers/store/canvasSlice';
@@ -60,27 +61,29 @@ const sliderDefaultValue = mapOpacityToSliderValue(100);
const snapCandidates = marks.slice(1, marks.length - 1);
const selectOpacity = createSelector(selectCanvasSlice, (canvas) => {
const selectedEntityIdentifier = canvas.selectedEntityIdentifier;
if (!selectedEntityIdentifier) {
return 100; // fallback to 100% opacity
}
const selectedEntity = selectEntity(canvas, selectedEntityIdentifier);
if (!selectedEntity) {
return 100; // fallback to 100% opacity
}
if (!isDrawableEntity(selectedEntity)) {
return 100; // fallback to 100% opacity
}
// Opacity is a float from 0-1, but we want to display it as a percentage
return selectedEntity.opacity * 100;
});
export const CanvasEntityOpacity = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const opacity = useAppSelector((s) => {
const selectedEntityIdentifier = selectSelectedEntityIdentifier(s);
if (!selectedEntityIdentifier) {
return null;
}
const canvas = selectCanvasSlice(s);
const selectedEntity = selectEntity(canvas, selectedEntityIdentifier);
if (!selectedEntity) {
return null;
}
if (!isDrawableEntity(selectedEntity)) {
return null;
}
return selectedEntity.opacity;
});
const opacity = useAppSelector(selectOpacity);
const [localOpacity, setLocalOpacity] = useState((opacity ?? 1) * 100);
const [localOpacity, setLocalOpacity] = useState(opacity);
const onChangeSlider = useCallback(
(opacity: number) => {

View File

@@ -1,6 +1,8 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { selectConfigSlice } from 'features/system/store/configSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -12,15 +14,11 @@ type Props = {
const formatValue = (v: number) => v.toFixed(2);
const marks = [0, 1, 2];
const selectWeightConfig = createSelector(selectConfigSlice, (config) => config.sd.ca.weight);
export const Weight = memo(({ weight, onChange }: Props) => {
const { t } = useTranslation();
const initial = useAppSelector((s) => s.config.sd.ca.weight.initial);
const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin);
const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax);
const numberInputMin = useAppSelector((s) => s.config.sd.ca.weight.numberInputMin);
const numberInputMax = useAppSelector((s) => s.config.sd.ca.weight.numberInputMax);
const coarseStep = useAppSelector((s) => s.config.sd.ca.weight.coarseStep);
const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep);
const config = useAppSelector(selectWeightConfig);
return (
<FormControl orientation="horizontal">
@@ -30,23 +28,23 @@ export const Weight = memo(({ weight, onChange }: Props) => {
<CompositeSlider
value={weight}
onChange={onChange}
defaultValue={initial}
min={sliderMin}
max={sliderMax}
step={coarseStep}
fineStep={fineStep}
defaultValue={config.initial}
min={config.sliderMin}
max={config.sliderMax}
step={config.coarseStep}
fineStep={config.fineStep}
marks={marks}
formatValue={formatValue}
/>
<CompositeNumberInput
value={weight}
onChange={onChange}
min={numberInputMin}
max={numberInputMax}
step={coarseStep}
fineStep={fineStep}
min={config.numberInputMin}
max={config.numberInputMax}
step={config.coarseStep}
fineStep={config.fineStep}
maxW={20}
defaultValue={initial}
defaultValue={config.initial}
/>
</FormControl>
);

View File

@@ -1,21 +1,16 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
const selectSelectedEntityIdentifier = createMemoizedSelector(
selectCanvasSlice,
(canvasState) => canvasState.selectedEntityIdentifier
);
export function useCanvasDeleteLayerHotkey() {
useAssertSingleton(useCanvasDeleteLayerHotkey.name);
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
const isStaging = useAppSelector(selectIsStaging);
const deleteSelectedLayer = useCallback(() => {
if (selectedEntityIdentifier === null) {

View File

@@ -1,6 +1,7 @@
import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { deepClone } from 'common/util/deepClone';
import { selectBase } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type {
CanvasEntityIdentifier,
@@ -30,7 +31,7 @@ export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIden
export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig => {
const [modelConfigs] = useControlNetAndT2IAdapterModels();
const baseModel = useAppSelector((s) => s.params.model?.base);
const baseModel = useAppSelector(selectBase);
const defaultControlAdapter = useMemo(() => {
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));
@@ -51,7 +52,7 @@ export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig
export const useDefaultIPAdapter = (): IPAdapterConfig => {
const [modelConfigs] = useIPAdapterModels();
const baseModel = useAppSelector((s) => s.params.model?.base);
const baseModel = useAppSelector(selectBase);
const defaultControlAdapter = useMemo(() => {
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));

View File

@@ -1,4 +1,4 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
@@ -271,6 +271,7 @@ export const {
} = paramsSlice.actions;
export const selectParamsSlice = (state: RootState) => state.params;
export const selectBase = createSelector(selectParamsSlice, (params) => params.model?.base);
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {

View File

@@ -39,6 +39,11 @@ export const selectEntityCount = createSelector(selectCanvasSlice, (canvas) => {
);
});
/**
* Selects if the canvas has any entities.
*/
export const selectHasEntities = createSelector(selectEntityCount, (count) => count > 0);
/**
* Selects the optimal dimension for the canvas based on the currently-model
*/
@@ -185,4 +190,3 @@ export const selectIsSelectedEntityDrawable = createSelector(
return isDrawableEntityType(selectedEntityIdentifier.type);
}
);

View File

@@ -1,5 +1,5 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import type { RgbaColor } from 'features/controlLayers/store/types';
export type ToolState = {
@@ -52,3 +52,5 @@ export const toolPersistConfig: PersistConfig<ToolState> = {
migrate,
persistDenylist: [],
};
export const selectToolSlice = (state: RootState) => state.tool;