tidy(ui): massive cleanup

- create a context for entity identifiers, massively simplifying UI for each entity int he list
- consolidate common redux actions
- remove now-unused code
This commit is contained in:
psychedelicious 2024-08-06 21:27:00 +10:00
parent 8436a44973
commit 894b8a29b9
45 changed files with 718 additions and 1028 deletions

View File

@ -4,8 +4,8 @@ import type { AppDispatch, RootState } from 'app/store/store';
import { import {
caImageChanged, caImageChanged,
caProcessedImageChanged, caProcessedImageChanged,
entityDeleted,
ipaImageChanged, ipaImageChanged,
layerDeleted,
} from 'features/controlLayers/store/canvasV2Slice'; } from 'features/controlLayers/store/canvasV2Slice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
@ -66,7 +66,7 @@ const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im
} }
} }
if (shouldDelete) { if (shouldDelete) {
dispatch(layerDeleted({ id })); dispatch(entityDeleted({ entityIdentifier: { id, type: 'layer' } }));
} }
}); });
}; };

View File

@ -1,6 +1,11 @@
import { logger } from 'app/logging/logger'; import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { caIsEnabledToggled, loraDeleted, modelChanged, vaeSelected } from 'features/controlLayers/store/canvasV2Slice'; import {
entityIsEnabledToggled,
loraDeleted,
modelChanged,
vaeSelected,
} from 'features/controlLayers/store/canvasV2Slice';
import { modelSelected } from 'features/parameters/store/actions'; import { modelSelected } from 'features/parameters/store/actions';
import { zParameterModel } from 'features/parameters/types/parameterSchemas'; import { zParameterModel } from 'features/parameters/types/parameterSchemas';
import { toast } from 'features/toast/toast'; import { toast } from 'features/toast/toast';
@ -49,7 +54,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
if (ca.model?.base !== newBaseModel) { if (ca.model?.base !== newBaseModel) {
modelsCleared += 1; modelsCleared += 1;
if (ca.isEnabled) { if (ca.isEnabled) {
dispatch(caIsEnabledToggled({ id: ca.id })); dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } }));
} }
} }
}); });

View File

@ -1,28 +1,26 @@
import { useDisclosure } from '@invoke-ai/ui-library'; import { useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CAHeader } from 'features/controlLayers/components/ControlAdapter/CAEntityHeader'; import { CAHeader } from 'features/controlLayers/components/ControlAdapter/CAEntityHeader';
import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings'; import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { memo, useCallback } from 'react'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
type Props = { type Props = {
id: string; id: string;
}; };
export const CA = memo(({ id }: Props) => { export const CA = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'control_adapter' }), [id]);
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id, type: 'control_adapter' }));
}, [dispatch, id]);
return ( return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect}> <EntityIdentifierContext.Provider value={entityIdentifier}>
<CAHeader id={id} isSelected={isSelected} onToggleVisibility={onToggle} /> <CanvasEntityContainer>
<CAHeader onToggleVisibility={onToggle} />
{isOpen && <CASettings id={id} />} {isOpen && <CASettings id={id} />}
</CanvasEntityContainer> </CanvasEntityContainer>
</EntityIdentifierContext.Provider>
); );
}); });

View File

@ -1,84 +1,14 @@
import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { Menu, MenuList } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { import { memo } from 'react';
caDeleted,
caMovedBackwardOne,
caMovedForwardOne,
caMovedToBack,
caMovedToFront,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowDownBold,
PiArrowLineDownBold,
PiArrowLineUpBold,
PiArrowUpBold,
PiTrashSimpleBold,
} from 'react-icons/pi';
type Props = {
id: string;
};
export const CAActionsMenu = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const ca = selectCAOrThrow(canvasV2, id);
const caIndex = canvasV2.controlAdapters.entities.indexOf(ca);
const caCount = canvasV2.controlAdapters.entities.length;
return {
canMoveForward: caIndex < caCount - 1,
canMoveBackward: caIndex > 0,
canMoveToFront: caIndex < caCount - 1,
canMoveToBack: caIndex > 0,
};
}),
[id]
);
const validActions = useAppSelector(selectValidActions);
const onDelete = useCallback(() => {
dispatch(caDeleted({ id }));
}, [dispatch, id]);
const moveForwardOne = useCallback(() => {
dispatch(caMovedForwardOne({ id }));
}, [dispatch, id]);
const moveToFront = useCallback(() => {
dispatch(caMovedToFront({ id }));
}, [dispatch, id]);
const moveBackwardOne = useCallback(() => {
dispatch(caMovedBackwardOne({ id }));
}, [dispatch, id]);
const moveToBack = useCallback(() => {
dispatch(caMovedToBack({ id }));
}, [dispatch, id]);
export const CAActionsMenu = memo(() => {
return ( return (
<Menu> <Menu>
<CanvasEntityMenuButton /> <CanvasEntityMenuButton />
<MenuList> <MenuList>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}> <CanvasEntityActionMenuItems />
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={moveForwardOne} isDisabled={!validActions.canMoveForward} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={moveBackwardOne} isDisabled={!validActions.canMoveBackward} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
<MenuItem onClick={onDelete} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
); );

View File

@ -1,41 +1,25 @@
import { Spacer } from '@invoke-ai/ui-library'; import { Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { CAActionsMenu } from 'features/controlLayers/components/ControlAdapter/CAActionsMenu'; import { CAActionsMenu } from 'features/controlLayers/components/ControlAdapter/CAActionsMenu';
import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter'; import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter';
import { caDeleted, caIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react';
import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
id: string;
isSelected: boolean;
onToggleVisibility: () => void; onToggleVisibility: () => void;
}; };
export const CAHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { export const CAHeader = memo(({ onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id).isEnabled);
const onToggleIsEnabled = useCallback(() => {
dispatch(caIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(caDeleted({ id }));
}, [dispatch, id]);
return ( return (
<CanvasEntityHeader onToggle={onToggleVisibility}> <CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} /> <CanvasEntityEnabledToggle />
<CanvasEntityTitle title={t('controlLayers.globalControlAdapter')} isSelected={isSelected} /> <CanvasEntityTitle />
<Spacer /> <Spacer />
<CAOpacityAndFilter id={id} /> <CAOpacityAndFilter />
<CAActionsMenu id={id} /> <CAActionsMenu />
<CanvasEntityDeleteButton onDelete={onDelete} /> <CanvasEntityDeleteButton />
</CanvasEntityHeader> </CanvasEntityHeader>
); );
}); });

View File

