mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): transform tool ux
This commit is contained in:
parent
dcb436adb1
commit
e7ae1ac9b2
@ -6,6 +6,7 @@ import { ControlLayersToolbar } from 'features/controlLayers/components/ControlL
|
|||||||
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
||||||
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
||||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||||
|
import { Transform } from 'features/controlLayers/components/Transform';
|
||||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import { memo, useRef } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ export const CanvasEditor = memo(() => {
|
|||||||
<Flex position="absolute" bottom={16}>
|
<Flex position="absolute" bottom={16}>
|
||||||
<CanvasManagerProviderGate>
|
<CanvasManagerProviderGate>
|
||||||
<Filter />
|
<Filter />
|
||||||
|
<Transform />
|
||||||
</CanvasManagerProviderGate>
|
</CanvasManagerProviderGate>
|
||||||
</Flex>
|
</Flex>
|
||||||
<CanvasDropArea />
|
<CanvasDropArea />
|
||||||
|
@ -22,13 +22,14 @@ export const ControlLayersToolbar = memo(() => {
|
|||||||
<Flex w="full" gap={2} alignItems="center">
|
<Flex w="full" gap={2} alignItems="center">
|
||||||
<ToggleProgressButton />
|
<ToggleProgressButton />
|
||||||
<ToolChooser />
|
<ToolChooser />
|
||||||
|
<Spacer />
|
||||||
{tool === 'brush' && <ToolBrushWidth />}
|
{tool === 'brush' && <ToolBrushWidth />}
|
||||||
{tool === 'eraser' && <ToolEraserWidth />}
|
{tool === 'eraser' && <ToolEraserWidth />}
|
||||||
<ToolFillColorPicker />
|
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<CanvasScale />
|
<CanvasScale />
|
||||||
<CanvasResetViewButton />
|
<CanvasResetViewButton />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
<ToolFillColorPicker />
|
||||||
<CanvasModeSwitcher />
|
<CanvasModeSwitcher />
|
||||||
<UndoRedoButtonGroup />
|
<UndoRedoButtonGroup />
|
||||||
<CanvasSettingsPopover />
|
<CanvasSettingsPopover />
|
||||||
|
@ -3,6 +3,7 @@ import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/
|
|||||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||||
import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset';
|
import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset';
|
||||||
|
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||||
import { RasterLayerMenuItemsRasterToControl } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl';
|
import { RasterLayerMenuItemsRasterToControl } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ export const RasterLayerMenuItems = memo(() => {
|
|||||||
<>
|
<>
|
||||||
<CanvasEntityMenuItemsFilter />
|
<CanvasEntityMenuItemsFilter />
|
||||||
<RasterLayerMenuItemsRasterToControl />
|
<RasterLayerMenuItemsRasterToControl />
|
||||||
|
<CanvasEntityMenuItemsTransform />
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<CanvasEntityMenuItemsArrange />
|
<CanvasEntityMenuItemsArrange />
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiBoundingBoxBold } from 'react-icons/pi';
|
import { PiBoundingBoxBold } from 'react-icons/pi';
|
||||||
@ -9,14 +10,18 @@ import { PiBoundingBoxBold } from 'react-icons/pi';
|
|||||||
export const ToolBboxButton = memo(() => {
|
export const ToolBboxButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming);
|
const isTransforming = useIsTransforming();
|
||||||
|
const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging);
|
||||||
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'bbox');
|
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'bbox');
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isStaging;
|
||||||
|
}, [isStaging, isTransforming]);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(toolChanged('bbox'));
|
dispatch(toolChanged('bbox'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useHotkeys('q', onClick, { enabled: !isDisabled }, [onClick, isDisabled]);
|
useHotkeys('q', onClick, { enabled: !isDisabled || isSelected }, [onClick, isSelected, isDisabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { isDrawableEntityType } from 'features/controlLayers/store/types';
|
import { isDrawableEntityType } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPaintBrushBold } from 'react-icons/pi';
|
import { PiPaintBrushBold } from 'react-icons/pi';
|
||||||
@ -10,19 +11,25 @@ import { PiPaintBrushBold } from 'react-icons/pi';
|
|||||||
export const ToolBrushButton = memo(() => {
|
export const ToolBrushButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const isTransforming = useIsTransforming();
|
||||||
|
const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging);
|
||||||
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'brush');
|
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'brush');
|
||||||
const isDisabled = useAppSelector((s) => {
|
const isDrawingToolAllowed = useAppSelector((s) => {
|
||||||
const entityType = s.canvasV2.selectedEntityIdentifier?.type;
|
if (!s.canvasV2.selectedEntityIdentifier?.type) {
|
||||||
const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false;
|
return false;
|
||||||
const isStaging = s.canvasV2.session.isStaging;
|
}
|
||||||
return !isDrawingToolAllowed || isStaging || s.canvasV2.tool.isTransforming;
|
return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isStaging || !isDrawingToolAllowed;
|
||||||
|
}, [isDrawingToolAllowed, isStaging, isTransforming]);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(toolChanged('brush'));
|
dispatch(toolChanged('brush'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useHotkeys('b', onClick, { enabled: !isDisabled }, [isDisabled, onClick]);
|
useHotkeys('b', onClick, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, onClick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { ButtonGroup } from '@invoke-ai/ui-library';
|
import { ButtonGroup } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { ToolBboxButton } from 'features/controlLayers/components/Tool/ToolBboxButton';
|
import { ToolBboxButton } from 'features/controlLayers/components/Tool/ToolBboxButton';
|
||||||
import { ToolBrushButton } from 'features/controlLayers/components/Tool/ToolBrushButton';
|
import { ToolBrushButton } from 'features/controlLayers/components/Tool/ToolBrushButton';
|
||||||
import { ToolEyeDropperButton } from 'features/controlLayers/components/Tool/ToolEyeDropperButton';
|
import { ToolEyeDropperButton } from 'features/controlLayers/components/Tool/ToolEyeDropperButton';
|
||||||
@ -9,17 +8,15 @@ import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanv
|
|||||||
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
|
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
|
||||||
|
|
||||||
import { ToolEraserButton } from './ToolEraserButton';
|
import { ToolEraserButton } from './ToolEraserButton';
|
||||||
import { ToolTransformButton } from './ToolTransformButton';
|
|
||||||
import { ToolViewButton } from './ToolViewButton';
|
import { ToolViewButton } from './ToolViewButton';
|
||||||
|
|
||||||
export const ToolChooser: React.FC = () => {
|
export const ToolChooser: React.FC = () => {
|
||||||
useCanvasResetLayerHotkey();
|
useCanvasResetLayerHotkey();
|
||||||
useCanvasDeleteLayerHotkey();
|
useCanvasDeleteLayerHotkey();
|
||||||
const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonGroup isAttached isDisabled={isTransforming}>
|
<ButtonGroup isAttached>
|
||||||
<ToolBrushButton />
|
<ToolBrushButton />
|
||||||
<ToolEraserButton />
|
<ToolEraserButton />
|
||||||
<ToolRectButton />
|
<ToolRectButton />
|
||||||
@ -28,7 +25,6 @@ export const ToolChooser: React.FC = () => {
|
|||||||
<ToolBboxButton />
|
<ToolBboxButton />
|
||||||
<ToolEyeDropperButton />
|
<ToolEyeDropperButton />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ToolTransformButton />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { isDrawableEntityType } from 'features/controlLayers/store/types';
|
import { isDrawableEntityType } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiEraserBold } from 'react-icons/pi';
|
import { PiEraserBold } from 'react-icons/pi';
|
||||||
@ -10,19 +11,24 @@ import { PiEraserBold } from 'react-icons/pi';
|
|||||||
export const ToolEraserButton = memo(() => {
|
export const ToolEraserButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const isTransforming = useIsTransforming();
|
||||||
|
const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging);
|
||||||
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eraser');
|
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eraser');
|
||||||
const isDisabled = useAppSelector((s) => {
|
const isDrawingToolAllowed = useAppSelector((s) => {
|
||||||
const entityType = s.canvasV2.selectedEntityIdentifier?.type;
|
if (!s.canvasV2.selectedEntityIdentifier?.type) {
|
||||||
const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false;
|
return false;
|
||||||
const isStaging = s.canvasV2.session.isStaging;
|
}
|
||||||
return !isDrawingToolAllowed || isStaging || s.canvasV2.tool.isTransforming;
|
return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type);
|
||||||
});
|
});
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isStaging || !isDrawingToolAllowed;
|
||||||
|
}, [isDrawingToolAllowed, isStaging, isTransforming]);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(toolChanged('eraser'));
|
dispatch(toolChanged('eraser'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useHotkeys('e', onClick, { enabled: !isDisabled }, [isDisabled, onClick]);
|
useHotkeys('e', onClick, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, onClick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiEyedropperBold } from 'react-icons/pi';
|
import { PiEyedropperBold } from 'react-icons/pi';
|
||||||
@ -9,14 +10,19 @@ import { PiEyedropperBold } from 'react-icons/pi';
|
|||||||
export const ToolEyeDropperButton = memo(() => {
|
export const ToolEyeDropperButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming);
|
const isTransforming = useIsTransforming();
|
||||||
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eyeDropper');
|
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eyeDropper');
|
||||||
|
const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging);
|
||||||
|
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isStaging;
|
||||||
|
}, [isStaging, isTransforming]);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(toolChanged('eyeDropper'));
|
dispatch(toolChanged('eyeDropper'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useHotkeys('i', onClick, { enabled: !isDisabled }, [onClick, isDisabled]);
|
useHotkeys('i', onClick, { enabled: !isDisabled || isSelected }, [onClick, isSelected, isDisabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { memo, useCallback } from 'react';
|
import { isDrawableEntityType } from 'features/controlLayers/store/types';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCursorBold } from 'react-icons/pi';
|
import { PiCursorBold } from 'react-icons/pi';
|
||||||
@ -9,16 +11,24 @@ import { PiCursorBold } from 'react-icons/pi';
|
|||||||
export const ToolMoveButton = memo(() => {
|
export const ToolMoveButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const isTransforming = useIsTransforming();
|
||||||
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'move');
|
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'move');
|
||||||
const isDisabled = useAppSelector(
|
const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging);
|
||||||
(s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming
|
const isDrawingToolAllowed = useAppSelector((s) => {
|
||||||
);
|
if (!s.canvasV2.selectedEntityIdentifier?.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type);
|
||||||
|
});
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isStaging || !isDrawingToolAllowed;
|
||||||
|
}, [isDrawingToolAllowed, isStaging, isTransforming]);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(toolChanged('move'));
|
dispatch(toolChanged('move'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useHotkeys('v', onClick, { enabled: !isDisabled }, [isDisabled, onClick]);
|
useHotkeys('v', onClick, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, onClick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { isDrawableEntityType } from 'features/controlLayers/store/types';
|
import { isDrawableEntityType } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiRectangleBold } from 'react-icons/pi';
|
import { PiRectangleBold } from 'react-icons/pi';
|
||||||
@ -11,18 +12,24 @@ export const ToolRectButton = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'rect');
|
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'rect');
|
||||||
const isDisabled = useAppSelector((s) => {
|
const isTransforming = useIsTransforming();
|
||||||
const entityType = s.canvasV2.selectedEntityIdentifier?.type;
|
const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging);
|
||||||
const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false;
|
const isDrawingToolAllowed = useAppSelector((s) => {
|
||||||
const isStaging = s.canvasV2.session.isStaging;
|
if (!s.canvasV2.selectedEntityIdentifier?.type) {
|
||||||
return !isDrawingToolAllowed || isStaging || s.canvasV2.tool.isTransforming;
|
return false;
|
||||||
|
}
|
||||||
|
return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isStaging || !isDrawingToolAllowed;
|
||||||
|
}, [isDrawingToolAllowed, isStaging, isTransforming]);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(toolChanged('rect'));
|
dispatch(toolChanged('rect'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useHotkeys('u', onClick, { enabled: !isDisabled }, [isDisabled, onClick]);
|
useHotkeys('u', onClick, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, onClick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import { Button, IconButton } from '@invoke-ai/ui-library';
|
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
|
||||||
import { $transformingEntity } from 'features/controlLayers/store/canvasV2Slice';
|
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiResizeBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
export const ToolTransformButton = memo(() => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const canvasManager = useStore($canvasManager);
|
|
||||||
const transformingEntity = useStore($transformingEntity);
|
|
||||||
const isDisabled = useAppSelector(
|
|
||||||
(s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging
|
|
||||||
);
|
|
||||||
|
|
||||||
const onTransform = useCallback(() => {
|
|
||||||
if (!canvasManager) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
canvasManager.startTransform();
|
|
||||||
}, [canvasManager]);
|
|
||||||
|
|
||||||
const onApplyTransformation = useCallback(() => {
|
|
||||||
if (!canvasManager) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
canvasManager.applyTransform();
|
|
||||||
}, [canvasManager]);
|
|
||||||
|
|
||||||
const onCancelTransformation = useCallback(() => {
|
|
||||||
if (!canvasManager) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
canvasManager.cancelTransform();
|
|
||||||
}, [canvasManager]);
|
|
||||||
|
|
||||||
useHotkeys(['ctrl+t', 'meta+t'], onTransform, { enabled: !isDisabled }, [isDisabled, onTransform]);
|
|
||||||
|
|
||||||
if (transformingEntity) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button onClick={onApplyTransformation}>{t('common.apply')}</Button>
|
|
||||||
<Button onClick={onCancelTransformation}>{t('common.cancel')}</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('controlLayers.tool.transform')} (Ctrl+T)`}
|
|
||||||
tooltip={`${t('controlLayers.tool.transform')} (Ctrl+T)`}
|
|
||||||
icon={<PiResizeBold />}
|
|
||||||
variant="solid"
|
|
||||||
onClick={onTransform}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ToolTransformButton.displayName = 'ToolTransformButton';
|
|
@ -1,7 +1,8 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiHandBold } from 'react-icons/pi';
|
import { PiHandBold } from 'react-icons/pi';
|
||||||
@ -9,13 +10,17 @@ import { PiHandBold } from 'react-icons/pi';
|
|||||||
export const ToolViewButton = memo(() => {
|
export const ToolViewButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const isTransforming = useIsTransforming();
|
||||||
|
const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging);
|
||||||
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'view');
|
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'view');
|
||||||
const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming);
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isStaging;
|
||||||
|
}, [isStaging, isTransforming]);
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(toolChanged('view'));
|
dispatch(toolChanged('view'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useHotkeys('h', onClick, { enabled: !isDisabled }, [onClick]);
|
useHotkeys('h', onClick, { enabled: !isDisabled || isSelected }, [onClick, isSelected, isDisabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
import { Button, ButtonGroup, Flex, Heading } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import {
|
||||||
|
EntityIdentifierContext,
|
||||||
|
useEntityIdentifierContext,
|
||||||
|
} from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
|
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiCheckBold, PiXBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const TransformBox = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
|
const adapter = useEntityAdapter(entityIdentifier);
|
||||||
|
const isProcessing = useStore(adapter.transformer.$isProcessing);
|
||||||
|
|
||||||
|
const applyTransform = useCallback(() => {
|
||||||
|
adapter.transformer.applyTransform();
|
||||||
|
}, [adapter.transformer]);
|
||||||
|
|
||||||
|
const cancelFilter = useCallback(() => {
|
||||||
|
adapter.transformer.stopTransform();
|
||||||
|
}, [adapter.transformer]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
bg="base.800"
|
||||||
|
borderRadius="base"
|
||||||
|
p={4}
|
||||||
|
flexDir="column"
|
||||||
|
gap={4}
|
||||||
|
w={420}
|
||||||
|
h="auto"
|
||||||
|
shadow="dark-lg"
|
||||||
|
transitionProperty="height"
|
||||||
|
transitionDuration="normal"
|
||||||
|
>
|
||||||
|
<Heading size="md" color="base.300" userSelect="none">
|
||||||
|
{t('controlLayers.tool.transform')}
|
||||||
|
</Heading>
|
||||||
|
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
|
||||||
|
<Button
|
||||||
|
leftIcon={<PiCheckBold />}
|
||||||
|
onClick={applyTransform}
|
||||||
|
isLoading={isProcessing}
|
||||||
|
loadingText={t('common.apply')}
|
||||||
|
>
|
||||||
|
{t('common.apply')}
|
||||||
|
</Button>
|
||||||
|
<Button leftIcon={<PiXBold />} onClick={cancelFilter} isLoading={isProcessing} loadingText={t('common.cancel')}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
TransformBox.displayName = 'Transform';
|
||||||
|
|
||||||
|
export const Transform = () => {
|
||||||
|
const canvasManager = useCanvasManager();
|
||||||
|
const transformingEntity = useStore(canvasManager.stateApi.$transformingEntity);
|
||||||
|
|
||||||
|
if (!transformingEntity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EntityIdentifierContext.Provider value={transformingEntity}>
|
||||||
|
<TransformBox />
|
||||||
|
</EntityIdentifierContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
|
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiFrameCornersBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
export const CanvasEntityMenuItemsTransform = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
|
const canvasManager = useCanvasManager();
|
||||||
|
const adapter = useEntityAdapter(entityIdentifier);
|
||||||
|
const transformingEntity = useStore(canvasManager.stateApi.$transformingEntity);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
adapter.transformer.startTransform();
|
||||||
|
}, [adapter.transformer]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem onClick={onClick} icon={<PiFrameCornersBold />} isDisabled={Boolean(transformingEntity)}>
|
||||||
|
{t('controlLayers.tool.transform')}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CanvasEntityMenuItemsTransform.displayName = 'CanvasEntityMenuItemsTransform';
|
@ -0,0 +1,12 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useIsTransforming = () => {
|
||||||
|
const canvasManager = useCanvasManager();
|
||||||
|
const transformingEntity = useStore(canvasManager.stateApi.$transformingEntity);
|
||||||
|
const isTransforming = useMemo(() => {
|
||||||
|
return Boolean(transformingEntity);
|
||||||
|
}, [transformingEntity]);
|
||||||
|
return isTransforming;
|
||||||
|
};
|
@ -6,6 +6,7 @@ import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskA
|
|||||||
import {
|
import {
|
||||||
$isDrawing,
|
$isDrawing,
|
||||||
$isMouseDown,
|
$isMouseDown,
|
||||||
|
$isProcessingTransform,
|
||||||
$lastAddedPoint,
|
$lastAddedPoint,
|
||||||
$lastCursorPos,
|
$lastCursorPos,
|
||||||
$lastMouseDownPos,
|
$lastMouseDownPos,
|
||||||
@ -230,6 +231,7 @@ export class CanvasStateApiModule {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$transformingEntity = $transformingEntity;
|
$transformingEntity = $transformingEntity;
|
||||||
|
$isProcessingTransform = $isProcessingTransform;
|
||||||
|
|
||||||
$toolState: WritableAtom<CanvasV2State['tool']> = atom();
|
$toolState: WritableAtom<CanvasV2State['tool']> = atom();
|
||||||
$currentFill: WritableAtom<RgbaColor> = atom();
|
$currentFill: WritableAtom<RgbaColor> = atom();
|
||||||
|
@ -7,6 +7,7 @@ import type { Coordinate, Rect } from 'features/controlLayers/store/types';
|
|||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { GroupConfig } from 'konva/lib/Group';
|
import type { GroupConfig } from 'konva/lib/Group';
|
||||||
import { debounce, get } from 'lodash-es';
|
import { debounce, get } from 'lodash-es';
|
||||||
|
import { atom } from 'nanostores';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,6 +90,8 @@ export class CanvasTransformer {
|
|||||||
*/
|
*/
|
||||||
isTransformEnabled: boolean = false;
|
isTransformEnabled: boolean = false;
|
||||||
|
|
||||||
|
$isProcessing = atom(false);
|
||||||
|
|
||||||
konva: {
|
konva: {
|
||||||
transformer: Konva.Transformer;
|
transformer: Konva.Transformer;
|
||||||
proxyRect: Konva.Rect;
|
proxyRect: Konva.Rect;
|
||||||
@ -493,13 +496,14 @@ export class CanvasTransformer {
|
|||||||
startTransform = () => {
|
startTransform = () => {
|
||||||
this.log.debug('Starting transform');
|
this.log.debug('Starting transform');
|
||||||
this.isTransforming = true;
|
this.isTransforming = true;
|
||||||
|
this.manager.stateApi.setTool('move')
|
||||||
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or
|
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or
|
||||||
// interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening
|
// interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening
|
||||||
// when the view tool is selected
|
// when the view tool is selected
|
||||||
const shouldListen = this.manager.stateApi.getToolState().selected !== 'view';
|
const shouldListen = this.manager.stateApi.getToolState().selected !== 'view';
|
||||||
this.parent.konva.layer.listening(shouldListen);
|
this.parent.konva.layer.listening(shouldListen);
|
||||||
this.setInteractionMode('all');
|
this.setInteractionMode('all');
|
||||||
|
this.manager.stateApi.$transformingEntity.set(this.parent.getEntityIdentifier());
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -507,6 +511,7 @@ export class CanvasTransformer {
|
|||||||
*/
|
*/
|
||||||
applyTransform = async () => {
|
applyTransform = async () => {
|
||||||
this.log.debug('Applying transform');
|
this.log.debug('Applying transform');
|
||||||
|
this.$isProcessing.set(true);
|
||||||
const rect = this.getRelativeRect();
|
const rect = this.getRelativeRect();
|
||||||
await this.parent.renderer.rasterize({ rect, replaceObjects: true });
|
await this.parent.renderer.rasterize({ rect, replaceObjects: true });
|
||||||
this.requestRectCalculation();
|
this.requestRectCalculation();
|
||||||
@ -530,6 +535,8 @@ export class CanvasTransformer {
|
|||||||
this.updatePosition();
|
this.updatePosition();
|
||||||
this.updateBbox();
|
this.updateBbox();
|
||||||
this.syncInteractionState();
|
this.syncInteractionState();
|
||||||
|
this.manager.stateApi.$transformingEntity.set(null);
|
||||||
|
this.$isProcessing.set(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -630,6 +630,7 @@ export const $lastMouseDownPos = atom<Coordinate | null>(null);
|
|||||||
export const $lastCursorPos = atom<Coordinate | null>(null);
|
export const $lastCursorPos = atom<Coordinate | null>(null);
|
||||||
export const $spaceKey = atom<boolean>(false);
|
export const $spaceKey = atom<boolean>(false);
|
||||||
export const $transformingEntity = atom<CanvasEntityIdentifier | null>(null);
|
export const $transformingEntity = atom<CanvasEntityIdentifier | null>(null);
|
||||||
|
export const $isProcessingTransform = atom<boolean>(false);
|
||||||
|
|
||||||
export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {
|
export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {
|
||||||
name: canvasV2Slice.name,
|
name: canvasV2Slice.name,
|
||||||
|
Loading…
Reference in New Issue
Block a user