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

View File

@ -1,6 +1,11 @@
import { logger } from 'app/logging/logger';
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 { zParameterModel } from 'features/parameters/types/parameterSchemas';
import { toast } from 'features/toast/toast';
@ -49,7 +54,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
if (ca.model?.base !== newBaseModel) {
modelsCleared += 1;
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 { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CAHeader } from 'features/controlLayers/components/ControlAdapter/CAEntityHeader';
import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
type Props = {
id: string;
};
export const CA = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'control_adapter' }), [id]);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id, type: 'control_adapter' }));
}, [dispatch, id]);
return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect}>
<CAHeader id={id} isSelected={isSelected} onToggleVisibility={onToggle} />
{isOpen && <CASettings id={id} />}
</CanvasEntityContainer>
<EntityIdentifierContext.Provider value={entityIdentifier}>
<CanvasEntityContainer>
<CAHeader onToggleVisibility={onToggle} />
{isOpen && <CASettings id={id} />}
</CanvasEntityContainer>
</EntityIdentifierContext.Provider>
);
});

View File

@ -1,84 +1,14 @@
import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { Menu, MenuList } from '@invoke-ai/ui-library';
import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import {
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]);
import { memo } from 'react';
export const CAActionsMenu = memo(() => {
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
{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>
<CanvasEntityActionMenuItems />
</MenuList>
</Menu>
);

View File

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

View File

@ -14,6 +14,7 @@ import {
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers';
import type { ChangeEvent } from 'react';
@ -21,14 +22,11 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi';
type Props = {
id: string;
};
const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`;
export const CAOpacityAndFilter = memo(({ id }: Props) => {
export const CAOpacityAndFilter = memo(() => {
const { id } = useEntityIdentifierContext();
const { t } = useTranslation();
const dispatch = useAppDispatch();
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 { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id, type: 'ip_adapter' }));
dispatch(entitySelected({ entityIdentifier: { id, type: 'ip_adapter' } }));
}, [dispatch, id]);
return (

View File

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

View File

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

View File

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

View File

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

View File

@ -1,84 +1,14 @@
import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { Menu, MenuList } from '@invoke-ai/ui-library';
import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import {
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]);
import { memo } from 'react';
export const LayerActionsMenu = memo(() => {
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
{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>
<CanvasEntityActionMenuItems />
</MenuList>
</Menu>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,50 +1,41 @@
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 { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
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 { memo, useCallback } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { RGMaskFillColorPicker } from './RGMaskFillColorPicker';
import { RGSettingsPopover } from './RGSettingsPopover';
type Props = {
id: string;
isSelected: boolean;
onToggleVisibility: () => void;
};
export const RGHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => {
export const RGHeader = memo(({ onToggleVisibility }: Props) => {
const { id } = useEntityIdentifierContext();
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 onToggleIsEnabled = useCallback(() => {
dispatch(rgIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(rgDeleted({ id }));
}, [dispatch, id]);
return (
<CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} />
<CanvasEntityTitle title={t('controlLayers.regionalGuidance')} isSelected={isSelected} />
<CanvasEntityEnabledToggle />
<CanvasEntityTitle />
<Spacer />
{autoNegative === 'invert' && (
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
{t('controlLayers.autoNegative')}
</Badge>
)}
<RGMaskFillColorPicker id={id} />
<RGSettingsPopover id={id} />
<RGActionsMenu id={id} />
<CanvasEntityDeleteButton onDelete={onDelete} />
<RGMaskFillColorPicker />
<RGSettingsPopover />
<RGActionsMenu />
<CanvasEntityDeleteButton />
</CanvasEntityHeader>
);
});

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import {
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgAutoNegativeChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import type { ChangeEvent } from 'react';
@ -19,19 +20,16 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiGearSixBold } from 'react-icons/pi';
type Props = {
id: string;
};
export const RGSettingsPopover = memo(({ id }: Props) => {
export const RGSettingsPopover = memo(() => {
const entityIdentifier = useEntityIdentifierContext();
const { t } = useTranslation();
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(
(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 (

View File

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

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 { 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 { memo, useCallback } from 'react';
type Props = PropsWithChildren<{
isSelected: boolean;
onSelect: () => void;
selectedBorderColor?: ChakraProps['bg'];
}>;
export const CanvasEntityContainer = memo((props: Props) => {
const { isSelected, onSelect, selectedBorderColor = 'base.400', children } = props;
const _onSelect = useCallback(() => {
export const CanvasEntityContainer = memo((props: PropsWithChildren) => {
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const isSelected = useIsEntitySelected(entityIdentifier);
const selectionColor = useEntitySelectionColor(entityIdentifier);
const onClick = useCallback(() => {
if (isSelected) {
return;
}
onSelect();
}, [isSelected, onSelect]);
dispatch(entitySelected({ entityIdentifier }));
}, [dispatch, entityIdentifier, isSelected]);
return (
<Flex
@ -24,12 +25,12 @@ export const CanvasEntityContainer = memo((props: Props) => {
flexDir="column"
w="full"
bg={isSelected ? 'base.800' : 'base.850'}
onClick={_onSelect}
onClick={onClick}
borderInlineStartWidth={5}
borderColor={isSelected ? selectedBorderColor : 'base.800'}
borderColor={isSelected ? selectionColor : 'base.800'}
borderRadius="base"
>
{children}
{props.children}
</Flex>
);
});

View File

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

View File

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

View File

@ -1,12 +1,30 @@
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 = {
title: string;
isSelected: boolean;
};
export const CanvasEntityTitle = memo(() => {
const { t } = useTranslation();
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 (
<Text size="sm" fontWeight="semibold" userSelect="none" color={isSelected ? 'base.100' : 'base.300'}>
{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 { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import {
caDeleted,
ipaDeleted,
layerDeleted,
rgDeleted,
entityDeleted,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { useCallback, useMemo } from 'react';
@ -26,19 +23,7 @@ export function useCanvasDeleteLayerHotkey() {
if (selectedEntityIdentifier === null) {
return;
}
const { type, id } = selectedEntityIdentifier;
if (type === 'layer') {
dispatch(layerDeleted({ id }));
}
if (type === 'regional_guidance') {
dispatch(rgDeleted({ id }));
}
if (type === 'control_adapter') {
dispatch(caDeleted({ id }));
}
if (type === 'ip_adapter') {
dispatch(ipaDeleted({ id }));
}
dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier }));
}, [dispatch, selectedEntityIdentifier]);
const isDeleteEnabled = useMemo(

View File

@ -2,9 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import {
imReset,
layerReset,
rgReset,
entityReset,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { useCallback, useMemo } from 'react';
@ -25,16 +23,7 @@ export function useCanvasResetLayerHotkey() {
if (selectedEntityIdentifier === null) {
return;
}
const { type, id } = selectedEntityIdentifier;
if (type === 'layer') {
dispatch(layerReset({ id }));
}
if (type === 'regional_guidance') {
dispatch(rgReset({ id }));
}
if (type === 'inpaint_mask') {
dispatch(imReset());
}
dispatch(entityReset({ entityIdentifier: selectedEntityIdentifier }));
}, [dispatch, selectedEntityIdentifier]);
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() {
const session = this.manager.stateApi.getSession();
const bbox = this.manager.stateApi.getBbox();
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.listening(toolState.selected === 'bbox');
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 { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
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 { get } from 'lodash-es';
import type { Logger } from 'roarr';
@ -48,6 +53,13 @@ export class CanvasLayerAdapter {
this.state = state;
}
/**
* Get this entity's entity identifier
*/
getEntityIdentifier = (): CanvasEntityIdentifier => {
return { id: this.id, type: this.type };
};
destroy = (): void => {
this.log.debug('Destroying layer');
// 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 { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
import type {
CanvasEntityIdentifier,
CanvasInpaintMaskState,
CanvasRegionalGuidanceState,
CanvasV2State,
@ -54,6 +55,13 @@ export class CanvasMaskAdapter {
this.maskOpacity = this.manager.stateApi.getMaskOpacity();
}
/**
* Get this entity's entity identifier
*/
getEntityIdentifier = (): CanvasEntityIdentifier => {
return { id: this.id, type: this.type };
};
destroy = (): void => {
this.log.debug('Destroying mask');
// 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 { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter';
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
import {
getPrefixedId,
konvaNodeToBlob,
previewBlob,
} from 'features/controlLayers/konva/util';
import { getPrefixedId, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util';
import {
type CanvasBrushLineState,
type CanvasEraserLineState,
@ -299,11 +295,17 @@ export class CanvasObjectRenderer {
this.buffer.id = getPrefixedId(this.buffer.type);
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') {
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') {
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 {
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 imageObject = imageDTOToImageObject(imageDTO);
await this.renderObject(imageObject, true);
this.manager.stateApi.rasterizeEntity(
{ id: this.parent.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } },
this.parent.type
);
this.manager.stateApi.rasterizeEntity({
entityIdentifier: this.parent.getEntityIdentifier(),
imageObject,
position: { x: Math.round(rect.x), y: Math.round(rect.y) },
});
};
/**

View File

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

View File

@ -356,7 +356,7 @@ export class CanvasTransformer {
};
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(
@ -600,7 +600,7 @@ export class CanvasTransformer {
// We shouldn't reset on the first render - the bbox will be calculated on the next render
if (!this.parent.renderer.hasObjects()) {
// 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();
return;

View File

@ -1,6 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { deepClone } from 'common/util/deepClone';
import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
@ -20,8 +21,20 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { pick } from 'lodash-es';
import { atom } from 'nanostores';
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';
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({
name: 'canvasV2',
initialState,
@ -138,8 +168,184 @@ export const canvasV2Slice = createSlice({
...bboxReducers,
...inpaintMaskReducers,
...sessionReducers,
entitySelected: (state, action: PayloadAction<CanvasEntityIdentifier>) => {
state.selectedEntityIdentifier = action.payload;
entitySelected: (state, action: PayloadAction<EntityIdentifierPayload>) => {
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) => {
state.regions.entities = [];
@ -176,10 +382,23 @@ export const {
toolChanged,
toolBufferChanged,
maskOpacityChanged,
entitySelected,
allEntitiesDeleted,
clipToBboxChanged,
canvasReset,
// All entities
entitySelected,
entityReset,
entityIsEnabledToggled,
entityMoved,
entityRasterized,
entityBrushLineAdded,
entityEraserLineAdded,
entityRectAdded,
entityDeleted,
entityArrangedForwardOne,
entityArrangedToFront,
entityArrangedBackwardOne,
entityArrangedToBack,
// bbox
bboxChanged,
bboxScaledSizeChanged,
@ -193,23 +412,10 @@ export const {
// layers
layerAdded,
layerRecalled,
layerDeleted,
layerReset,
layerMovedForwardOne,
layerMovedToFront,
layerMovedBackwardOne,
layerMovedToBack,
layerIsEnabledToggled,
layerOpacityChanged,
layerTranslated,
layerBboxChanged,
layerImageAdded,
layerAllDeleted,
layerImageCacheChanged,
layerBrushLineAdded,
layerEraserLineAdded,
layerRectAdded,
layerRasterized,
// IP Adapters
ipaAdded,
ipaRecalled,
@ -224,16 +430,8 @@ export const {
ipaBeginEndStepPctChanged,
// Control Adapters
caAdded,
caBboxChanged,
caDeleted,
caAllDeleted,
caIsEnabledToggled,
caMovedBackwardOne,
caMovedForwardOne,
caMovedToBack,
caMovedToFront,
caOpacityChanged,
caTranslated,
caRecalled,
caImageChanged,
caProcessedImageChanged,
@ -244,19 +442,10 @@ export const {
caProcessorPendingBatchIdChanged,
caWeightChanged,
caBeginEndStepPctChanged,
caScaled,
// Regions
rgAdded,
rgRecalled,
rgReset,
rgIsEnabledToggled,
rgMoved,
rgDeleted,
rgAllDeleted,
rgMovedForwardOne,
rgMovedToFront,
rgMovedBackwardOne,
rgMovedToBack,
rgPositivePromptChanged,
rgNegativePromptChanged,
rgFillChanged,
@ -270,10 +459,6 @@ export const {
rgIPAdapterMethodChanged,
rgIPAdapterModelChanged,
rgIPAdapterCLIPVisionModelChanged,
rgBrushLineAdded,
rgEraserLineAdded,
rgRectAdded,
rgRasterized,
// Compositing
setInfillMethod,
setInfillTileSize,
@ -319,16 +504,9 @@ export const {
loraIsEnabledChanged,
loraAllDeleted,
// Inpaint mask
imReset,
imRecalled,
imIsEnabledToggled,
imMoved,
imFillChanged,
imImageCacheChanged,
imBrushLineAdded,
imEraserLineAdded,
imRectAdded,
inpaintMaskRasterized,
// Staging
sessionStartedStaging,
sessionImageStaged,

View File

@ -1,24 +1,20 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { IRect } from 'konva/lib/types';
import { isEqual } from 'lodash-es';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type {
CanvasV2State,
CanvasControlAdapterState,
CanvasControlNetState,
CanvasT2IAdapterState,
CanvasV2State,
ControlModeV2,
ControlNetConfig,
CanvasControlNetState,
Filter,
PositionChangedArg,
ProcessorConfig,
ScaleChangedArg,
T2IAdapterConfig,
CanvasT2IAdapterState,
} from './types';
import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types';
@ -56,58 +52,6 @@ export const controlAdaptersReducers = {
state.controlAdapters.entities.push(data);
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) => {
state.controlAdapters.entities = [];
},
@ -119,38 +63,6 @@ export const controlAdaptersReducers = {
}
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: {
reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => {
const { id, imageDTO, objectId } = action.payload;

View File

@ -1,35 +1,16 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import type {
CanvasBrushLineState,
CanvasEraserLineState,
CanvasInpaintMaskState,
CanvasRectState,
CanvasV2State,
Coordinate,
EntityRasterizedArg,
} from 'features/controlLayers/store/types';
import type { CanvasInpaintMaskState, CanvasV2State } from 'features/controlLayers/store/types';
import { imageDTOToImageWithDims } from 'features/controlLayers/store/types';
import type { ImageDTO } from 'services/api/types';
import type { RgbColor } from './types';
export const inpaintMaskReducers = {
imReset: (state) => {
state.inpaintMask.objects = [];
state.inpaintMask.imageCache = null;
},
imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => {
const { data } = action.payload;
state.inpaintMask = data;
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 }>) => {
const { fill } = action.payload;
state.inpaintMask.fill = fill;
@ -38,25 +19,4 @@ export const inpaintMaskReducers = {
const { imageDTO } = action.payload;
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>;

View File

@ -1,21 +1,10 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { IRect } from 'konva/lib/types';
import { merge } from 'lodash-es';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
import type {
CanvasBrushLineState,
CanvasEraserLineState,
CanvasLayerState,
CanvasRectState,
CanvasV2State,
EntityRasterizedArg,
ImageObjectAddedArg,
PositionChangedArg,
} from './types';
import type { CanvasLayerState, CanvasV2State, ImageObjectAddedArg } from './types';
import { imageDTOToImageObject, imageDTOToImageWithDims } from './types';
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.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) => {
state.layers.entities = [];
state.layers.imageCache = null;
@ -111,72 +54,6 @@ export const layersReducers = {
layer.opacity = opacity;
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: (
state,
action: PayloadAction<ImageObjectAddedArg & { objectId: string; pos?: { x: number; y: number } }>
@ -198,14 +75,4 @@ export const layersReducers = {
const { imageDTO } = action.payload;
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>;

View File

@ -1,16 +1,6 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type {
CanvasBrushLineState,
CanvasEraserLineState,
CanvasRectState,
CanvasV2State,
CLIPVisionModelV2,
EntityRasterizedArg,
IPMethodV2,
PositionChangedArg,
} from 'features/controlLayers/store/types';
import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
@ -71,73 +61,14 @@ export const regionsReducers = {
},
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 }>) => {
const { data } = action.payload;
state.regions.entities.push(data);
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) => {
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 }>) => {
const { id, prompt } = action.payload;
const rg = selectRG(state, id);
@ -286,44 +217,4 @@ export const regionsReducers = {
}
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>;

View File

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