@ -14,6 +14,7 @@ import {
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
@ -21,14 +22,11 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi'; import { PiDropHalfFill } from 'react-icons/pi';
type Props = {
id: string;
};
const marks = [0, 25, 50, 75, 100]; const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`; const formatPct = (v: number | string) => `${v} %`;
export const CAOpacityAndFilter = memo(({ id }: Props) => { export const CAOpacityAndFilter = memo(() => {
const { id } = useEntityIdentifierContext();
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const opacity = useAppSelector((s) => Math.round(selectCAOrThrow(s.canvasV2, id).opacity * 100)); const opacity = useAppSelector((s) => Math.round(selectCAOrThrow(s.canvasV2, id).opacity * 100));

View File

@ -15,7 +15,7 @@ export const IPA = memo(({ id }: Props) => {
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => { const onSelect = useCallback(() => {
dispatch(entitySelected({ id, type: 'ip_adapter' })); dispatch(entitySelected({ entityIdentifier: { id, type: 'ip_adapter' } }));
}, [dispatch, id]); }, [dispatch, id]);
return ( return (

View File

@ -1,25 +1,21 @@
import { useDisclosure } from '@invoke-ai/ui-library'; import { useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { IMHeader } from 'features/controlLayers/components/InpaintMask/IMHeader'; import { IMHeader } from 'features/controlLayers/components/InpaintMask/IMHeader';
import { IMSettings } from 'features/controlLayers/components/InpaintMask/IMSettings'; import { IMSettings } from 'features/controlLayers/components/InpaintMask/IMSettings';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { memo, useCallback } from 'react'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
export const IM = memo(() => { export const IM = memo(() => {
const dispatch = useAppDispatch(); const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id: 'inpaint_mask', type: 'inpaint_mask' }), []);
const selectedBorderColor = useAppSelector((s) => rgbColorToString(s.canvasV2.inpaintMask.fill));
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'inpaint_mask');
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id: 'inpaint_mask', type: 'inpaint_mask' }));
}, [dispatch]);
return ( return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect} selectedBorderColor={selectedBorderColor}> <EntityIdentifierContext.Provider value={entityIdentifier}>
<IMHeader isSelected={isSelected} onToggleVisibility={onToggle} /> <CanvasEntityContainer>
<IMHeader onToggleVisibility={onToggle} />
{isOpen && <IMSettings />} {isOpen && <IMSettings />}
</CanvasEntityContainer> </CanvasEntityContainer>
</EntityIdentifierContext.Provider>
); );
}); });

View File

@ -1,25 +1,14 @@
import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { Menu, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { imReset } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
export const IMActionsMenu = memo(() => { export const IMActionsMenu = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onReset = useCallback(() => {
dispatch(imReset());
}, [dispatch]);
return ( return (
<Menu> <Menu>
<CanvasEntityMenuButton /> <CanvasEntityMenuButton />
<MenuList> <MenuList>
<MenuItem onClick={onReset} icon={<PiArrowCounterClockwiseBold />}> <CanvasEntityActionMenuItems />
{t('accessibility.reset')}
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
); );

View File

@ -1,32 +1,21 @@
import { Spacer } from '@invoke-ai/ui-library'; import { Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { IMActionsMenu } from 'features/controlLayers/components/InpaintMask/IMActionsMenu'; import { IMActionsMenu } from 'features/controlLayers/components/InpaintMask/IMActionsMenu';
import { imIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { IMMaskFillColorPicker } from './IMMaskFillColorPicker'; import { IMMaskFillColorPicker } from './IMMaskFillColorPicker';
type Props = { type Props = {
isSelected: boolean;
onToggleVisibility: () => void; onToggleVisibility: () => void;
}; };
export const IMHeader = memo(({ isSelected, onToggleVisibility }: Props) => { export const IMHeader = memo(({ onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => s.canvasV2.inpaintMask.isEnabled);
const onToggleIsEnabled = useCallback(() => {
dispatch(imIsEnabledToggled());
}, [dispatch]);
return ( return (
<CanvasEntityHeader onToggle={onToggleVisibility}> <CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} /> <CanvasEntityEnabledToggle />
<CanvasEntityTitle title={t('controlLayers.inpaintMask')} isSelected={isSelected} /> <CanvasEntityTitle />
<Spacer /> <Spacer />
<IMMaskFillColorPicker /> <IMMaskFillColorPicker />
<IMActionsMenu /> <IMActionsMenu />

View File

@ -1,35 +1,33 @@
import { useDisclosure } from '@invoke-ai/ui-library'; import { useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { LayerHeader } from 'features/controlLayers/components/Layer/LayerHeader'; import { LayerHeader } from 'features/controlLayers/components/Layer/LayerHeader';
import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { LayerImageDropData } from 'features/dnd/types'; import type { LayerImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react'; import { memo, useMemo } from 'react';
type Props = { type Props = {
id: string; id: string;
}; };
export const Layer = memo(({ id }: Props) => { export const Layer = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'layer' }), [id]);
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id, type: 'layer' }));
}, [dispatch, id]);
const droppableData = useMemo<LayerImageDropData>( const droppableData = useMemo<LayerImageDropData>(
() => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }), () => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }),
[id] [id]
); );
return ( return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect}> <EntityIdentifierContext.Provider value={entityIdentifier}>
<LayerHeader id={id} onToggleVisibility={onToggle} isSelected={isSelected} /> <CanvasEntityContainer>
{isOpen && <LayerSettings id={id} />} <LayerHeader onToggleVisibility={onToggle} />
{isOpen && <LayerSettings />}
<IAIDroppable data={droppableData} /> <IAIDroppable data={droppableData} />
</CanvasEntityContainer> </CanvasEntityContainer>
</EntityIdentifierContext.Provider>
); );
}); });

View File

@ -1,84 +1,14 @@
import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { Menu, MenuList } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { import { memo } from 'react';
layerDeleted,
layerMovedBackwardOne,
layerMovedForwardOne,
layerMovedToBack,
layerMovedToFront,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowDownBold,
PiArrowLineDownBold,
PiArrowLineUpBold,
PiArrowUpBold,
PiTrashSimpleBold,
} from 'react-icons/pi';
type Props = {
id: string;
};
export const LayerActionsMenu = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const layer = selectLayerOrThrow(canvasV2, id);
const layerIndex = canvasV2.layers.entities.indexOf(layer);
const layerCount = canvasV2.layers.entities.length;
return {
canMoveForward: layerIndex < layerCount - 1,
canMoveBackward: layerIndex > 0,
canMoveToFront: layerIndex < layerCount - 1,
canMoveToBack: layerIndex > 0,
};
}),
[id]
);
const validActions = useAppSelector(selectValidActions);
const onDelete = useCallback(() => {
dispatch(layerDeleted({ id }));
}, [dispatch, id]);
const moveForwardOne = useCallback(() => {
dispatch(layerMovedForwardOne({ id }));
}, [dispatch, id]);
const moveToFront = useCallback(() => {
dispatch(layerMovedToFront({ id }));
}, [dispatch, id]);
const moveBackwardOne = useCallback(() => {
dispatch(layerMovedBackwardOne({ id }));
}, [dispatch, id]);
const moveToBack = useCallback(() => {
dispatch(layerMovedToBack({ id }));
}, [dispatch, id]);
export const LayerActionsMenu = memo(() => {
return ( return (
<Menu> <Menu>
<CanvasEntityMenuButton /> <CanvasEntityMenuButton />
<MenuList> <MenuList>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}> <CanvasEntityActionMenuItems />
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={moveForwardOne} isDisabled={!validActions.canMoveForward} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={moveBackwardOne} isDisabled={!validActions.canMoveBackward} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
<MenuItem onClick={onDelete} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
); );

View File

@ -1,46 +1,26 @@
import { Spacer } from '@invoke-ai/ui-library'; import { Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu'; import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu';
import { layerDeleted, layerIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react';
import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { LayerOpacity } from './LayerOpacity'; import { LayerOpacity } from './LayerOpacity';
type Props = { type Props = {
id: string;
isSelected: boolean;
onToggleVisibility: () => void; onToggleVisibility: () => void;
}; };
export const LayerHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { export const LayerHeader = memo(({ onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).isEnabled);
const objectCount = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).objects.length);
const onToggleIsEnabled = useCallback(() => {
dispatch(layerIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(layerDeleted({ id }));
}, [dispatch, id]);
const title = useMemo(() => {
return `${t('controlLayers.layer')} (${t('controlLayers.objects', { count: objectCount })})`;
}, [objectCount, t]);
return ( return (
<CanvasEntityHeader onToggle={onToggleVisibility}> <CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} /> <CanvasEntityEnabledToggle />
<CanvasEntityTitle title={title} isSelected={isSelected} /> <CanvasEntityTitle />
<Spacer /> <Spacer />
<LayerOpacity id={id} /> <LayerOpacity />
<LayerActionsMenu id={id} /> <LayerActionsMenu />
<CanvasEntityDeleteButton onDelete={onDelete} /> <CanvasEntityDeleteButton />
</CanvasEntityHeader> </CanvasEntityHeader>
); );
}); });

View File

@ -13,28 +13,26 @@ import {
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { layerOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; import { layerOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi'; import { PiDropHalfFill } from 'react-icons/pi';
type Props = {
id: string;
};
const marks = [0, 25, 50, 75, 100]; const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`; const formatPct = (v: number | string) => `${v} %`;
export const LayerOpacity = memo(({ id }: Props) => { export const LayerOpacity = memo(() => {
const entityIdentifier = useEntityIdentifierContext();
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.canvasV2, id).opacity * 100)); const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.canvasV2, entityIdentifier.id).opacity * 100));
const onChangeOpacity = useCallback( const onChangeOpacity = useCallback(
(v: number) => { (v: number) => {
dispatch(layerOpacityChanged({ id, opacity: v / 100 })); dispatch(layerOpacityChanged({ id: entityIdentifier.id, opacity: v / 100 }));
}, },
[dispatch, id] [dispatch, entityIdentifier.id]
); );
return ( return (
<Popover isLazy> <Popover isLazy>

View File

@ -1,11 +1,9 @@
import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { memo } from 'react'; import { memo } from 'react';
type Props = { export const LayerSettings = memo(() => {
id: string; const entityIdentifier = useEntityIdentifierContext();
};
export const LayerSettings = memo(({ id }: Props) => {
return <CanvasEntitySettings>PLACEHOLDER</CanvasEntitySettings>; return <CanvasEntitySettings>PLACEHOLDER</CanvasEntitySettings>;
}); });

View File

@ -1,30 +1,25 @@
import { useDisclosure } from '@invoke-ai/ui-library'; import { useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader'; import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader';
import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings'; import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react'; import { memo, useMemo } from 'react';
type Props = { type Props = {
id: string; id: string;
}; };
export const RG = memo(({ id }: Props) => { export const RG = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'regional_guidance' }), [id]);
const selectedBorderColor = useAppSelector((s) => rgbColorToString(selectRGOrThrow(s.canvasV2, id).fill));
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id, type: 'regional_guidance' }));
}, [dispatch, id]);
return ( return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect} selectedBorderColor={selectedBorderColor}> <EntityIdentifierContext.Provider value={entityIdentifier}>
<RGHeader id={id} isSelected={isSelected} onToggleVisibility={onToggle} /> <CanvasEntityContainer>
{isOpen && <RGSettings id={id} />} <RGHeader onToggleVisibility={onToggle} />
{isOpen && <RGSettings />}
</CanvasEntityContainer> </CanvasEntityContainer>
</EntityIdentifierContext.Provider>
); );
}); });

View File

