diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 2826fb5394..b04596c0dd 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1646,7 +1646,11 @@ "storeNotInitialized": "Store is not initialized" }, "controlLayers": { + "resetCanvas": "Reset Canvas", "resetAll": "Reset All", + "clearCaches": "Clear Caches", + "recalculateRects": "Recalculate Rects", + "clipToBbox": "Clip Strokes to Bbox", "addLayer": "Add Layer", "moveToFront": "Move to Front", "moveToBack": "Move to Back", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx deleted file mode 100644 index 58942034bc..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - Button, - Checkbox, - Flex, - FormControl, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CanvasSettingsDynamicGridToggle } from 'features/controlLayers/components/CanvasSettingsDynamicGridToggle'; -import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { clipToBboxChanged, invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { RiSettings4Fill } from 'react-icons/ri'; - -const ControlLayersSettingsPopover = () => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const canvasManager = useStore($canvasManager); - const clipToBbox = useAppSelector((s) => s.canvasV2.settings.clipToBbox); - const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); - const onChangeInvertScroll = useCallback( - (e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)), - [dispatch] - ); - const onChangeClipToBbox = useCallback( - (e: ChangeEvent<HTMLInputElement>) => dispatch(clipToBboxChanged(e.target.checked)), - [dispatch] - ); - const clearCaches = useCallback(() => { - canvasManager?.cache.clearAll(); - }, [canvasManager]); - const calculateBboxes = useCallback(() => { - if (!canvasManager) { - return; - } - const adapters = [ - ...canvasManager.rasterLayerAdapters.values(), - ...canvasManager.controlLayerAdapters.values(), - ...canvasManager.regionalGuidanceAdapters.values(), - ...canvasManager.inpaintMaskAdapters.values(), - ]; - for (const adapter of adapters) { - adapter.transformer.requestRectCalculation(); - } - }, [canvasManager]); - return ( - <Popover isLazy> - <PopoverTrigger> - <IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} /> - </PopoverTrigger> - <PopoverContent> - <PopoverBody> - <Flex direction="column" gap={2}> - <FormControl w="full"> - <FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel> - <Checkbox isChecked={invertScroll} onChange={onChangeInvertScroll} /> - </FormControl> - <FormControl w="full"> - <FormLabel flexGrow={1}>{t('unifiedCanvas.clipToBbox')}</FormLabel> - <Checkbox isChecked={clipToBbox} onChange={onChangeClipToBbox} /> - </FormControl> - <CanvasSettingsDynamicGridToggle /> - <Button onClick={clearCaches} size="sm"> - Clear Caches - </Button> - <Button onClick={calculateBboxes} size="sm"> - Calculate Bboxes - </Button> - </Flex> - </PopoverBody> - </PopoverContent> - </Popover> - ); -}; - -export default memo(ControlLayersSettingsPopover); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 69ec88c5cf..ec968f19f8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,63 +1,38 @@ /* eslint-disable i18next/no-literal-string */ -import { Flex, Switch } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; +import { Flex, Spacer } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton'; import { CanvasScale } from 'features/controlLayers/components/CanvasScale'; -import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; -import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton'; +import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover'; import { ToolBrushWidth } from 'features/controlLayers/components/Tool/ToolBrushWidth'; import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser'; import { ToolEraserWidth } from 'features/controlLayers/components/Tool/ToolEraserWidth'; import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; -import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; export const ControlLayersToolbar = memo(() => { const tool = useAppSelector((s) => s.canvasV2.tool.selected); - const canvasManager = useStore($canvasManager); - const onChangeDebugging = useCallback( - (e: ChangeEvent<HTMLInputElement>) => { - if (!canvasManager) { - return; - } - if (e.target.checked) { - canvasManager.enableDebugging(); - } else { - canvasManager.disableDebugging(); - } - }, - [canvasManager] - ); return ( - <Flex w="full" gap={2}> - <Flex flex={1} justifyContent="center"> - <Flex gap={2} marginInlineEnd="auto" alignItems="center"> - <ToggleProgressButton /> - <ToolChooser /> - </Flex> - </Flex> - <Flex flex={1} gap={2} justifyContent="center" alignItems="center"> + <CanvasManagerProviderGate> + <Flex w="full" gap={2} alignItems="center"> + <ToggleProgressButton /> + <ToolChooser /> {tool === 'brush' && <ToolBrushWidth />} {tool === 'eraser' && <ToolEraserWidth />} + <Spacer /> + <CanvasScale /> + <CanvasResetViewButton /> + <Spacer /> + <ToolFillColorPicker /> + <UndoRedoButtonGroup /> + <CanvasSettingsPopover /> + <ViewerToggleMenu /> </Flex> - <CanvasScale /> - <CanvasResetViewButton /> - <Switch onChange={onChangeDebugging}>debug</Switch> - <Flex flex={1} justifyContent="center"> - <Flex gap={2} marginInlineStart="auto" alignItems="center"> - <ToolFillColorPicker /> - <UndoRedoButtonGroup /> - <ControlLayersSettingsPopover /> - <ResetCanvasButton /> - <ViewerToggleMenu /> - </Flex> - </Flex> - </Flex> + </CanvasManagerProviderGate> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ResetCanvasButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ResetCanvasButton.tsx deleted file mode 100644 index f0880b6e02..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ResetCanvasButton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { canvasReset } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; -import { PiTrashBold } from 'react-icons/pi'; - -export const ResetCanvasButton = memo(() => { - const dispatch = useAppDispatch(); - const onClick = useCallback(() => { - dispatch(canvasReset()); - }, [dispatch]); - return <IconButton onClick={onClick} icon={<PiTrashBold />} aria-label="Reset canvas" colorScheme="error" />; -}); - -ResetCanvasButton.displayName = 'ResetCanvasButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClearCachesButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClearCachesButton.tsx new file mode 100644 index 0000000000..a3fd6c7317 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClearCachesButton.tsx @@ -0,0 +1,19 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasSettingsClearCachesButton = memo(() => { + const { t } = useTranslation(); + const canvasManager = useCanvasManager(); + const clearCaches = useCallback(() => { + canvasManager.cache.clearAll(); + }, [canvasManager]); + return ( + <Button onClick={clearCaches} size="sm" colorScheme="warning"> + {t('controlLayers.clearCaches')} + </Button> + ); +}); + +CanvasSettingsClearCachesButton.displayName = 'CanvasSettingsClearCachesButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx new file mode 100644 index 0000000000..205c36070c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx @@ -0,0 +1,24 @@ +import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { clipToBboxChanged } from 'features/controlLayers/store/canvasV2Slice'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasSettingsClipToBboxCheckbox = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const clipToBbox = useAppSelector((s) => s.canvasV2.settings.clipToBbox); + const onChange = useCallback( + (e: ChangeEvent<HTMLInputElement>) => dispatch(clipToBboxChanged(e.target.checked)), + [dispatch] + ); + return ( + <FormControl w="full"> + <FormLabel flexGrow={1}>{t('controlLayers.clipToBbox')}</FormLabel> + <Checkbox isChecked={clipToBbox} onChange={onChange} /> + </FormControl> + ); +}); + +CanvasSettingsClipToBboxCheckbox.displayName = 'CanvasSettingsClipToBboxCheckbox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsDynamicGridToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx similarity index 85% rename from invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsDynamicGridToggle.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx index 916abc5574..71ffe1e729 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsDynamicGridToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx @@ -4,7 +4,7 @@ import { settingsDynamicGridToggled } from 'features/controlLayers/store/canvasV import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -export const CanvasSettingsDynamicGridToggle = memo(() => { +export const CanvasSettingsDynamicGridSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const dynamicGrid = useAppSelector((s) => s.canvasV2.settings.dynamicGrid); @@ -22,4 +22,4 @@ export const CanvasSettingsDynamicGridToggle = memo(() => { ); }); -CanvasSettingsDynamicGridToggle.displayName = 'CanvasSettingsDynamicGridToggle'; +CanvasSettingsDynamicGridSwitch.displayName = 'CanvasSettingsDynamicGridSwitch'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx new file mode 100644 index 0000000000..1c9a4f174c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx @@ -0,0 +1,24 @@ +import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasSettingsInvertScrollCheckbox = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); + const onChange = useCallback( + (e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)), + [dispatch] + ); + return ( + <FormControl w="full"> + <FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel> + <Checkbox isChecked={invertScroll} onChange={onChange} /> + </FormControl> + ); +}); + +CanvasSettingsInvertScrollCheckbox.displayName = 'CanvasSettingsInvertScrollCheckbox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx new file mode 100644 index 0000000000..c0c91ba936 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx @@ -0,0 +1,61 @@ +import { + Divider, + Flex, + IconButton, + Popover, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, + useShiftModifier, +} from '@invoke-ai/ui-library'; +import { CanvasSettingsClearCachesButton } from 'features/controlLayers/components/Settings/CanvasSettingsClearCachesButton'; +import { CanvasSettingsClipToBboxCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox'; +import { CanvasSettingsDynamicGridSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch'; +import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox'; +import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton'; +import { CanvasSettingsResetButton } from 'features/controlLayers/components/Settings/CanvasSettingsResetButton'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { RiSettings4Fill } from 'react-icons/ri'; + +export const CanvasSettingsPopover = memo(() => { + const { t } = useTranslation(); + return ( + <Popover isLazy> + <PopoverTrigger> + <IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} /> + </PopoverTrigger> + <PopoverContent> + <PopoverArrow /> + <PopoverBody> + <Flex direction="column" gap={2}> + <CanvasSettingsInvertScrollCheckbox /> + <CanvasSettingsClipToBboxCheckbox /> + <CanvasSettingsDynamicGridSwitch /> + <CanvasSettingsResetButton /> + <DebugSettings /> + </Flex> + </PopoverBody> + </PopoverContent> + </Popover> + ); +}); + +CanvasSettingsPopover.displayName = 'CanvasSettingsPopover'; + +const DebugSettings = () => { + const shift = useShiftModifier(); + + if (!shift) { + return null; + } + + return ( + <> + <Divider /> + <CanvasSettingsClearCachesButton /> + <CanvasSettingsRecalculateRectsButton /> + </> + ); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton.tsx new file mode 100644 index 0000000000..7b3be93bdd --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton.tsx @@ -0,0 +1,28 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasSettingsRecalculateRectsButton = memo(() => { + const { t } = useTranslation(); + const canvasManager = useCanvasManager(); + const onClick = useCallback(() => { + const adapters = [ + ...canvasManager.rasterLayerAdapters.values(), + ...canvasManager.controlLayerAdapters.values(), + ...canvasManager.regionalGuidanceAdapters.values(), + ...canvasManager.inpaintMaskAdapters.values(), + ]; + for (const adapter of adapters) { + adapter.transformer.requestRectCalculation(); + } + }, [canvasManager]); + + return ( + <Button onClick={onClick} size="sm" colorScheme="warning"> + {t('controlLayers.recalculateRects')} + </Button> + ); +}); + +CanvasSettingsRecalculateRectsButton.displayName = 'CanvasSettingsRecalculateRectsButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsResetButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsResetButton.tsx new file mode 100644 index 0000000000..a40a3f3aae --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsResetButton.tsx @@ -0,0 +1,20 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { canvasReset } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasSettingsResetButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + dispatch(canvasReset()); + }, [dispatch]); + return ( + <Button onClick={onClick} colorScheme="error" size="sm"> + {t('controlLayers.resetCanvas')} + </Button> + ); +}); + +CanvasSettingsResetButton.displayName = 'CanvasSettingsResetButton';