feat(ui): iterate on filter UI, flow

This commit is contained in:
psychedelicious 2024-08-20 23:45:21 +10:00
parent 677dddcfc9
commit 7e400d876f
20 changed files with 266 additions and 200 deletions

View File

@ -1694,7 +1694,6 @@
"objects_zero": "empty",
"objects_one": "{{count}} object",
"objects_other": "{{count}} objects",
"filter": "Filter",
"convertToControlLayer": "Convert to Control Layer",
"convertToRasterLayer": "Convert to Raster Layer",
"enableTransparencyEffect": "Enable Transparency Effect",
@ -1723,6 +1722,14 @@
"solid": "Solid",
"checkerboard": "Checkerboard",
"dynamicGrid": "Dynamic Grid"
},
"filter": {
"filter": "Filter",
"filters": "Filters",
"filterType": "Filter Type",
"preview": "Preview",
"apply": "Apply",
"cancel": "Cancel"
}
},
"upscaling": {

View File

@ -6,10 +6,10 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { PartialAppConfig } from 'app/types/invokeai';
import ImageUploadOverlay from 'common/components/ImageUploadOverlay';
import { useScopeFocusWatcher } from 'common/hooks/interactionScopes';
import { useClearStorage } from 'common/hooks/useClearStorage';
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { useScopeFocusWatcher } from 'common/hooks/interactionScopes';
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';

View File

@ -2,8 +2,13 @@ import { createAction } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { ipaImageChanged, rasterLayerAdded, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import {
controlLayerAdded,
ipaImageChanged,
rasterLayerAdded,
rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
@ -99,7 +104,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
* Image dropped on Raster layer
*/
if (
overData.actionType === 'ADD_LAYER_FROM_IMAGE' &&
overData.actionType === 'ADD_RASTER_LAYER_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
@ -113,6 +118,24 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
return;
}
/**
* Image dropped on Raster layer
*/
if (
overData.actionType === 'ADD_CONTROL_LAYER_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = getState().canvasV2.bbox.rect;
const overrides: Partial<CanvasControlLayerState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(controlLayerAdded({ overrides, isSelected: true }));
return;
}
/**
* Image dropped on node image field
*/

View File

@ -33,28 +33,23 @@ type IAINoImageFallbackProps = FlexProps & {
};
export const IAINoContentFallback = memo((props: IAINoImageFallbackProps) => {
const { icon = PiImageBold, boxSize = 16, sx, ...rest } = props;
const styles = useMemo(
() => ({
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
flexDir: 'column',
gap: 2,
userSelect: 'none',
opacity: 0.7,
color: 'base.500',
fontSize: 'md',
...sx,
}),
[sx]
);
const { icon = PiImageBold, boxSize = 16, ...rest } = props;
return (
<Flex sx={styles} {...rest}>
<Flex
w="full"
h="full"
alignItems="center"
justifyContent="center"
borderRadius="base"
flexDir="column"
gap={2}
userSelect="none"
opacity={0.7}
color="base.500"
fontSize="md"
{...rest}
>
{icon && <Icon as={icon} boxSize={boxSize} opacity={0.7} />}
{props.label && <Text textAlign="center">{props.label}</Text>}
</Flex>

View File

@ -1,12 +1,17 @@
import { Flex } from '@invoke-ai/ui-library';
import IAIDroppable from 'common/components/IAIDroppable';
import type { AddLayerFromImageDropData } from 'features/dnd/types';
import type { AddControlLayerFromImageDropData, AddRasterLayerFromImageDropData } from 'features/dnd/types';
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo } from 'react';
const addLayerFromImageDropData: AddLayerFromImageDropData = {
id: 'add-layer-from-image-drop-data',
actionType: 'ADD_LAYER_FROM_IMAGE',
const addRasterLayerFromImageDropData: AddRasterLayerFromImageDropData = {
id: 'add-raster-layer-from-image-drop-data',
actionType: 'ADD_RASTER_LAYER_FROM_IMAGE',
};
const addControlLayerFromImageDropData: AddControlLayerFromImageDropData = {
id: 'add-control-layer-from-image-drop-data',
actionType: 'ADD_CONTROL_LAYER_FROM_IMAGE',
};
export const CanvasDropArea = memo(() => {
@ -17,9 +22,14 @@ export const CanvasDropArea = memo(() => {
}
return (
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
<IAIDroppable dropLabel="Create Layer" data={addLayerFromImageDropData} />
</Flex>
<>
<Flex position="absolute" top={0} right={0} bottom="50%" left={0} gap={2} pointerEvents="none">
<IAIDroppable dropLabel="Create Raster Layer" data={addRasterLayerFromImageDropData} />
</Flex>
<Flex position="absolute" top="50%" right={0} bottom={0} left={0} gap={2} pointerEvents="none">
<IAIDroppable dropLabel="Create Control Layer" data={addControlLayerFromImageDropData} />
</Flex>
</>
);
});

View File

@ -1,6 +1,9 @@
import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { $filterConfig, $filteringEntity } from 'features/controlLayers/store/canvasV2Slice';
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType';
@ -13,6 +16,7 @@ type Props = {
export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onChangeModel }: Props) => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base);
const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels();
const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]);
@ -23,8 +27,27 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
return;
}
onChangeModel(modelConfig);
// When we set the model for the first time, we'll set the default filter settings and open the filter popup
if (modelKey) {
// If there is already a model key, this is not the first time we're setting the model
return;
}
// Update the filter, preferring the model's default
if (isFilterType(modelConfig.default_settings?.preprocessor)) {
$filterConfig.set(IMAGE_FILTERS[modelConfig.default_settings.preprocessor].buildDefaults(modelConfig.base));
} else {
$filterConfig.set(IMAGE_FILTERS.canny_image_processor.buildDefaults(modelConfig.base));
}
// Open the filter popup by setting this entity as the filtering entity
if (!$filteringEntity.get()) {
$filteringEntity.set(entityIdentifier);
}
},
[onChangeModel]
[entityIdentifier, modelKey, onChangeModel]
);
const getIsDisabled = useCallback(

View File

@ -3,6 +3,7 @@ import { Flex } from '@invoke-ai/ui-library';
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar';
import { Filter } from 'features/controlLayers/components/Filters/Filter';
import { StageComponent } from 'features/controlLayers/components/StageComponent';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { memo, useRef } from 'react';
@ -31,6 +32,9 @@ export const CanvasEditor = memo(() => {
<Flex position="absolute" bottom={2} gap={2} align="center" justify="center">
<StagingAreaToolbar />
</Flex>
<Flex position="absolute" bottom={16}>
<Filter />
</Flex>
<CanvasDropArea />
</Flex>
);

View File

@ -1,29 +0,0 @@
/* eslint-disable i18next/no-literal-string */
import { useStore } from '@nanostores/react';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList';
import { Filter } from 'features/controlLayers/components/Filters/Filter';
import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { memo } from 'react';
import { Panel, PanelGroup } from 'react-resizable-panels';
export const ControlLayersPanelContent = memo(() => {
const filteringEntity = useStore($filteringEntity);
return (
<PanelGroup direction="vertical">
<Panel id="canvas-entity-list-panel" order={0}>
<CanvasEntityList />
</Panel>
{Boolean(filteringEntity) && (
<>
<ResizeHandle orientation="horizontal" />
<Panel id="filter-panel" order={1}>
<Filter />
</Panel>
</>
)}
</PanelGroup>
);
});
ControlLayersPanelContent.displayName = 'ControlLayersPanelContent';

View File

@ -1,15 +1,21 @@
import { Button, ButtonGroup, Flex } from '@invoke-ai/ui-library';
import { Button, ButtonGroup, Flex, Heading } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { FilterSettings } from 'features/controlLayers/components/Filters/FilterSettings';
import { FilterTypeSelect } from 'features/controlLayers/components/Filters/FilterTypeSelect';
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice';
import { $filterConfig, $filteringEntity, $isProcessingFilter } from 'features/controlLayers/store/canvasV2Slice';
import { type FilterConfig, IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCheckBold, PiShootingStarBold, PiXBold } from 'react-icons/pi';
export const Filter = memo(() => {
const { t } = useTranslation();
const filteringEntity = useStore($filteringEntity);
const filterConfig = useStore($filterConfig);
const isProcessingFilter = useStore($isProcessingFilter);
const preview = useCallback(() => {
const previewFilter = useCallback(() => {
if (!filteringEntity) {
return;
}
@ -24,7 +30,7 @@ export const Filter = memo(() => {
entity.adapter.filter.previewFilter();
}, [filteringEntity]);
const apply = useCallback(() => {
const applyFilter = useCallback(() => {
if (!filteringEntity) {
return;
}
@ -39,7 +45,7 @@ export const Filter = memo(() => {
entity.adapter.filter.applyFilter();
}, [filteringEntity]);
const cancel = useCallback(() => {
const cancelFilter = useCallback(() => {
if (!filteringEntity) {
return;
}
@ -54,21 +60,62 @@ export const Filter = memo(() => {
entity.adapter.filter.cancelFilter();
}, [filteringEntity]);
const onChangeFilterConfig = useCallback((filterConfig: FilterConfig) => {
$filterConfig.set(filterConfig);
}, []);
const onChangeFilterType = useCallback((filterType: FilterConfig['type']) => {
$filterConfig.set(IMAGE_FILTERS[filterType].buildDefaults());
}, []);
if (!filteringEntity || !filterConfig) {
return null;
}
return (
<Flex flexDir="column" gap={3} w="full" h="full">
<FilterTypeSelect />
<ButtonGroup isAttached={false}>
<Button onClick={preview} isDisabled={!filteringEntity}>
Preview
<Flex
bg="base.800"
borderRadius="base"
p={4}
flexDir="column"
gap={4}
w={420}
h="auto"
shadow="dark-lg"
transitionProperty="height"
transitionDuration="normal"
>
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.filter.filter')}
</Heading>
<FilterTypeSelect filterType={filterConfig.type} onChange={onChangeFilterType} />
<FilterSettings filterConfig={filterConfig} onChange={onChangeFilterConfig} />
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
<Button
leftIcon={<PiShootingStarBold />}
onClick={previewFilter}
isLoading={isProcessingFilter}
loadingText={t('controlLayers.filter.preview')}
>
{t('controlLayers.filter.preview')}
</Button>
<Button onClick={apply} isDisabled={!filteringEntity}>
Apply
<Button
leftIcon={<PiCheckBold />}
onClick={applyFilter}
isLoading={isProcessingFilter}
loadingText={t('controlLayers.filter.apply')}
>
{t('controlLayers.filter.apply')}
</Button>
<Button onClick={cancel} isDisabled={!filteringEntity}>
Cancel
<Button
leftIcon={<PiXBold />}
onClick={cancelFilter}
isLoading={isProcessingFilter}
loadingText={t('controlLayers.filter.cancel')}
>
{t('controlLayers.filter.cancel')}
</Button>
</ButtonGroup>
<FilterSettings />
</Flex>
);
});

View File

@ -1,4 +1,3 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { FilterCanny } from 'features/controlLayers/components/Filters/FilterCanny';
import { FilterColorMap } from 'features/controlLayers/components/Filters/FilterColorMap';
@ -11,67 +10,67 @@ import { FilterMediapipeFace } from 'features/controlLayers/components/Filters/F
import { FilterMidasDepth } from 'features/controlLayers/components/Filters/FilterMidasDepth';
import { FilterMlsdImage } from 'features/controlLayers/components/Filters/FilterMlsdImage';
import { FilterPidi } from 'features/controlLayers/components/Filters/FilterPidi';
import { filterConfigChanged } from 'features/controlLayers/store/canvasV2Slice';
import { type FilterConfig, IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import type { FilterConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export const FilterSettings = memo(() => {
const dispatch = useAppDispatch();
type Props = { filterConfig: FilterConfig; onChange: (filterConfig: FilterConfig) => void };
export const FilterSettings = memo(({ filterConfig, onChange }: Props) => {
const { t } = useTranslation();
const config = useAppSelector((s) => s.canvasV2.filter.config);
const updateFilter = useCallback(
(config: FilterConfig) => {
dispatch(filterConfigChanged({ config }));
},
[dispatch]
if (filterConfig.type === 'canny_image_processor') {
return <FilterCanny config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'color_map_image_processor') {
return <FilterColorMap config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'content_shuffle_image_processor') {
return <FilterContentShuffle config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'depth_anything_image_processor') {
return <FilterDepthAnything config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'dw_openpose_image_processor') {
return <FilterDWOpenpose config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'hed_image_processor') {
return <FilterHed config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'lineart_image_processor') {
return <FilterLineart config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'mediapipe_face_processor') {
return <FilterMediapipeFace config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'midas_depth_image_processor') {
return <FilterMidasDepth config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'mlsd_image_processor') {
return <FilterMlsdImage config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'pidi_image_processor') {
return <FilterPidi config={filterConfig} onChange={onChange} />;
}
return (
<IAINoContentFallback
py={4}
label={`${t(IMAGE_FILTERS[filterConfig.type].labelTKey)} has no settings`}
icon={null}
/>
);
if (config.type === 'canny_image_processor') {
return <FilterCanny config={config} onChange={updateFilter} />;
}
if (config.type === 'color_map_image_processor') {
return <FilterColorMap config={config} onChange={updateFilter} />;
}
if (config.type === 'content_shuffle_image_processor') {
return <FilterContentShuffle config={config} onChange={updateFilter} />;
}
if (config.type === 'depth_anything_image_processor') {
return <FilterDepthAnything config={config} onChange={updateFilter} />;
}
if (config.type === 'dw_openpose_image_processor') {
return <FilterDWOpenpose config={config} onChange={updateFilter} />;
}
if (config.type === 'hed_image_processor') {
return <FilterHed config={config} onChange={updateFilter} />;
}
if (config.type === 'lineart_image_processor') {
return <FilterLineart config={config} onChange={updateFilter} />;
}
if (config.type === 'mediapipe_face_processor') {
return <FilterMediapipeFace config={config} onChange={updateFilter} />;
}
if (config.type === 'midas_depth_image_processor') {
return <FilterMidasDepth config={config} onChange={updateFilter} />;
}
if (config.type === 'mlsd_image_processor') {
return <FilterMlsdImage config={config} onChange={updateFilter} />;
}
if (config.type === 'pidi_image_processor') {
return <FilterPidi config={config} onChange={updateFilter} />;
}
return <IAINoContentFallback label={`${t(IMAGE_FILTERS[config.type].labelTKey)} has no settings`} icon={null} />;
});
FilterSettings.displayName = 'Filter';

View File

@ -1,9 +1,9 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { filterSelected } from 'features/controlLayers/store/canvasV2Slice';
import type { FilterConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
import { configSelector } from 'features/system/store/configSelectors';
import { includes, map } from 'lodash-es';
@ -16,10 +16,13 @@ const selectDisabledProcessors = createMemoizedSelector(
(config) => config.sd.disabledControlNetProcessors
);
export const FilterTypeSelect = memo(() => {
type Props = {
filterType: FilterConfig['type'];
onChange: (filterType: FilterConfig['type']) => void;
};
export const FilterTypeSelect = memo(({ filterType, onChange }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const filterType = useAppSelector((s) => s.canvasV2.filter.config.type);
const disabledProcessors = useAppSelector(selectDisabledProcessors);
const options = useMemo(() => {
return map(IMAGE_FILTERS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter(
@ -33,9 +36,9 @@ export const FilterTypeSelect = memo(() => {
return;
}
assert(isFilterType(v.value));
dispatch(filterSelected({ type: v.value }));
onChange(v.value);
},
[dispatch]
[onChange]
);
const value = useMemo(() => options.find((o) => o.value === filterType) ?? null, [options, filterType]);
@ -43,7 +46,7 @@ export const FilterTypeSelect = memo(() => {
<Flex gap={2}>
<FormControl>
<InformationalPopover feature="controlNetProcessor">
<FormLabel m={0}>{t('controlLayers.filter')}</FormLabel>
<FormLabel m={0}>{t('controlLayers.filter.filterType')}</FormLabel>
</InformationalPopover>
<Combobox value={value} options={options} onChange={_onChange} isSearchable={false} isClearable={false} />
</FormControl>

View File

@ -14,7 +14,7 @@ export const CanvasEntityMenuItemsFilter = memo(() => {
return (
<MenuItem onClick={filter} icon={<PiShootingStarBold />}>
{t('controlLayers.filter')}
{t('controlLayers.filter.filter')}
</MenuItem>
);
});

View File

@ -1,5 +1,4 @@
import type { JSONObject } from 'common/types';
import { parseify } from 'common/util/serialize';
import type { JSONObject, SerializableObject } from 'common/types';
import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
@ -35,10 +34,10 @@ export class CanvasFilter {
}
previewFilter = async () => {
const { config } = this.manager.stateApi.getFilterState();
const config = this.manager.stateApi.$filterConfig.get();
this.log.trace({ config }, 'Previewing filter');
const dispatch = this.manager.stateApi._store.dispatch;
const rect = this.parent.transformer.getRelativeRect()
const rect = this.parent.transformer.getRelativeRect();
const imageDTO = await this.parent.renderer.rasterize(rect, false);
// TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now
const filterNode = IMAGE_FILTERS[config.type].buildNode(imageDTO, config as never);
@ -66,22 +65,30 @@ export class CanvasFilter {
if (event.origin !== this.id || event.invocation_source_id !== filterNode.id) {
return;
}
this.log.trace({ event: parseify(event) }, 'Handling filter processing completion');
this.manager.socket.off('invocation_complete', listener);
this.log.trace({ event } as SerializableObject, 'Handling filter processing completion');
const { result } = event;
assert(result.type === 'image_output', `Processor did not return an image output, got: ${result}`);
const imageDTO = await getImageDTO(result.image.image_name);
assert(imageDTO, "Failed to fetch processor output's image DTO");
this.imageState = imageDTOToImageObject(imageDTO);
this.parent.renderer.clearBuffer();
await this.parent.renderer.setBuffer(this.imageState);
this.parent.renderer.hideObjects([this.imageState.id]);
this.manager.socket.off('invocation_complete', listener);
this.parent.renderer.hideObjects();
this.manager.stateApi.$isProcessingFilter.set(false);
};
this.manager.socket.on('invocation_complete', listener);
this.log.trace({ enqueueBatchArg: parseify(enqueueBatchArg) }, 'Enqueuing filter batch');
this.log.trace({ enqueueBatchArg } as SerializableObject, 'Enqueuing filter batch');
this.manager.stateApi.$isProcessingFilter.set(true);
dispatch(
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
fixedCacheKey: 'enqueueBatch',
@ -119,6 +126,7 @@ export class CanvasFilter {
this.parent.renderer.showObjects();
this.manager.stateApi.$filteringEntity.set(null);
this.imageState = null;
this.manager.stateApi.$isProcessingFilter.set(false);
};
destroy = () => {

View File

@ -4,9 +4,11 @@ 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 {
$filterConfig,
$filteringEntity,
$isDrawing,
$isMouseDown,
$isProcessingFilter,
$lastAddedPoint,
$lastCursorPos,
$lastMouseDownPos,
@ -161,9 +163,6 @@ export class CanvasStateApi {
getIsSelected = (id: string) => {
return this.getState().selectedEntityIdentifier?.id === id;
};
getLogLevel = () => {
return this._store.getState().system.consoleLogLevel;
};
getFilterState = () => {
return this._store.getState().canvasV2.filter;
};
@ -234,6 +233,8 @@ export class CanvasStateApi {
$transformingEntity = $transformingEntity;
$filteringEntity = $filteringEntity;
$filterConfig = $filterConfig;
$isProcessingFilter = $isProcessingFilter;
$toolState: WritableAtom<CanvasV2State['tool']> = atom();
$currentFill: WritableAtom<RgbaColor> = atom();

View File

@ -143,10 +143,6 @@ const initialState: CanvasV2State = {
stagedImages: [],
selectedStagedImageIndex: 0,
},
filter: {
autoProcess: true,
config: IMAGE_FILTERS.canny_image_processor.buildDefaults(),
},
};
export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) {
@ -486,12 +482,6 @@ export const canvasV2Slice = createSlice({
state.inpaintMask = deepClone(initialState.inpaintMask);
state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier);
},
filterSelected: (state, action: PayloadAction<{ type: FilterConfig['type'] }>) => {
state.filter.config = IMAGE_FILTERS[action.payload.type].buildDefaults();
},
filterConfigChanged: (state, action: PayloadAction<{ config: FilterConfig }>) => {
state.filter.config = action.payload.config;
},
rasterizationCachesInvalidated: (state) => {
// Invalidate the rasterization caches for all entities.
@ -669,9 +659,6 @@ export const {
sessionStagingAreaReset,
sessionNextStagedImageSelected,
sessionPrevStagedImageSelected,
// Filter
filterSelected,
filterConfigChanged,
} = canvasV2Slice.actions;
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;
@ -699,6 +686,8 @@ export const $lastCursorPos = atom<Coordinate | null>(null);
export const $spaceKey = atom<boolean>(false);
export const $transformingEntity = atom<CanvasEntityIdentifier | null>(null);
export const $filteringEntity = atom<CanvasEntityIdentifier | null>(null);
export const $filterConfig = atom<FilterConfig>(IMAGE_FILTERS.canny_image_processor.buildDefaults());
export const $isProcessingFilter = atom(false);
export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {
name: canvasV2Slice.name,

View File

@ -4,14 +4,8 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import { isEqual, merge } from 'lodash-es';
import { assert } from 'tsafe';
import type {
CanvasControlLayerState,
CanvasRasterLayerState,
CanvasV2State,
ControlNetConfig,
Rect,
T2IAdapterConfig,
} from './types';
import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State, Rect } from './types';
import { initialControlNetV2 } from './types';
export const selectRasterLayer = (state: CanvasV2State, id: string) =>
state.rasterLayers.entities.find((layer) => layer.id === id);
@ -73,11 +67,8 @@ export const rasterLayersReducers = {
state.rasterLayers.compositeRasterizationCache.push(action.payload);
},
rasterLayerConvertedToControlLayer: {
reducer: (
state,
action: PayloadAction<{ id: string; newId: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }>
) => {
const { id, newId, controlAdapter } = action.payload;
reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => {
const { id, newId } = action.payload;
const layer = selectRasterLayer(state, id);
if (!layer) {
return;
@ -88,7 +79,7 @@ export const rasterLayersReducers = {
...deepClone(layer),
id: newId,
type: 'control_layer',
controlAdapter,
controlAdapter: deepClone(initialControlNetV2),
withTransparencyEffect: true,
};
@ -103,7 +94,7 @@ export const rasterLayersReducers = {
state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id };
},
prepare: (payload: { id: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }) => ({
prepare: (payload: { id: string }) => ({
payload: { ...payload, newId: getPrefixedId('control_layer') },
}),
},

View File

@ -930,10 +930,6 @@ export type CanvasV2State = {
stagedImages: StagingAreaImage[];
selectedStagedImageIndex: number;
};
filter: {
autoProcess: boolean;
config: FilterConfig;
};
};
export type StageAttrs = {

View File

@ -44,8 +44,12 @@ export type RGIPAdapterImageDropData = BaseDropData & {
};
};
export type AddLayerFromImageDropData = BaseDropData & {
actionType: 'ADD_LAYER_FROM_IMAGE';
export type AddRasterLayerFromImageDropData = BaseDropData & {
actionType: 'ADD_RASTER_LAYER_FROM_IMAGE';
};
export type AddControlLayerFromImageDropData = BaseDropData & {
actionType: 'ADD_CONTROL_LAYER_FROM_IMAGE';
};
type UpscaleInitialImageDropData = BaseDropData & {
@ -91,7 +95,8 @@ export type TypesafeDroppableData =
| RGIPAdapterImageDropData
| SelectForCompareDropData
| UpscaleInitialImageDropData
| AddLayerFromImageDropData;
| AddRasterLayerFromImageDropData
| AddControlLayerFromImageDropData;
type BaseDragData = {
id: string;

View File

@ -14,19 +14,13 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData?
switch (actionType) {
case 'SET_CURRENT_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_CA_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_IPA_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_RG_IP_ADAPTER_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'ADD_LAYER_FROM_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'ADD_RASTER_LAYER_FROM_IMAGE':
case 'ADD_CONTROL_LAYER_FROM_IMAGE':
case 'SET_UPSCALE_INITIAL_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_NODES_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SELECT_FOR_COMPARE':
return payloadType === 'IMAGE_DTO';
case 'ADD_TO_BOARD': {

View File

@ -4,7 +4,7 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton';
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList';
import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice';
import { selectEntityCount } from 'features/controlLayers/store/selectors';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
@ -115,7 +115,7 @@ const ParametersPanelTextToImage = () => {
</Flex>
</TabPanel>
<TabPanel p={0} w="full" h="full">
<ControlLayersPanelContent />
<CanvasEntityList />
</TabPanel>
</TabPanels>
</Tabs>