From e4376e21dda4b1302c14f02b7c5bab63b25083d2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:08:59 +1000 Subject: [PATCH] feat(ui): split up tool chooser buttons Prep for distinct toolbars for generation vs canvas modes --- .../components/BboxToolButton.tsx | 33 +++ .../components/BrushToolButton.tsx | 39 ++++ .../components/EraserToolButton.tsx | 39 ++++ .../components/MoveToolButton.tsx | 35 +++ .../components/RectToolButton.tsx | 39 ++++ .../controlLayers/components/ToolChooser.tsx | 208 ++---------------- .../components/ViewToolButton.tsx | 32 +++ .../hooks/useCanvasDeleteLayerHotkey.ts | 50 +++++ .../hooks/useCanvasResetLayerHotkey.ts | 53 +++++ .../src/features/controlLayers/store/types.ts | 8 +- 10 files changed, 344 insertions(+), 192 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx new file mode 100644 index 0000000000..70440f10ac --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx @@ -0,0 +1,33 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiBoundingBoxBold } from 'react-icons/pi'; + +export const BboxToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'bbox'); + + const onClick = useCallback(() => { + dispatch(toolChanged('bbox')); + }, [dispatch]); + + useHotkeys('q', onClick, [onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +BboxToolButton.displayName = 'BboxToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx new file mode 100644 index 0000000000..0dcaa7fa7c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx @@ -0,0 +1,39 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { isDrawableEntityType } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiPaintBrushBold } from 'react-icons/pi'; + +export const BrushToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'brush'); + const isDisabled = useAppSelector((s) => { + const entityType = s.canvasV2.selectedEntityIdentifier?.type; + const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false; + const isStaging = s.canvasV2.session.isStaging; + return !isDrawingToolAllowed || isStaging; + }); + + const onClick = useCallback(() => { + dispatch(toolChanged('brush')); + }, [dispatch]); + + useHotkeys('b', onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +BrushToolButton.displayName = 'BrushToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx new file mode 100644 index 0000000000..698b37c81f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx @@ -0,0 +1,39 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { isDrawableEntityType } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiEraserBold } from 'react-icons/pi'; + +export const EraserToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eraser'); + const isDisabled = useAppSelector((s) => { + const entityType = s.canvasV2.selectedEntityIdentifier?.type; + const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false; + const isStaging = s.canvasV2.session.isStaging; + return !isDrawingToolAllowed || isStaging; + }); + + const onClick = useCallback(() => { + dispatch(toolChanged('eraser')); + }, [dispatch]); + + useHotkeys('e', onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +EraserToolButton.displayName = 'EraserToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx new file mode 100644 index 0000000000..48dcfeb247 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx @@ -0,0 +1,35 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiCursorBold } from 'react-icons/pi'; + +export const MoveToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'move'); + const isDisabled = useAppSelector( + (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging + ); + + const onClick = useCallback(() => { + dispatch(toolChanged('move')); + }, [dispatch]); + + useHotkeys('v', onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +MoveToolButton.displayName = 'MoveToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx new file mode 100644 index 0000000000..4a8ccadd09 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx @@ -0,0 +1,39 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { isDrawableEntityType } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiRectangleBold } from 'react-icons/pi'; + +export const RectToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'rect'); + const isDisabled = useAppSelector((s) => { + const entityType = s.canvasV2.selectedEntityIdentifier?.type; + const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false; + const isStaging = s.canvasV2.session.isStaging; + return !isDrawingToolAllowed || isStaging; + }); + + const onClick = useCallback(() => { + dispatch(toolChanged('rect')); + }, [dispatch]); + + useHotkeys('u', onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +RectToolButton.displayName = 'RectToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index dc07df3581..e1bfe85a1f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -1,199 +1,25 @@ -import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - caDeleted, - imReset, - ipaDeleted, - layerDeleted, - layerReset, - rgDeleted, - rgReset, - selectCanvasV2Slice, - toolChanged, -} from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; -import { useCallback, useMemo } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiBoundingBoxBold, - PiCursorBold, - PiEraserBold, - PiHandBold, - PiPaintBrushBold, - PiRectangleBold, -} from 'react-icons/pi'; - -const DRAWING_TOOL_TYPES = ['layer', 'regional_guidance', 'inpaint_mask']; - -const getIsDrawingToolEnabled = (entityIdentifier: CanvasEntityIdentifier | null) => { - if (!entityIdentifier) { - return false; - } - return DRAWING_TOOL_TYPES.includes(entityIdentifier.type); -}; - -const selectSelectedEntityIdentifier = createMemoizedSelector( - selectCanvasV2Slice, - (canvasV2State) => canvasV2State.selectedEntityIdentifier -); +import { ButtonGroup } from '@invoke-ai/ui-library'; +import { BboxToolButton } from 'features/controlLayers/components/BboxToolButton'; +import { BrushToolButton } from 'features/controlLayers/components/BrushToolButton'; +import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton'; +import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton'; +import { RectToolButton } from 'features/controlLayers/components/RectToolButton'; +import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton'; +import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey'; +import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey'; export const ToolChooser: React.FC = () => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); - const isDrawingToolDisabled = useMemo( - () => !getIsDrawingToolEnabled(selectedEntityIdentifier), - [selectedEntityIdentifier] - ); - const isMoveToolDisabled = useMemo(() => selectedEntityIdentifier === null, [selectedEntityIdentifier]); - const tool = useAppSelector((s) => s.canvasV2.tool.selected); - - const setToolToBrush = useCallback(() => { - dispatch(toolChanged('brush')); - }, [dispatch]); - useHotkeys('b', setToolToBrush, { enabled: !isDrawingToolDisabled && !isStaging }, [ - isDrawingToolDisabled, - isStaging, - setToolToBrush, - ]); - const setToolToEraser = useCallback(() => { - dispatch(toolChanged('eraser')); - }, [dispatch]); - useHotkeys('e', setToolToEraser, { enabled: !isDrawingToolDisabled && !isStaging }, [ - isDrawingToolDisabled, - isStaging, - setToolToEraser, - ]); - const setToolToRect = useCallback(() => { - dispatch(toolChanged('rect')); - }, [dispatch]); - useHotkeys('u', setToolToRect, { enabled: !isDrawingToolDisabled && !isStaging }, [ - isDrawingToolDisabled, - isStaging, - setToolToRect, - ]); - const setToolToMove = useCallback(() => { - dispatch(toolChanged('move')); - }, [dispatch]); - useHotkeys('v', setToolToMove, { enabled: !isMoveToolDisabled && !isStaging }, [ - isMoveToolDisabled, - isStaging, - setToolToMove, - ]); - const setToolToView = useCallback(() => { - dispatch(toolChanged('view')); - }, [dispatch]); - useHotkeys('h', setToolToView, [setToolToView]); - const setToolToBbox = useCallback(() => { - dispatch(toolChanged('bbox')); - }, [dispatch]); - useHotkeys('q', setToolToBbox, [setToolToBbox]); - - const resetSelectedLayer = useCallback(() => { - if (selectedEntityIdentifier === null) { - return; - } - const { type, id } = selectedEntityIdentifier; - if (type === 'layer') { - dispatch(layerReset({ id })); - } - if (type === 'regional_guidance') { - dispatch(rgReset({ id })); - } - if (type === 'inpaint_mask') { - dispatch(imReset()); - } - }, [dispatch, selectedEntityIdentifier]); - const isResetEnabled = useMemo( - () => - (!isStaging && selectedEntityIdentifier?.type === 'layer') || - selectedEntityIdentifier?.type === 'regional_guidance' || - selectedEntityIdentifier?.type === 'inpaint_mask', - [isStaging, selectedEntityIdentifier?.type] - ); - useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [ - isResetEnabled, - isStaging, - resetSelectedLayer, - ]); - - const deleteSelectedLayer = useCallback(() => { - if (selectedEntityIdentifier === null) { - return; - } - const { type, id } = selectedEntityIdentifier; - if (type === 'layer') { - dispatch(layerDeleted({ id })); - } - if (type === 'regional_guidance') { - dispatch(rgDeleted({ id })); - } - if (type === 'control_adapter') { - dispatch(caDeleted({ id })); - } - if (type === 'ip_adapter') { - dispatch(ipaDeleted({ id })); - } - }, [dispatch, selectedEntityIdentifier]); - const isDeleteEnabled = useMemo( - () => selectedEntityIdentifier !== null && !isStaging, - [selectedEntityIdentifier, isStaging] - ); - useHotkeys('shift+d', deleteSelectedLayer, { enabled: isDeleteEnabled }, [isDeleteEnabled, deleteSelectedLayer]); + useCanvasResetLayerHotkey(); + useCanvasDeleteLayerHotkey(); return ( - } - variant={tool === 'brush' ? 'solid' : 'outline'} - onClick={setToolToBrush} - isDisabled={isDrawingToolDisabled || isStaging} - /> - } - variant={tool === 'eraser' ? 'solid' : 'outline'} - onClick={setToolToEraser} - isDisabled={isDrawingToolDisabled || isStaging} - /> - } - variant={tool === 'rect' ? 'solid' : 'outline'} - onClick={setToolToRect} - isDisabled={isDrawingToolDisabled || isStaging} - /> - } - variant={tool === 'move' ? 'solid' : 'outline'} - onClick={setToolToMove} - isDisabled={isMoveToolDisabled || isStaging} - /> - } - variant={tool === 'view' ? 'solid' : 'outline'} - onClick={setToolToView} - isDisabled={isStaging} - /> - } - variant={tool === 'bbox' ? 'solid' : 'outline'} - onClick={setToolToBbox} - isDisabled={isStaging} - /> + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx new file mode 100644 index 0000000000..b9f6b1691d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx @@ -0,0 +1,32 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiHandBold } from 'react-icons/pi'; + +export const ViewToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'view'); + const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging); + const onClick = useCallback(() => { + dispatch(toolChanged('view')); + }, [dispatch]); + + useHotkeys('h', onClick, [onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +ViewToolButton.displayName = 'ViewToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts new file mode 100644 index 0000000000..1e2fb57901 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts @@ -0,0 +1,50 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { + caDeleted, + ipaDeleted, + layerDeleted, + rgDeleted, + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import { useCallback, useMemo } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; + +const selectSelectedEntityIdentifier = createMemoizedSelector( + selectCanvasV2Slice, + (canvasV2State) => canvasV2State.selectedEntityIdentifier +); + +export function useCanvasDeleteLayerHotkey() { + useAssertSingleton(useCanvasDeleteLayerHotkey.name); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + + const deleteSelectedLayer = useCallback(() => { + if (selectedEntityIdentifier === null) { + return; + } + const { type, id } = selectedEntityIdentifier; + if (type === 'layer') { + dispatch(layerDeleted({ id })); + } + if (type === 'regional_guidance') { + dispatch(rgDeleted({ id })); + } + if (type === 'control_adapter') { + dispatch(caDeleted({ id })); + } + if (type === 'ip_adapter') { + dispatch(ipaDeleted({ id })); + } + }, [dispatch, selectedEntityIdentifier]); + + const isDeleteEnabled = useMemo( + () => selectedEntityIdentifier !== null && !isStaging, + [selectedEntityIdentifier, isStaging] + ); + + useHotkeys('shift+d', deleteSelectedLayer, { enabled: isDeleteEnabled }, [isDeleteEnabled, deleteSelectedLayer]); +} diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts new file mode 100644 index 0000000000..2d1c3c74f0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts @@ -0,0 +1,53 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { + imReset, + layerReset, + rgReset, + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import { useCallback, useMemo } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; + +const selectSelectedEntityIdentifier = createMemoizedSelector( + selectCanvasV2Slice, + (canvasV2State) => canvasV2State.selectedEntityIdentifier +); + +export function useCanvasResetLayerHotkey() { + useAssertSingleton(useCanvasResetLayerHotkey.name); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + + const resetSelectedLayer = useCallback(() => { + if (selectedEntityIdentifier === null) { + return; + } + const { type, id } = selectedEntityIdentifier; + if (type === 'layer') { + dispatch(layerReset({ id })); + } + if (type === 'regional_guidance') { + dispatch(rgReset({ id })); + } + if (type === 'inpaint_mask') { + dispatch(imReset()); + } + }, [dispatch, selectedEntityIdentifier]); + + const isResetEnabled = useMemo( + () => + (!isStaging && selectedEntityIdentifier?.type === 'layer') || + selectedEntityIdentifier?.type === 'regional_guidance' || + selectedEntityIdentifier?.type === 'inpaint_mask', + [isStaging, selectedEntityIdentifier?.type] + ); + + useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [ + isResetEnabled, + isStaging, + resetSelectedLayer, + ]); +} diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 18c8928940..f31ce9e998 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -655,7 +655,7 @@ const zImageFill = z.object({ }); const zFill = z.discriminatedUnion('type', [zColorFill, zImageFill]); const zInpaintMaskEntity = z.object({ - id: zId, + id: z.literal('inpaint_mask'), type: z.literal('inpaint_mask'), isEnabled: z.boolean(), x: z.number(), @@ -945,3 +945,9 @@ export function isDrawableEntityAdapter( ): adapter is CanvasLayer | CanvasRegion | CanvasInpaintMask { return adapter instanceof CanvasLayer || adapter instanceof CanvasRegion || adapter instanceof CanvasInpaintMask; } + +export function isDrawableEntityType( + entityType: CanvasEntity['type'] +): entityType is 'layer' | 'regional_guidance' | 'inpaint_mask' { + return entityType === 'layer' || entityType === 'regional_guidance' || entityType === 'inpaint_mask'; +}