mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
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:
@@ -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]);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user