@ -1,37 +1,22 @@
import { Menu, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { Menu, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks'; import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks';
import { import {
rgDeleted,
rgMovedBackwardOne,
rgMovedForwardOne,
rgMovedToBack,
rgMovedToFront,
rgNegativePromptChanged, rgNegativePromptChanged,
rgPositivePromptChanged, rgPositivePromptChanged,
rgReset,
selectCanvasV2Slice, selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice'; } from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { PiPlusBold } from 'react-icons/pi';
PiArrowCounterClockwiseBold,
PiArrowDownBold,
PiArrowLineDownBold,
PiArrowLineUpBold,
PiArrowUpBold,
PiPlusBold,
PiTrashSimpleBold,
} from 'react-icons/pi';
type Props = { export const RGActionsMenu = memo(() => {
id: string; const { id } = useEntityIdentifierContext();
};
export const RGActionsMenu = memo(({ id }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [onAddIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); const [onAddIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id);
@ -39,13 +24,7 @@ export const RGActionsMenu = memo(({ id }: Props) => {
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const rg = selectRGOrThrow(canvasV2, id); const rg = selectRGOrThrow(canvasV2, id);
const rgIndex = canvasV2.regions.entities.indexOf(rg);
const rgCount = canvasV2.regions.entities.length;
return { return {
isMoveForwardOneDisabled: rgIndex < rgCount - 1,
isMoveBackardOneDisabled: rgIndex > 0,
isMoveToFrontDisabled: rgIndex < rgCount - 1,
isMoveToBackDisabled: rgIndex > 0,
isAddPositivePromptDisabled: rg.positivePrompt === null, isAddPositivePromptDisabled: rg.positivePrompt === null,
isAddNegativePromptDisabled: rg.negativePrompt === null, isAddNegativePromptDisabled: rg.negativePrompt === null,
}; };
@ -53,29 +32,11 @@ export const RGActionsMenu = memo(({ id }: Props) => {
[id] [id]
); );
const actions = useAppSelector(selectActionsValidity); const actions = useAppSelector(selectActionsValidity);
const onDelete = useCallback(() => {
dispatch(rgDeleted({ id }));
}, [dispatch, id]);
const onReset = useCallback(() => {
dispatch(rgReset({ id }));
}, [dispatch, id]);
const onMoveForwardOne = useCallback(() => {
dispatch(rgMovedForwardOne({ id }));
}, [dispatch, id]);
const onMoveToFront = useCallback(() => {
dispatch(rgMovedToFront({ id }));
}, [dispatch, id]);
const onMoveBackwardOne = useCallback(() => {
dispatch(rgMovedBackwardOne({ id }));
}, [dispatch, id]);
const onMoveToBack = useCallback(() => {
dispatch(rgMovedToBack({ id }));
}, [dispatch, id]);
const onAddPositivePrompt = useCallback(() => { const onAddPositivePrompt = useCallback(() => {
dispatch(rgPositivePromptChanged({ id, prompt: '' })); dispatch(rgPositivePromptChanged({ id: id, prompt: '' }));
}, [dispatch, id]); }, [dispatch, id]);
const onAddNegativePrompt = useCallback(() => { const onAddNegativePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ id, prompt: '' })); dispatch(rgNegativePromptChanged({ id: id, prompt: '' }));
}, [dispatch, id]); }, [dispatch, id]);
return ( return (
@ -92,25 +53,7 @@ export const RGActionsMenu = memo(({ id }: Props) => {
{t('controlLayers.addIPAdapter')} {t('controlLayers.addIPAdapter')}
</MenuItem> </MenuItem>
<MenuDivider /> <MenuDivider />
<MenuItem onClick={onMoveToFront} isDisabled={actions.isMoveToFrontDisabled} icon={<PiArrowLineUpBold />}> <CanvasEntityActionMenuItems />
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={onMoveForwardOne} isDisabled={actions.isMoveForwardOneDisabled} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={onMoveBackwardOne} isDisabled={actions.isMoveBackardOneDisabled} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={onMoveToBack} isDisabled={actions.isMoveToBackDisabled} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
<MenuDivider />
<MenuItem onClick={onReset} icon={<PiArrowCounterClockwiseBold />}>
{t('accessibility.reset')}
</MenuItem>
<MenuItem onClick={onDelete} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
); );

View File

@ -1,50 +1,41 @@
import { Badge, Spacer } from '@invoke-ai/ui-library'; import { Badge, Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { RGActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RGActionsMenu'; import { RGActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RGActionsMenu';
import { rgDeleted, rgIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo, useCallback } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RGMaskFillColorPicker } from './RGMaskFillColorPicker'; import { RGMaskFillColorPicker } from './RGMaskFillColorPicker';
import { RGSettingsPopover } from './RGSettingsPopover'; import { RGSettingsPopover } from './RGSettingsPopover';
type Props = { type Props = {
id: string;
isSelected: boolean;
onToggleVisibility: () => void; onToggleVisibility: () => void;
}; };
export const RGHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { export const RGHeader = memo(({ onToggleVisibility }: Props) => {
const { id } = useEntityIdentifierContext();
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).isEnabled);
const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative);
const onToggleIsEnabled = useCallback(() => {
dispatch(rgIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(rgDeleted({ id }));
}, [dispatch, id]);
return ( return (
<CanvasEntityHeader onToggle={onToggleVisibility}> <CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} /> <CanvasEntityEnabledToggle />
<CanvasEntityTitle title={t('controlLayers.regionalGuidance')} isSelected={isSelected} /> <CanvasEntityTitle />
<Spacer /> <Spacer />
{autoNegative === 'invert' && ( {autoNegative === 'invert' && (
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none"> <Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
{t('controlLayers.autoNegative')} {t('controlLayers.autoNegative')}
</Badge> </Badge>
)} )}
<RGMaskFillColorPicker id={id} /> <RGMaskFillColorPicker />
<RGSettingsPopover id={id} /> <RGSettingsPopover />
<RGActionsMenu id={id} /> <RGActionsMenu />
<CanvasEntityDeleteButton onDelete={onDelete} /> <CanvasEntityDeleteButton />
</CanvasEntityHeader> </CanvasEntityHeader>
); );
}); });

View File

@ -3,25 +3,23 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker'; import RgbColorPicker from 'common/components/RgbColorPicker';
import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice'; import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import type { RgbColor } from 'react-colorful'; import type { RgbColor } from 'react-colorful';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
type Props = { export const RGMaskFillColorPicker = memo(() => {
id: string; const entityIdentifier = useEntityIdentifierContext();
};
export const RGMaskFillColorPicker = memo(({ id }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const fill = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).fill); const fill = useAppSelector((s) => selectRGOrThrow(s.canvasV2, entityIdentifier.id).fill);
const onChange = useCallback( const onChange = useCallback(
(fill: RgbColor) => { (fill: RgbColor) => {
dispatch(rgFillChanged({ id, fill })); dispatch(rgFillChanged({ id: entityIdentifier.id, fill }));
}, },
[dispatch, id] [dispatch, entityIdentifier.id]
); );
return ( return (
<Popover isLazy> <Popover isLazy>

View File

@ -1,6 +1,7 @@
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons';
import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo } from 'react'; import { memo } from 'react';
@ -8,11 +9,8 @@ import { RGIPAdapters } from './RGIPAdapters';
import { RGNegativePrompt } from './RGNegativePrompt'; import { RGNegativePrompt } from './RGNegativePrompt';
import { RGPositivePrompt } from './RGPositivePrompt'; import { RGPositivePrompt } from './RGPositivePrompt';
type Props = { export const RGSettings = memo(() => {
id: string; const { id } = useEntityIdentifierContext();
};
export const RGSettings = memo(({ id }: Props) => {
const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt !== null); const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt !== null);
const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt !== null); const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt !== null);
const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.length > 0); const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.length > 0);

View File

@ -12,6 +12,7 @@ import {
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgAutoNegativeChanged } from 'features/controlLayers/store/canvasV2Slice'; import { rgAutoNegativeChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
@ -19,19 +20,16 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiGearSixBold } from 'react-icons/pi'; import { PiGearSixBold } from 'react-icons/pi';
type Props = { export const RGSettingsPopover = memo(() => {
id: string; const entityIdentifier = useEntityIdentifierContext();
};
export const RGSettingsPopover = memo(({ id }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, entityIdentifier.id).autoNegative);
const onChange = useCallback( const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => { (e: ChangeEvent<HTMLInputElement>) => {
dispatch(rgAutoNegativeChanged({ id, autoNegative: e.target.checked ? 'invert' : 'off' })); dispatch(rgAutoNegativeChanged({ id: entityIdentifier.id, autoNegative: e.target.checked ? 'invert' : 'off' }));
}, },
[dispatch, id] [dispatch, entityIdentifier.id]
); );
return ( return (

View File

@ -13,10 +13,8 @@ import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanva
export const ToolChooser: React.FC = () => { export const ToolChooser: React.FC = () => {
useCanvasResetLayerHotkey(); useCanvasResetLayerHotkey();
useCanvasDeleteLayerHotkey(); useCanvasDeleteLayerHotkey();
const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive);
const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming); const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming);
if (isCanvasSessionActive) {
return ( return (
<> <>
<ButtonGroup isAttached isDisabled={isTransforming}> <ButtonGroup isAttached isDisabled={isTransforming}>
@ -30,15 +28,4 @@ export const ToolChooser: React.FC = () => {
<TransformToolButton /> <TransformToolButton />
</> </>
); );
}
return (
<ButtonGroup isAttached isDisabled={isTransforming}>
<BrushToolButton />
<EraserToolButton />
<RectToolButton />
<MoveToolButton />
<ViewToolButton />
</ButtonGroup>
);
}; };

View File

@ -0,0 +1,127 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
entityArrangedBackwardOne,
entityArrangedForwardOne,
entityArrangedToBack,
entityArrangedToFront,
entityDeleted,
entityReset,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowCounterClockwiseBold,
PiArrowDownBold,
PiArrowLineDownBold,
PiArrowLineUpBold,
PiArrowUpBold,
PiTrashSimpleBold,
} from 'react-icons/pi';
const getIndexAndCount = (
canvasV2: CanvasV2State,
{ id, type }: CanvasEntityIdentifier
): { index: number; count: number } => {
if (type === 'layer') {
return {
index: canvasV2.layers.entities.findIndex((entity) => entity.id === id),
count: canvasV2.layers.entities.length,
};
} else if (type === 'control_adapter') {
return {
index: canvasV2.controlAdapters.entities.findIndex((entity) => entity.id === id),
count: canvasV2.controlAdapters.entities.length,
};
} else if (type === 'regional_guidance') {
return {
index: canvasV2.regions.entities.findIndex((entity) => entity.id === id),
count: canvasV2.regions.entities.length,
};
} else {
return {
index: -1,
count: 0,
};
}
};
export const CanvasEntityActionMenuItems = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const { index, count } = getIndexAndCount(canvasV2, entityIdentifier);
return {
isArrangeable:
entityIdentifier.type === 'layer' ||
entityIdentifier.type === 'control_adapter' ||
entityIdentifier.type === 'regional_guidance',
isDeleteable: entityIdentifier.type !== 'inpaint_mask',
canMoveForwardOne: index < count - 1,
canMoveBackwardOne: index > 0,
canMoveToFront: index < count - 1,
canMoveToBack: index > 0,
};
}),
[entityIdentifier]
);
const validActions = useAppSelector(selectValidActions);
const deleteEntity = useCallback(() => {
dispatch(entityDeleted({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const resetEntity = useCallback(() => {
dispatch(entityReset({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveForwardOne = useCallback(() => {
dispatch(entityArrangedForwardOne({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveToFront = useCallback(() => {
dispatch(entityArrangedToFront({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveBackwardOne = useCallback(() => {
dispatch(entityArrangedBackwardOne({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveToBack = useCallback(() => {
dispatch(entityArrangedToBack({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<>
{validActions.isArrangeable && (
<>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={moveForwardOne} isDisabled={!validActions.canMoveForwardOne} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={moveBackwardOne} isDisabled={!validActions.canMoveBackwardOne} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
</>
)}
<MenuItem onClick={resetEntity} icon={<PiArrowCounterClockwiseBold />}>
{t('accessibility.reset')}
</MenuItem>
{validActions.isDeleteable && (
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
)}
</>
);
});
CanvasEntityActionMenuItems.displayName = 'CanvasEntityActionMenuItems';

View File

@ -1,22 +1,23 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor';
import { useIsEntitySelected } from 'features/controlLayers/hooks/useIsEntitySelected';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
type Props = PropsWithChildren<{ export const CanvasEntityContainer = memo((props: PropsWithChildren) => {
isSelected: boolean; const dispatch = useAppDispatch();
onSelect: () => void; const entityIdentifier = useEntityIdentifierContext();
selectedBorderColor?: ChakraProps['bg']; const isSelected = useIsEntitySelected(entityIdentifier);
}>; const selectionColor = useEntitySelectionColor(entityIdentifier);
const onClick = useCallback(() => {
export const CanvasEntityContainer = memo((props: Props) => {
const { isSelected, onSelect, selectedBorderColor = 'base.400', children } = props;
const _onSelect = useCallback(() => {
if (isSelected) { if (isSelected) {
return; return;
} }
onSelect(); dispatch(entitySelected({ entityIdentifier }));
}, [isSelected, onSelect]); }, [dispatch, entityIdentifier, isSelected]);
return ( return (
<Flex <Flex
@ -24,12 +25,12 @@ export const CanvasEntityContainer = memo((props: Props) => {
flexDir="column" flexDir="column"
w="full" w="full"
bg={isSelected ? 'base.800' : 'base.850'} bg={isSelected ? 'base.800' : 'base.850'}
onClick={_onSelect} onClick={onClick}
borderInlineStartWidth={5} borderInlineStartWidth={5}
borderColor={isSelected ? selectedBorderColor : 'base.800'} borderColor={isSelected ? selectionColor : 'base.800'}
borderRadius="base" borderRadius="base"
> >
{children} {props.children}
</Flex> </Flex>
); );
}); });

View File

@ -1,13 +1,19 @@
import { IconButton } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { memo } from 'react'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi'; import { PiTrashSimpleBold } from 'react-icons/pi';
type Props = { onDelete: () => void }; export const CanvasEntityDeleteButton = memo(() => {
export const CanvasEntityDeleteButton = memo(({ onDelete }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const onClick = useCallback(() => {
dispatch(entityDeleted({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return ( return (
<IconButton <IconButton
size="sm" size="sm"
@ -15,7 +21,7 @@ export const CanvasEntityDeleteButton = memo(({ onDelete }: Props) => {
aria-label={t('common.delete')} aria-label={t('common.delete')}
tooltip={t('common.delete')} tooltip={t('common.delete')}
icon={<PiTrashSimpleBold />} icon={<PiTrashSimpleBold />}
onClick={onDelete} onClick={onClick}
onDoubleClick={stopPropagation} // double click expands the layer onDoubleClick={stopPropagation} // double click expands the layer
/> />
); );

View File

@ -1,16 +1,21 @@
import { IconButton } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { memo } from 'react'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiCheckBold } from 'react-icons/pi'; import { PiCheckBold } from 'react-icons/pi';
type Props = { export const CanvasEntityEnabledToggle = memo(() => {
isEnabled: boolean;
onToggle: () => void;
};
export const CanvasEntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const isEnabled = useEntityIsEnabled(entityIdentifier);
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
dispatch(entityIsEnabledToggled({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return ( return (
<IconButton <IconButton
@ -19,7 +24,7 @@ export const CanvasEntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) =
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')} tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
variant="outline" variant="outline"
icon={isEnabled ? <PiCheckBold /> : undefined} icon={isEnabled ? <PiCheckBold /> : undefined}
onClick={onToggle} onClick={onClick}
colorScheme="base" colorScheme="base"
onDoubleClick={stopPropagation} // double click expands the layer onDoubleClick={stopPropagation} // double click expands the layer
/> />

View File

@ -1,12 +1,30 @@
import { Text } from '@invoke-ai/ui-library'; import { Text } from '@invoke-ai/ui-library';
import { memo } from 'react'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntitySelected } from 'features/controlLayers/hooks/useIsEntitySelected';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
type Props = { export const CanvasEntityTitle = memo(() => {
title: string; const { t } = useTranslation();
isSelected: boolean; const entityIdentifier = useEntityIdentifierContext();
}; const isSelected = useIsEntitySelected(entityIdentifier);
const title = useMemo(() => {
if (entityIdentifier.type === 'inpaint_mask') {
return t('controlLayers.inpaintMask');
} else if (entityIdentifier.type === 'control_adapter') {
return t('controlLayers.globalControlAdapter');
} else if (entityIdentifier.type === 'layer') {
return t('controlLayers.layer');
} else if (entityIdentifier.type === 'ip_adapter') {
return t('controlLayers.ipAdapter');
} else if (entityIdentifier.type === 'regional_guidance') {
return t('controlLayers.regionalGuidance');
} else {
assert(false, 'Unexpected entity type');
}
}, [entityIdentifier.type, t]);
export const CanvasEntityTitle = memo(({ title, isSelected }: Props) => {
return ( return (
<Text size="sm" fontWeight="semibold" userSelect="none" color={isSelected ? 'base.100' : 'base.300'}> <Text size="sm" fontWeight="semibold" userSelect="none" color={isSelected ? 'base.100' : 'base.300'}>
{title} {title}

View File

@ -0,0 +1,11 @@
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { createContext, useContext } from 'react';
import { assert } from 'tsafe';
export const EntityIdentifierContext = createContext<CanvasEntityIdentifier | null>(null);
export const useEntityIdentifierContext = (): CanvasEntityIdentifier => {
const entityIdentifier = useContext(EntityIdentifierContext);
assert(entityIdentifier, 'useEntityIdentifier must be used within a EntityIdentifierProvider');
return entityIdentifier;
};

View File

@ -2,10 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { import {
caDeleted, entityDeleted,
ipaDeleted,
layerDeleted,
rgDeleted,
selectCanvasV2Slice, selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice'; } from 'features/controlLayers/store/canvasV2Slice';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
@ -26,19 +23,7 @@ export function useCanvasDeleteLayerHotkey() {
if (selectedEntityIdentifier === null) { if (selectedEntityIdentifier === null) {
return; return;
} }
const { type, id } = selectedEntityIdentifier; dispatch(entityDeleted({ entityIdentifier: 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]); }, [dispatch, selectedEntityIdentifier]);
const isDeleteEnabled = useMemo( const isDeleteEnabled = useMemo(

View File

@ -2,9 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { import {
imReset, entityReset,
layerReset,
rgReset,
selectCanvasV2Slice, selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice'; } from 'features/controlLayers/store/canvasV2Slice';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
@ -25,16 +23,7 @@ export function useCanvasResetLayerHotkey() {
if (selectedEntityIdentifier === null) { if (selectedEntityIdentifier === null) {
return; return;
} }
const { type, id } = selectedEntityIdentifier; dispatch(entityReset({ entityIdentifier: selectedEntityIdentifier }));
if (type === 'layer') {
dispatch(layerReset({ id }));
}
if (type === 'regional_guidance') {
dispatch(rgReset({ id }));
}
if (type === 'inpaint_mask') {
dispatch(imReset());
}
}, [dispatch, selectedEntityIdentifier]); }, [dispatch, selectedEntityIdentifier]);
const isResetEnabled = useMemo( const isResetEnabled = useMemo(

View File

@ -0,0 +1,22 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntityIsEnabled = (entityIdentifier: CanvasEntityIdentifier) => {
const selectIsEnabled = useMemo(
() =>
createSelector(selectCanvasV2Slice, (canvasV2) => {
const entity = selectEntity(canvasV2, entityIdentifier);
if (!entity) {
return false;
} else {
return entity.isEnabled;
}
}),
[entityIdentifier]
);
const isEnabled = useAppSelector(selectIsEnabled);
return isEnabled;
};

View File

@ -0,0 +1,27 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntitySelectionColor = (entityIdentifier: CanvasEntityIdentifier) => {
const selectSelectionColor = useMemo(
() =>
createSelector(selectCanvasV2Slice, (canvasV2) => {
const entity = selectEntity(canvasV2, entityIdentifier);
if (!entity) {
return 'base.400';
} else if (entity.type === 'inpaint_mask') {
return rgbColorToString(entity.fill);
} else if (entity.type === 'regional_guidance') {
return rgbColorToString(entity.fill);
} else {
return 'base.400';
}
}),
[entityIdentifier]
);
const selectionColor = useAppSelector(selectSelectionColor);
return selectionColor;
};

View File

@ -0,0 +1,12 @@
import { useAppSelector } from 'app/store/storeHooks';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useIsEntitySelected = (entityIdentifier: CanvasEntityIdentifier) => {
const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier);
const isSelected = useMemo(() => {
return selectedEntityIdentifier?.id === entityIdentifier.id;
}, [selectedEntityIdentifier, entityIdentifier.id]);
return isSelected;
};

View File

@ -218,16 +218,9 @@ export class CanvasBbox {
} }
render() { render() {
const session = this.manager.stateApi.getSession();
const bbox = this.manager.stateApi.getBbox(); const bbox = this.manager.stateApi.getBbox();
const toolState = this.manager.stateApi.getToolState(); const toolState = this.manager.stateApi.getToolState();
if (!session.isActive) {
this.konva.group.listening(false);
this.konva.group.visible(false);
return;
}
this.konva.group.visible(true); this.konva.group.visible(true);
this.konva.group.listening(toolState.selected === 'bbox'); this.konva.group.listening(toolState.selected === 'bbox');
this.konva.rect.setAttrs({ this.konva.rect.setAttrs({

View File

@ -2,7 +2,12 @@ import { deepClone } from 'common/util/deepClone';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
import type { CanvasLayerState, CanvasV2State, GetLoggingContext } from 'features/controlLayers/store/types'; import type {
CanvasEntityIdentifier,
CanvasLayerState,
CanvasV2State,
GetLoggingContext,
} from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import type { Logger } from 'roarr'; import type { Logger } from 'roarr';
@ -48,6 +53,13 @@ export class CanvasLayerAdapter {
this.state = state; this.state = state;
} }
/**
* Get this entity's entity identifier
*/
getEntityIdentifier = (): CanvasEntityIdentifier => {
return { id: this.id, type: this.type };
};
destroy = (): void => { destroy = (): void => {
this.log.debug('Destroying layer'); this.log.debug('Destroying layer');
// We need to call the destroy method on all children so they can do their own cleanup. // We need to call the destroy method on all children so they can do their own cleanup.

View File

@ -2,6 +2,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
import type { import type {
CanvasEntityIdentifier,
CanvasInpaintMaskState, CanvasInpaintMaskState,
CanvasRegionalGuidanceState, CanvasRegionalGuidanceState,
CanvasV2State, CanvasV2State,
@ -54,6 +55,13 @@ export class CanvasMaskAdapter {
this.maskOpacity = this.manager.stateApi.getMaskOpacity(); this.maskOpacity = this.manager.stateApi.getMaskOpacity();
} }
/**
* Get this entity's entity identifier
*/
getEntityIdentifier = (): CanvasEntityIdentifier => {
return { id: this.id, type: this.type };
};
destroy = (): void => { destroy = (): void => {
this.log.debug('Destroying mask'); this.log.debug('Destroying mask');
// We need to call the destroy method on all children so they can do their own cleanup. // We need to call the destroy method on all children so they can do their own cleanup.

View File

@ -8,11 +8,7 @@ import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLaye
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter';
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
import { import { getPrefixedId, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util';
getPrefixedId,
konvaNodeToBlob,
previewBlob,
} from 'features/controlLayers/konva/util';
import { import {
type CanvasBrushLineState, type CanvasBrushLineState,
type CanvasEraserLineState, type CanvasEraserLineState,
@ -299,11 +295,17 @@ export class CanvasObjectRenderer {
this.buffer.id = getPrefixedId(this.buffer.type); this.buffer.id = getPrefixedId(this.buffer.type);
if (this.buffer.type === 'brush_line') { if (this.buffer.type === 'brush_line') {
this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, this.parent.type); this.manager.stateApi.addBrushLine({
entityIdentifier: this.parent.getEntityIdentifier(),
brushLine: this.buffer,
});
} else if (this.buffer.type === 'eraser_line') { } else if (this.buffer.type === 'eraser_line') {
this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, this.parent.type); this.manager.stateApi.addEraserLine({
entityIdentifier: this.parent.getEntityIdentifier(),
eraserLine: this.buffer,
});
} else if (this.buffer.type === 'rect') { } else if (this.buffer.type === 'rect') {
this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, this.parent.type); this.manager.stateApi.addRect({ entityIdentifier: this.parent.getEntityIdentifier(), rect: this.buffer });
} else { } else {
this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type'); this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type');
} }
@ -356,10 +358,11 @@ export class CanvasObjectRenderer {
const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true);
const imageObject = imageDTOToImageObject(imageDTO); const imageObject = imageDTOToImageObject(imageDTO);
await this.renderObject(imageObject, true); await this.renderObject(imageObject, true);
this.manager.stateApi.rasterizeEntity( this.manager.stateApi.rasterizeEntity({
{ id: this.parent.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }, entityIdentifier: this.parent.getEntityIdentifier(),
this.parent.type imageObject,
); position: { x: Math.round(rect.x), y: Math.round(rect.y) },
});
}; };
/** /**

View File

@ -15,44 +15,31 @@ import {
$stageAttrs, $stageAttrs,
bboxChanged, bboxChanged,
brushWidthChanged, brushWidthChanged,
caTranslated, entityBrushLineAdded,
entityEraserLineAdded,
entityMoved,
entityRasterized,
entityRectAdded,
entityReset,
entitySelected, entitySelected,
eraserWidthChanged, eraserWidthChanged,
imBrushLineAdded,
imEraserLineAdded,
imImageCacheChanged, imImageCacheChanged,
imMoved,
imRectAdded,
inpaintMaskRasterized,
layerBrushLineAdded,
layerEraserLineAdded,
layerImageCacheChanged, layerImageCacheChanged,
layerRasterized,
layerRectAdded,
layerReset,
layerTranslated,
rgBrushLineAdded,
rgEraserLineAdded,
rgImageCacheChanged, rgImageCacheChanged,
rgMoved,
rgRasterized,
rgRectAdded,
toolBufferChanged, toolBufferChanged,
toolChanged, toolChanged,
} from 'features/controlLayers/store/canvasV2Slice'; } from 'features/controlLayers/store/canvasV2Slice';
import type { import type {
CanvasBrushLineState, EntityBrushLineAddedPayload,
CanvasEntityIdentifier, EntityEraserLineAddedPayload,
CanvasEntityState, EntityIdentifierPayload,
CanvasEraserLineState, EntityMovedPayload,
CanvasRectState, EntityRasterizedPayload,
EntityRasterizedArg, EntityRectAddedPayload,
PositionChangedArg,
Rect, Rect,
Tool, Tool,
} from 'features/controlLayers/store/types'; } from 'features/controlLayers/store/types';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
const log = logger('canvas'); const log = logger('canvas');
@ -69,67 +56,31 @@ export class CanvasStateApi {
getState = () => { getState = () => {
return this._store.getState().canvasV2; return this._store.getState().canvasV2;
}; };
resetEntity = (arg: { id: string }, entityType: CanvasEntityState['type']) => { resetEntity = (arg: EntityIdentifierPayload) => {
log.trace({ arg, entityType }, 'Resetting entity'); log.trace(arg, 'Resetting entity');
if (entityType === 'layer') { this._store.dispatch(entityReset(arg));
this._store.dispatch(layerReset(arg));
}
}; };
setEntityPosition = (arg: PositionChangedArg, entityType: CanvasEntityState['type']) => { setEntityPosition = (arg: EntityMovedPayload) => {
log.trace({ arg, entityType }, 'Setting entity position'); log.trace(arg, 'Setting entity position');
if (entityType === 'layer') { this._store.dispatch(entityMoved(arg));
this._store.dispatch(layerTranslated(arg));
} else if (entityType === 'regional_guidance') {
this._store.dispatch(rgMoved(arg));
} else if (entityType === 'inpaint_mask') {
this._store.dispatch(imMoved(arg));
} else if (entityType === 'control_adapter') {
this._store.dispatch(caTranslated(arg));
}
}; };
addBrushLine = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntityState['type']) => { addBrushLine = (arg: EntityBrushLineAddedPayload) => {
log.trace({ arg, entityType }, 'Adding brush line'); log.trace(arg, 'Adding brush line');
if (entityType === 'layer') { this._store.dispatch(entityBrushLineAdded(arg));
this._store.dispatch(layerBrushLineAdded(arg));
} else if (entityType === 'regional_guidance') {
this._store.dispatch(rgBrushLineAdded(arg));
} else if (entityType === 'inpaint_mask') {
this._store.dispatch(imBrushLineAdded(arg));
}
}; };
addEraserLine = (arg: { id: string; eraserLine: CanvasEraserLineState }, entityType: CanvasEntityState['type']) => { addEraserLine = (arg: EntityEraserLineAddedPayload) => {
log.trace({ arg, entityType }, 'Adding eraser line'); log.trace(arg, 'Adding eraser line');
if (entityType === 'layer') { this._store.dispatch(entityEraserLineAdded(arg));
this._store.dispatch(layerEraserLineAdded(arg));
} else if (entityType === 'regional_guidance') {
this._store.dispatch(rgEraserLineAdded(arg));
} else if (entityType === 'inpaint_mask') {
this._store.dispatch(imEraserLineAdded(arg));
}
}; };
addRect = (arg: { id: string; rect: CanvasRectState }, entityType: CanvasEntityState['type']) => { addRect = (arg: EntityRectAddedPayload) => {
log.trace({ arg, entityType }, 'Adding rect'); log.trace(arg, 'Adding rect');
if (entityType === 'layer') { this._store.dispatch(entityRectAdded(arg));
this._store.dispatch(layerRectAdded(arg));
} else if (entityType === 'regional_guidance') {
this._store.dispatch(rgRectAdded(arg));
} else if (entityType === 'inpaint_mask') {
this._store.dispatch(imRectAdded(arg));
}
}; };
rasterizeEntity = (arg: EntityRasterizedArg, entityType: CanvasEntityState['type']) => { rasterizeEntity = (arg: EntityRasterizedPayload) => {
log.trace({ arg, entityType }, 'Rasterizing entity'); log.trace(arg, 'Rasterizing entity');
if (entityType === 'layer') { this._store.dispatch(entityRasterized(arg));
this._store.dispatch(layerRasterized(arg));
} else if (entityType === 'inpaint_mask') {
this._store.dispatch(inpaintMaskRasterized(arg));
} else if (entityType === 'regional_guidance') {
this._store.dispatch(rgRasterized(arg));
} else {
assert(false, 'Rasterizing not supported for this entity type');
}
}; };
setSelectedEntity = (arg: CanvasEntityIdentifier) => { setSelectedEntity = (arg: EntityIdentifierPayload) => {
log.trace({ arg }, 'Setting selected entity'); log.trace({ arg }, 'Setting selected entity');
this._store.dispatch(entitySelected(arg)); this._store.dispatch(entitySelected(arg));
}; };

View File

@ -356,7 +356,7 @@ export class CanvasTransformer {
}; };
this.log.trace({ position }, 'Position changed'); this.log.trace({ position }, 'Position changed');
this.manager.stateApi.setEntityPosition({ id: this.parent.id, position }, this.parent.type); this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.getEntityIdentifier(), position });
}); });
this.subscriptions.add( this.subscriptions.add(
@ -600,7 +600,7 @@ export class CanvasTransformer {
// We shouldn't reset on the first render - the bbox will be calculated on the next render // We shouldn't reset on the first render - the bbox will be calculated on the next render
if (!this.parent.renderer.hasObjects()) { if (!this.parent.renderer.hasObjects()) {
// The layer is fully transparent but has objects - reset it // The layer is fully transparent but has objects - reset it
this.manager.stateApi.resetEntity({ id: this.parent.id }, this.parent.type); this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() });
} }
this.syncInteractionState(); this.syncInteractionState();
return; return;

View File

@ -1,6 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSlice } from '@reduxjs/toolkit'; import { createAction, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store'; import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { deepClone } from 'common/util/deepClone'; import { deepClone } from 'common/util/deepClone';
import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
@ -20,8 +21,20 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { pick } from 'lodash-es'; import { pick } from 'lodash-es';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
import type { InvocationDenoiseProgressEvent } from 'services/events/types'; import type { InvocationDenoiseProgressEvent } from 'services/events/types';
import { assert } from 'tsafe';
import type { CanvasEntityIdentifier, CanvasV2State, Coordinate, StageAttrs } from './types'; import type {
CanvasEntityIdentifier,
CanvasV2State,
Coordinate,
EntityBrushLineAddedPayload,
EntityEraserLineAddedPayload,
EntityIdentifierPayload,
EntityMovedPayload,
EntityRasterizedPayload,
EntityRectAddedPayload,
StageAttrs,
} from './types';
import { RGBA_RED } from './types'; import { RGBA_RED } from './types';
const initialState: CanvasV2State = { const initialState: CanvasV2State = {
@ -122,6 +135,23 @@ const initialState: CanvasV2State = {
}, },
}; };
export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) {
switch (type) {
case 'layer':
return state.layers.entities.find((layer) => layer.id === id);
case 'control_adapter':
return state.controlAdapters.entities.find((ca) => ca.id === id);
case 'inpaint_mask':
return state.inpaintMask;
case 'regional_guidance':
return state.regions.entities.find((rg) => rg.id === id);
case 'ip_adapter':
return state.ipAdapters.entities.find((ip) => ip.id === id);
default:
return;
}
}
export const canvasV2Slice = createSlice({ export const canvasV2Slice = createSlice({
name: 'canvasV2', name: 'canvasV2',
initialState, initialState,
@ -138,8 +168,184 @@ export const canvasV2Slice = createSlice({
...bboxReducers, ...bboxReducers,
...inpaintMaskReducers, ...inpaintMaskReducers,
...sessionReducers, ...sessionReducers,
entitySelected: (state, action: PayloadAction<CanvasEntityIdentifier>) => { entitySelected: (state, action: PayloadAction<EntityIdentifierPayload>) => {
state.selectedEntityIdentifier = action.payload; const { entityIdentifier } = action.payload;
state.selectedEntityIdentifier = entityIdentifier;
},
entityReset: (state, action: PayloadAction<EntityIdentifierPayload>) => {
const { entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
entity.isEnabled = true;
entity.objects = [];
entity.position = { x: 0, y: 0 };
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.isEnabled = true;
entity.objects = [];
entity.position = { x: 0, y: 0 };
entity.imageCache = null;
} else {
assert(false, 'Not implemented');
}
},
entityIsEnabledToggled: (state, action: PayloadAction<EntityIdentifierPayload>) => {
const { entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.isEnabled = !entity.isEnabled;
},
entityMoved: (state, action: PayloadAction<EntityMovedPayload>) => {
const { entityIdentifier, position } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
entity.position = position;
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.position = position;
entity.imageCache = null;
} else {
assert(false, 'Not implemented');
}
},
entityRasterized: (state, action: PayloadAction<EntityRasterizedPayload>) => {
const { entityIdentifier, imageObject, position } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
entity.objects = [imageObject];
entity.position = position;
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects = [imageObject];
entity.position = position;
entity.imageCache = null;
} else {
assert(false, 'Not implemented');
}
},
entityBrushLineAdded: (state, action: PayloadAction<EntityBrushLineAddedPayload>) => {
const { entityIdentifier, brushLine } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
entity.objects.push(brushLine);
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects.push(brushLine);
entity.imageCache = null;
} else {
assert(false, 'Not implemented');
}
},
entityEraserLineAdded: (state, action: PayloadAction<EntityEraserLineAddedPayload>) => {
const { entityIdentifier, eraserLine } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
entity.objects.push(eraserLine);
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects.push(eraserLine);
entity.imageCache = null;
} else {
assert(false, 'Not implemented');
}
},
entityRectAdded: (state, action: PayloadAction<EntityRectAddedPayload>) => {
const { entityIdentifier, rect } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
entity.objects.push(rect);
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects.push(rect);
entity.imageCache = null;
} else {
assert(false, 'Not implemented');
}
},
entityDeleted: (state, action: PayloadAction<EntityIdentifierPayload>) => {
const { entityIdentifier } = action.payload;
if (entityIdentifier.type === 'layer') {
state.layers.entities = state.layers.entities.filter((layer) => layer.id !== entityIdentifier.id);
state.layers.imageCache = null;
} else if (entityIdentifier.type === 'regional_guidance') {
state.regions.entities = state.regions.entities.filter((rg) => rg.id !== entityIdentifier.id);
} else {
assert(false, 'Not implemented');
}
},
entityArrangedForwardOne: (state, action: PayloadAction<EntityIdentifierPayload>) => {
const { entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
if (entity.type === 'layer') {
moveOneToEnd(state.layers.entities, entity);
state.layers.imageCache = null;
} else if (entity.type === 'regional_guidance') {
moveOneToEnd(state.regions.entities, entity);
} else if (entity.type === 'control_adapter') {
moveOneToEnd(state.controlAdapters.entities, entity);
}
},
entityArrangedToFront: (state, action: PayloadAction<EntityIdentifierPayload>) => {
const { entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
if (entity.type === 'layer') {
moveToEnd(state.layers.entities, entity);
state.layers.imageCache = null;
} else if (entity.type === 'regional_guidance') {
moveToEnd(state.regions.entities, entity);
} else if (entity.type === 'control_adapter') {
moveToEnd(state.controlAdapters.entities, entity);
}
},
entityArrangedBackwardOne: (state, action: PayloadAction<EntityIdentifierPayload>) => {
const { entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
if (entity.type === 'layer') {
moveOneToStart(state.layers.entities, entity);
state.layers.imageCache = null;
} else if (entity.type === 'regional_guidance') {
moveOneToStart(state.regions.entities, entity);
} else if (entity.type === 'control_adapter') {
moveOneToStart(state.controlAdapters.entities, entity);
}
},
entityArrangedToBack: (state, action: PayloadAction<EntityIdentifierPayload>) => {
const { entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
if (entity.type === 'layer') {
moveToStart(state.layers.entities, entity);
state.layers.imageCache = null;
} else if (entity.type === 'regional_guidance') {
moveToStart(state.regions.entities, entity);
} else if (entity.type === 'control_adapter') {
moveToStart(state.controlAdapters.entities, entity);
}
}, },
allEntitiesDeleted: (state) => { allEntitiesDeleted: (state) => {
state.regions.entities = []; state.regions.entities = [];
@ -176,10 +382,23 @@ export const {
toolChanged, toolChanged,
toolBufferChanged, toolBufferChanged,
maskOpacityChanged, maskOpacityChanged,
entitySelected,
allEntitiesDeleted, allEntitiesDeleted,
clipToBboxChanged, clipToBboxChanged,
canvasReset, canvasReset,
// All entities
entitySelected,
entityReset,
entityIsEnabledToggled,
entityMoved,
entityRasterized,
entityBrushLineAdded,
entityEraserLineAdded,
entityRectAdded,
entityDeleted,
entityArrangedForwardOne,
entityArrangedToFront,
entityArrangedBackwardOne,
entityArrangedToBack,
// bbox // bbox
bboxChanged, bboxChanged,
bboxScaledSizeChanged, bboxScaledSizeChanged,
@ -193,23 +412,10 @@ export const {
// layers // layers
layerAdded, layerAdded,
layerRecalled, layerRecalled,
layerDeleted,
layerReset,
layerMovedForwardOne,
layerMovedToFront,
layerMovedBackwardOne,
layerMovedToBack,
layerIsEnabledToggled,
layerOpacityChanged, layerOpacityChanged,
layerTranslated,
layerBboxChanged,
layerImageAdded, layerImageAdded,
layerAllDeleted, layerAllDeleted,
layerImageCacheChanged, layerImageCacheChanged,
layerBrushLineAdded,
layerEraserLineAdded,
layerRectAdded,
layerRasterized,
// IP Adapters // IP Adapters
ipaAdded, ipaAdded,
ipaRecalled, ipaRecalled,
@ -224,16 +430,8 @@ export const {
ipaBeginEndStepPctChanged, ipaBeginEndStepPctChanged,
// Control Adapters // Control Adapters
caAdded, caAdded,
caBboxChanged,
caDeleted,
caAllDeleted, caAllDeleted,
caIsEnabledToggled,
caMovedBackwardOne,
caMovedForwardOne,
caMovedToBack,
caMovedToFront,
caOpacityChanged, caOpacityChanged,
caTranslated,
caRecalled, caRecalled,
caImageChanged, caImageChanged,
caProcessedImageChanged, caProcessedImageChanged,
@ -244,19 +442,10 @@ export const {
caProcessorPendingBatchIdChanged, caProcessorPendingBatchIdChanged,
caWeightChanged, caWeightChanged,
caBeginEndStepPctChanged, caBeginEndStepPctChanged,
caScaled,
// Regions // Regions
rgAdded, rgAdded,
rgRecalled, rgRecalled,
rgReset,
rgIsEnabledToggled,
rgMoved,
rgDeleted,
rgAllDeleted, rgAllDeleted,
rgMovedForwardOne,
rgMovedToFront,
rgMovedBackwardOne,
rgMovedToBack,
rgPositivePromptChanged, rgPositivePromptChanged,
rgNegativePromptChanged, rgNegativePromptChanged,
rgFillChanged, rgFillChanged,
@ -270,10 +459,6 @@ export const {
rgIPAdapterMethodChanged, rgIPAdapterMethodChanged,
rgIPAdapterModelChanged, rgIPAdapterModelChanged,
rgIPAdapterCLIPVisionModelChanged, rgIPAdapterCLIPVisionModelChanged,
rgBrushLineAdded,
rgEraserLineAdded,
rgRectAdded,
rgRasterized,
// Compositing // Compositing
setInfillMethod, setInfillMethod,
setInfillTileSize, setInfillTileSize,
@ -319,16 +504,9 @@ export const {
loraIsEnabledChanged, loraIsEnabledChanged,
loraAllDeleted, loraAllDeleted,
// Inpaint mask // Inpaint mask
imReset,
imRecalled, imRecalled,
imIsEnabledToggled,
imMoved,
imFillChanged, imFillChanged,
imImageCacheChanged, imImageCacheChanged,
imBrushLineAdded,
imEraserLineAdded,
imRectAdded,
inpaintMaskRasterized,
// Staging // Staging
sessionStartedStaging, sessionStartedStaging,
sessionImageStaged, sessionImageStaged,

View File

@ -1,24 +1,20 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common';
import type { IRect } from 'konva/lib/types';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { import type {
CanvasV2State,
CanvasControlAdapterState, CanvasControlAdapterState,
CanvasControlNetState,
CanvasT2IAdapterState,
CanvasV2State,
ControlModeV2, ControlModeV2,
ControlNetConfig, ControlNetConfig,
CanvasControlNetState,
Filter, Filter,
PositionChangedArg,
ProcessorConfig, ProcessorConfig,
ScaleChangedArg,
T2IAdapterConfig, T2IAdapterConfig,
CanvasT2IAdapterState,
} from './types'; } from './types';
import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types'; import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types';
@ -56,58 +52,6 @@ export const controlAdaptersReducers = {
state.controlAdapters.entities.push(data); state.controlAdapters.entities.push(data);
state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id }; state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id };
}, },
caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.isEnabled = !ca.isEnabled;
},
caTranslated: (state, action: PayloadAction<PositionChangedArg>) => {
const { id, position } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.position = position;
},
caScaled: (state, action: PayloadAction<ScaleChangedArg>) => {
const { id, scale, position } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
if (ca.imageObject) {
ca.imageObject.x *= scale;
ca.imageObject.y *= scale;
ca.imageObject.height *= scale;
ca.imageObject.width *= scale;
}
if (ca.processedImageObject) {
ca.processedImageObject.x *= scale;
ca.processedImageObject.y *= scale;
ca.processedImageObject.height *= scale;
ca.processedImageObject.width *= scale;
}
ca.position = position;
ca.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.bbox = bbox;
ca.bboxNeedsUpdate = false;
},
caDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.controlAdapters.entities = state.controlAdapters.entities.filter((ca) => ca.id !== id);
},
caAllDeleted: (state) => { caAllDeleted: (state) => {
state.controlAdapters.entities = []; state.controlAdapters.entities = [];
}, },
@ -119,38 +63,6 @@ export const controlAdaptersReducers = {
} }
ca.opacity = opacity; ca.opacity = opacity;
}, },
caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveOneToEnd(state.controlAdapters.entities, ca);
},
caMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveToEnd(state.controlAdapters.entities, ca);
},
caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveOneToStart(state.controlAdapters.entities, ca);
},
caMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveToStart(state.controlAdapters.entities, ca);
},
caImageChanged: { caImageChanged: {
reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => {
const { id, imageDTO, objectId } = action.payload; const { id, imageDTO, objectId } = action.payload;

View File

@ -1,35 +1,16 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import type { import type { CanvasInpaintMaskState, CanvasV2State } from 'features/controlLayers/store/types';
CanvasBrushLineState,
CanvasEraserLineState,
CanvasInpaintMaskState,
CanvasRectState,
CanvasV2State,
Coordinate,
EntityRasterizedArg,
} from 'features/controlLayers/store/types';
import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims } from 'features/controlLayers/store/types';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import type { RgbColor } from './types'; import type { RgbColor } from './types';
export const inpaintMaskReducers = { export const inpaintMaskReducers = {
imReset: (state) => {
state.inpaintMask.objects = [];
state.inpaintMask.imageCache = null;
},
imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => {
const { data } = action.payload; const { data } = action.payload;
state.inpaintMask = data; state.inpaintMask = data;
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
}, },
imIsEnabledToggled: (state) => {
state.inpaintMask.isEnabled = !state.inpaintMask.isEnabled;
},
imMoved: (state, action: PayloadAction<{ position: Coordinate }>) => {
const { position } = action.payload;
state.inpaintMask.position = position;
},
imFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => { imFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => {
const { fill } = action.payload; const { fill } = action.payload;
state.inpaintMask.fill = fill; state.inpaintMask.fill = fill;
@ -38,25 +19,4 @@ export const inpaintMaskReducers = {
const { imageDTO } = action.payload; const { imageDTO } = action.payload;
state.inpaintMask.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; state.inpaintMask.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
}, },
imBrushLineAdded: (state, action: PayloadAction<{ brushLine: CanvasBrushLineState }>) => {
const { brushLine } = action.payload;
state.inpaintMask.objects.push(brushLine);
state.layers.imageCache = null;
},
imEraserLineAdded: (state, action: PayloadAction<{ eraserLine: CanvasEraserLineState }>) => {
const { eraserLine } = action.payload;
state.inpaintMask.objects.push(eraserLine);
state.layers.imageCache = null;
},
imRectAdded: (state, action: PayloadAction<{ rect: CanvasRectState }>) => {
const { rect } = action.payload;
state.inpaintMask.objects.push(rect);
state.layers.imageCache = null;
},
inpaintMaskRasterized: (state, action: PayloadAction<EntityRasterizedArg>) => {
const { imageObject, position } = action.payload;
state.inpaintMask.objects = [imageObject];
state.inpaintMask.position = position;
state.inpaintMask.imageCache = null;
},
} satisfies SliceCaseReducers<CanvasV2State>; } satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -1,21 +1,10 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getPrefixedId } from 'features/controlLayers/konva/util'; import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { IRect } from 'konva/lib/types';
import { merge } from 'lodash-es'; import { merge } from 'lodash-es';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import type { import type { CanvasLayerState, CanvasV2State, ImageObjectAddedArg } from './types';
CanvasBrushLineState,
CanvasEraserLineState,
CanvasLayerState,
CanvasRectState,
CanvasV2State,
EntityRasterizedArg,
ImageObjectAddedArg,
PositionChangedArg,
} from './types';
import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from './types';
export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id);
@ -52,52 +41,6 @@ export const layersReducers = {
state.selectedEntityIdentifier = { type: 'layer', id: data.id }; state.selectedEntityIdentifier = { type: 'layer', id: data.id };
state.layers.imageCache = null; state.layers.imageCache = null;
}, },
layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.isEnabled = !layer.isEnabled;
state.layers.imageCache = null;
},
layerTranslated: (state, action: PayloadAction<PositionChangedArg>) => {
const { id, position } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.position = position;
state.layers.imageCache = null;
},
layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
if (bbox === null) {
// TODO(psyche): Clear objects when bbox is cleared - right now this doesn't work bc bbox calculation for layers
// doesn't work - always returns null
// layer.objects = [];
}
},
layerReset: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.isEnabled = true;
layer.objects = [];
state.layers.imageCache = null;
layer.position = { x: 0, y: 0 };
},
layerDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.layers.entities = state.layers.entities.filter((l) => l.id !== id);
state.layers.imageCache = null;
},
layerAllDeleted: (state) => { layerAllDeleted: (state) => {
state.layers.entities = []; state.layers.entities = [];
state.layers.imageCache = null; state.layers.imageCache = null;
@ -111,72 +54,6 @@ export const layersReducers = {
layer.opacity = opacity; layer.opacity = opacity;
state.layers.imageCache = null; state.layers.imageCache = null;
}, },
layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveOneToEnd(state.layers.entities, layer);
state.layers.imageCache = null;
},
layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveToEnd(state.layers.entities, layer);
state.layers.imageCache = null;
},
layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveOneToStart(state.layers.entities, layer);
state.layers.imageCache = null;
},
layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveToStart(state.layers.entities, layer);
state.layers.imageCache = null;
},
layerBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: CanvasBrushLineState }>) => {
const { id, brushLine } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push(brushLine);
state.layers.imageCache = null;
},
layerEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: CanvasEraserLineState }>) => {
const { id, eraserLine } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push(eraserLine);
state.layers.imageCache = null;
},
layerRectAdded: (state, action: PayloadAction<{ id: string; rect: CanvasRectState }>) => {
const { id, rect } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push(rect);
state.layers.imageCache = null;
},
layerImageAdded: ( layerImageAdded: (
state, state,
action: PayloadAction<ImageObjectAddedArg & { objectId: string; pos?: { x: number; y: number } }> action: PayloadAction<ImageObjectAddedArg & { objectId: string; pos?: { x: number; y: number } }>
@ -198,14 +75,4 @@ export const layersReducers = {
const { imageDTO } = action.payload; const { imageDTO } = action.payload;
state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
}, },
layerRasterized: (state, action: PayloadAction<EntityRasterizedArg>) => {
const { id, imageObject, position } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects = [imageObject];
layer.position = position;
state.layers.imageCache = null;
},
} satisfies SliceCaseReducers<CanvasV2State>; } satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -1,16 +1,6 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getPrefixedId } from 'features/controlLayers/konva/util'; import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
CanvasBrushLineState,
CanvasEraserLineState,
CanvasRectState,
CanvasV2State,
CLIPVisionModelV2,
EntityRasterizedArg,
IPMethodV2,
PositionChangedArg,
} from 'features/controlLayers/store/types';
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
@ -71,73 +61,14 @@ export const regionsReducers = {
}, },
prepare: () => ({ payload: { id: getPrefixedId('regional_guidance') } }), prepare: () => ({ payload: { id: getPrefixedId('regional_guidance') } }),
}, },
rgReset: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects = [];
rg.imageCache = null;
},
rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => { rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => {
const { data } = action.payload; const { data } = action.payload;
state.regions.entities.push(data); state.regions.entities.push(data);
state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id };
}, },
rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (rg) {
rg.isEnabled = !rg.isEnabled;
}
},
rgMoved: (state, action: PayloadAction<PositionChangedArg>) => {
const { id, position } = action.payload;
const rg = selectRG(state, id);
if (rg) {
rg.position = position;
}
},
rgDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.regions.entities = state.regions.entities.filter((ca) => ca.id !== id);
},
rgAllDeleted: (state) => { rgAllDeleted: (state) => {
state.regions.entities = []; state.regions.entities = [];
}, },
rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveOneToEnd(state.regions.entities, rg);
},
rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveToEnd(state.regions.entities, rg);
},
rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveOneToStart(state.regions.entities, rg);
},
rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveToStart(state.regions.entities, rg);
},
rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload; const { id, prompt } = action.payload;
const rg = selectRG(state, id); const rg = selectRG(state, id);
@ -286,44 +217,4 @@ export const regionsReducers = {
} }
ipa.clipVisionModel = clipVisionModel; ipa.clipVisionModel = clipVisionModel;
}, },
rgBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: CanvasBrushLineState }>) => {
const { id, brushLine } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push(brushLine);
state.layers.imageCache = null;
},
rgEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: CanvasEraserLineState }>) => {
const { id, eraserLine } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push(eraserLine);
state.layers.imageCache = null;
},
rgRectAdded: (state, action: PayloadAction<{ id: string; rect: CanvasRectState }>) => {
const { id, rect } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push(rect);
state.layers.imageCache = null;
},
rgRasterized: (state, action: PayloadAction<EntityRasterizedArg>) => {
const { id, imageObject, position } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects = [imageObject];
rg.position = position;
rg.imageCache = null;
},
} satisfies SliceCaseReducers<CanvasV2State>; } satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -924,11 +924,20 @@ export type PositionChangedArg = { id: string; position: Coordinate };
export type ScaleChangedArg = { id: string; scale: Coordinate; position: Coordinate }; export type ScaleChangedArg = { id: string; scale: Coordinate; position: Coordinate };
export type BboxChangedArg = { id: string; bbox: Rect | null }; export type BboxChangedArg = { id: string; bbox: Rect | null };
export type BrushLineAddedArg = { id: string; brushLine: CanvasBrushLineState }; export type EntityIdentifierPayload = { entityIdentifier: CanvasEntityIdentifier };
export type EraserLineAddedArg = { id: string; eraserLine: CanvasEraserLineState }; export type EntityMovedPayload = { entityIdentifier: CanvasEntityIdentifier; position: Coordinate };
export type RectAddedArg = { id: string; rect: CanvasRectState }; export type EntityBrushLineAddedPayload = { entityIdentifier: CanvasEntityIdentifier; brushLine: CanvasBrushLineState };
export type EntityEraserLineAddedPayload = {
entityIdentifier: CanvasEntityIdentifier;
eraserLine: CanvasEraserLineState;
};
export type EntityRectAddedPayload = { entityIdentifier: CanvasEntityIdentifier; rect: CanvasRectState };
export type EntityRasterizedPayload = {
entityIdentifier: CanvasEntityIdentifier;
imageObject: CanvasImageState;
position: Coordinate;
};
export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate }; export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate };
export type EntityRasterizedArg = { id: string; imageObject: CanvasImageState; position: Coordinate };
//#region Type guards //#region Type guards
export const isLine = (obj: CanvasObjectState): obj is CanvasBrushLineState | CanvasEraserLineState => { export const isLine = (obj: CanvasObjectState): obj is CanvasBrushLineState | CanvasEraserLineState => {