Merge branch 'main' into sdxl_t2i_bgr

This commit is contained in:
dunkeroni
2024-10-25 23:44:13 -04:00
committed by GitHub
20 changed files with 145 additions and 89 deletions

View File

@@ -1682,6 +1682,8 @@
"controlLayer": "Control Layer", "controlLayer": "Control Layer",
"inpaintMask": "Inpaint Mask", "inpaintMask": "Inpaint Mask",
"regionalGuidance": "Regional Guidance", "regionalGuidance": "Regional Guidance",
"canvasAsRasterLayer": "$t(controlLayers.canvas) as $t(controlLayers.rasterLayer)",
"canvasAsControlLayer": "$t(controlLayers.canvas) as $t(controlLayers.controlLayer)",
"referenceImage": "Reference Image", "referenceImage": "Reference Image",
"regionalReferenceImage": "Regional Reference Image", "regionalReferenceImage": "Regional Reference Image",
"globalReferenceImage": "Global Reference Image", "globalReferenceImage": "Global Reference Image",
@@ -1894,6 +1896,7 @@
"include": "Include", "include": "Include",
"exclude": "Exclude", "exclude": "Exclude",
"neutral": "Neutral", "neutral": "Neutral",
"apply": "Apply",
"reset": "Reset", "reset": "Reset",
"saveAs": "Save As", "saveAs": "Save As",
"cancel": "Cancel", "cancel": "Cancel",

View File

@@ -13,7 +13,7 @@ export const CanvasAlertsPreserveMask = memo(() => {
} }
return ( return (
<Alert status="warning" borderRadius="base" fontSize="sm" shadow="md" w="fit-content" alignSelf="flex-end"> <Alert status="warning" borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
<AlertIcon /> <AlertIcon />
<AlertTitle>{t('controlLayers.settings.preserveMask.alert')}</AlertTitle> <AlertTitle>{t('controlLayers.settings.preserveMask.alert')}</AlertTitle>
</Alert> </Alert>

View File

@@ -98,7 +98,7 @@ const CanvasAlertsSelectedEntityStatusContent = memo(({ entityIdentifier, adapte
} }
return ( return (
<Alert status={alert.status} borderRadius="base" fontSize="sm" shadow="md" w="fit-content" alignSelf="flex-end"> <Alert status={alert.status} borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
<AlertIcon /> <AlertIcon />
<AlertTitle>{alert.title}</AlertTitle> <AlertTitle>{alert.title}</AlertTitle>
</Alert> </Alert>

View File

@@ -132,7 +132,6 @@ const AlertWrapper = ({
fontSize="sm" fontSize="sm"
shadow="md" shadow="md"
w="fit-content" w="fit-content"
alignSelf="flex-end"
> >
<Flex w="full" alignItems="center"> <Flex w="full" alignItems="center">
<AlertIcon /> <AlertIcon />

View File

@@ -1,3 +1,4 @@
import { MenuGroup } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems'; import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems';
import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems'; import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems';
@@ -8,7 +9,9 @@ import {
EntityIdentifierContext, EntityIdentifierContext,
useEntityIdentifierContext, useEntityIdentifierContext,
} from 'features/controlLayers/contexts/EntityIdentifierContext'; } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityTypeString } from 'features/controlLayers/hooks/useEntityTypeString';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import type { PropsWithChildren } from 'react';
import { memo } from 'react'; import { memo } from 'react';
import type { Equals } from 'tsafe'; import type { Equals } from 'tsafe';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
@@ -46,9 +49,20 @@ export const CanvasContextMenuSelectedEntityMenuItems = memo(() => {
return ( return (
<EntityIdentifierContext.Provider value={selectedEntityIdentifier}> <EntityIdentifierContext.Provider value={selectedEntityIdentifier}>
<CanvasContextMenuSelectedEntityMenuItemsContent /> <CanvasContextMenuSelectedEntityMenuGroup>
<CanvasContextMenuSelectedEntityMenuItemsContent />
</CanvasContextMenuSelectedEntityMenuGroup>
</EntityIdentifierContext.Provider> </EntityIdentifierContext.Provider>
); );
}); });
CanvasContextMenuSelectedEntityMenuItems.displayName = 'CanvasContextMenuSelectedEntityMenuItems'; CanvasContextMenuSelectedEntityMenuItems.displayName = 'CanvasContextMenuSelectedEntityMenuItems';
const CanvasContextMenuSelectedEntityMenuGroup = memo((props: PropsWithChildren) => {
const entityIdentifier = useEntityIdentifierContext();
const title = useEntityTypeString(entityIdentifier.type);
return <MenuGroup title={title}>{props.children}</MenuGroup>;
});
CanvasContextMenuSelectedEntityMenuGroup.displayName = 'CanvasContextMenuSelectedEntityMenuGroup';

View File

@@ -71,12 +71,16 @@ export const CanvasMainPanelContent = memo(() => {
> >
<InvokeCanvasComponent /> <InvokeCanvasComponent />
<CanvasManagerProviderGate> <CanvasManagerProviderGate>
{showHUD && ( <Flex
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none"> position="absolute"
<CanvasHUD /> flexDir="column"
</Flex> top={1}
)} insetInlineStart={1}
<Flex flexDir="column" position="absolute" top={1} insetInlineEnd={1} pointerEvents="none" gap={2}> pointerEvents="none"
gap={2}
alignItems="flex-start"
>
{showHUD && <CanvasHUD />}
<CanvasAlertsSelectedEntityStatus /> <CanvasAlertsSelectedEntityStatus />
<CanvasAlertsPreserveMask /> <CanvasAlertsPreserveMask />
<CanvasAlertsSendingToGallery /> <CanvasAlertsSendingToGallery />

View File

@@ -27,11 +27,10 @@ export const ControlLayerMenuItems = memo(() => {
<CanvasEntityMenuItemsSelectObject /> <CanvasEntityMenuItemsSelectObject />
<ControlLayerMenuItemsTransparencyEffect /> <ControlLayerMenuItemsTransparencyEffect />
<MenuDivider /> <MenuDivider />
<ControlLayerMenuItemsCopyToSubMenu />
<ControlLayerMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox /> <CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsSave /> <CanvasEntityMenuItemsSave />
<MenuDivider />
<ControlLayerMenuItemsConvertToSubMenu />
<ControlLayerMenuItemsCopyToSubMenu />
</> </>
); );
}); });

View File

@@ -8,6 +8,7 @@ import {
MenuItem, MenuItem,
MenuList, MenuList,
Spacer, Spacer,
Spinner,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
@@ -25,7 +26,7 @@ import { IMAGE_FILTERS } from 'features/controlLayers/store/filters';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useCallback, useMemo, useRef } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiFloppyDiskBold, PiPlayFill, PiXBold } from 'react-icons/pi'; import { PiCaretDownBold } from 'react-icons/pi';
const FilterContent = memo( const FilterContent = memo(
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => { ({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
@@ -115,39 +116,41 @@ const FilterContent = memo(
<ButtonGroup isAttached={false} size="sm" w="full"> <ButtonGroup isAttached={false} size="sm" w="full">
<Button <Button
variant="ghost" variant="ghost"
leftIcon={<PiPlayFill />}
onClick={adapter.filterer.processImmediate} onClick={adapter.filterer.processImmediate}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.process')} loadingText={t('controlLayers.filter.process')}
isDisabled={!isValid || autoProcess} isDisabled={isProcessing || !isValid || autoProcess}
> >
{t('controlLayers.filter.process')} {t('controlLayers.filter.process')}
{isProcessing && <Spinner ms={3} boxSize={5} color="base.600" />}
</Button> </Button>
<Spacer /> <Spacer />
<Button <Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.filterer.reset} onClick={adapter.filterer.reset}
isLoading={isProcessing} isDisabled={isProcessing}
loadingText={t('controlLayers.filter.reset')} loadingText={t('controlLayers.filter.reset')}
variant="ghost" variant="ghost"
> >
{t('controlLayers.filter.reset')} {t('controlLayers.filter.reset')}
</Button> </Button>
<Button
onClick={adapter.filterer.apply}
loadingText={t('controlLayers.filter.apply')}
variant="ghost"
isDisabled={isProcessing || !isValid || !hasProcessed}
>
{t('controlLayers.filter.apply')}
</Button>
<Menu> <Menu>
<MenuButton <MenuButton
as={Button} as={Button}
leftIcon={<PiFloppyDiskBold />}
isLoading={isProcessing}
loadingText={t('controlLayers.selectObject.saveAs')} loadingText={t('controlLayers.selectObject.saveAs')}
variant="ghost" variant="ghost"
isDisabled={!isValid || !hasProcessed} isDisabled={isProcessing || !isValid || !hasProcessed}
rightIcon={<PiCaretDownBold />}
> >
{t('controlLayers.selectObject.saveAs')} {t('controlLayers.selectObject.saveAs')}
</MenuButton> </MenuButton>
<MenuList> <MenuList>
<MenuItem isDisabled={!isValid || !hasProcessed} onClick={adapter.filterer.apply}>
{t('controlLayers.replaceCurrent')}
</MenuItem>
<MenuItem isDisabled={!isValid || !hasProcessed} onClick={saveAsInpaintMask}> <MenuItem isDisabled={!isValid || !hasProcessed} onClick={saveAsInpaintMask}>
{t('controlLayers.newInpaintMask')} {t('controlLayers.newInpaintMask')}
</MenuItem> </MenuItem>
@@ -162,12 +165,7 @@ const FilterContent = memo(
</MenuItem> </MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
<Button <Button variant="ghost" onClick={adapter.filterer.cancel} loadingText={t('controlLayers.filter.cancel')}>
variant="ghost"
leftIcon={<PiXBold />}
onClick={adapter.filterer.cancel}
loadingText={t('controlLayers.filter.cancel')}
>
{t('controlLayers.filter.cancel')} {t('controlLayers.filter.cancel')}
</Button> </Button>
</ButtonGroup> </ButtonGroup>

View File

@@ -20,10 +20,9 @@ export const InpaintMaskMenuItems = memo(() => {
<MenuDivider /> <MenuDivider />
<CanvasEntityMenuItemsTransform /> <CanvasEntityMenuItemsTransform />
<MenuDivider /> <MenuDivider />
<CanvasEntityMenuItemsCropToBbox />
<MenuDivider />
<InpaintMaskMenuItemsConvertToSubMenu />
<InpaintMaskMenuItemsCopyToSubMenu /> <InpaintMaskMenuItemsCopyToSubMenu />
<InpaintMaskMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
</> </>
); );
}); });

View File

@@ -25,11 +25,10 @@ export const RasterLayerMenuItems = memo(() => {
<CanvasEntityMenuItemsFilter /> <CanvasEntityMenuItemsFilter />
<CanvasEntityMenuItemsSelectObject /> <CanvasEntityMenuItemsSelectObject />
<MenuDivider /> <MenuDivider />
<RasterLayerMenuItemsCopyToSubMenu />
<RasterLayerMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox /> <CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsSave /> <CanvasEntityMenuItemsSave />
<MenuDivider />
<RasterLayerMenuItemsConvertToSubMenu />
<RasterLayerMenuItemsCopyToSubMenu />
</> </>
); );
}); });

View File

@@ -25,10 +25,9 @@ export const RegionalGuidanceMenuItems = memo(() => {
<CanvasEntityMenuItemsTransform /> <CanvasEntityMenuItemsTransform />
<RegionalGuidanceMenuItemsAutoNegative /> <RegionalGuidanceMenuItemsAutoNegative />
<MenuDivider /> <MenuDivider />
<CanvasEntityMenuItemsCropToBbox />
<MenuDivider />
<RegionalGuidanceMenuItemsConvertToSubMenu />
<RegionalGuidanceMenuItemsCopyToSubMenu /> <RegionalGuidanceMenuItemsCopyToSubMenu />
<RegionalGuidanceMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
</> </>
); );
}); });

View File

@@ -10,6 +10,7 @@ import {
MenuItem, MenuItem,
MenuList, MenuList,
Spacer, Spacer,
Spinner,
Text, Text,
Tooltip, Tooltip,
UnorderedList, UnorderedList,
@@ -29,7 +30,7 @@ import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/us
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { memo, useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiFloppyDiskBold, PiInfoBold, PiPlayFill, PiXBold } from 'react-icons/pi'; import { PiCaretDownBold, PiInfoBold } from 'react-icons/pi';
const SelectObjectContent = memo( const SelectObjectContent = memo(
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => { ({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
@@ -42,10 +43,6 @@ const SelectObjectContent = memo(
const hasImageState = useStore(adapter.segmentAnything.$hasImageState); const hasImageState = useStore(adapter.segmentAnything.$hasImageState);
const autoProcess = useAppSelector(selectAutoProcess); const autoProcess = useAppSelector(selectAutoProcess);
const replaceCurrent = useCallback(() => {
adapter.segmentAnything.apply();
}, [adapter.segmentAnything]);
const saveAsInpaintMask = useCallback(() => { const saveAsInpaintMask = useCallback(() => {
adapter.segmentAnything.saveAs('inpaint_mask'); adapter.segmentAnything.saveAs('inpaint_mask');
}, [adapter.segmentAnything]); }, [adapter.segmentAnything]);
@@ -115,58 +112,59 @@ const SelectObjectContent = memo(
<ButtonGroup isAttached={false} size="sm" w="full"> <ButtonGroup isAttached={false} size="sm" w="full">
<Button <Button
leftIcon={<PiPlayFill />}
onClick={adapter.segmentAnything.processImmediate} onClick={adapter.segmentAnything.processImmediate}
isLoading={isProcessing}
loadingText={t('controlLayers.selectObject.process')} loadingText={t('controlLayers.selectObject.process')}
variant="ghost" variant="ghost"
isDisabled={!hasPoints || autoProcess} isDisabled={isProcessing || !hasPoints || autoProcess}
> >
{t('controlLayers.selectObject.process')} {t('controlLayers.selectObject.process')}
{isProcessing && <Spinner ms={3} boxSize={5} color="base.600" />}
</Button> </Button>
<Spacer /> <Spacer />
<Button <Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.segmentAnything.reset} onClick={adapter.segmentAnything.reset}
isLoading={isProcessing} isDisabled={isProcessing || !hasPoints}
loadingText={t('controlLayers.selectObject.reset')} loadingText={t('controlLayers.selectObject.reset')}
variant="ghost" variant="ghost"
> >
{t('controlLayers.selectObject.reset')} {t('controlLayers.selectObject.reset')}
</Button> </Button>
<Button
onClick={adapter.segmentAnything.apply}
loadingText={t('controlLayers.selectObject.apply')}
variant="ghost"
isDisabled={isProcessing || !hasImageState}
>
{t('controlLayers.selectObject.apply')}
</Button>
<Menu> <Menu>
<MenuButton <MenuButton
as={Button} as={Button}
leftIcon={<PiFloppyDiskBold />}
isLoading={isProcessing}
loadingText={t('controlLayers.selectObject.saveAs')} loadingText={t('controlLayers.selectObject.saveAs')}
variant="ghost" variant="ghost"
isDisabled={!hasImageState} isDisabled={isProcessing || !hasImageState}
rightIcon={<PiCaretDownBold />}
> >
{t('controlLayers.selectObject.saveAs')} {t('controlLayers.selectObject.saveAs')}
</MenuButton> </MenuButton>
<MenuList> <MenuList>
<MenuItem isDisabled={!hasImageState} onClick={replaceCurrent}> <MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsInpaintMask}>
{t('controlLayers.replaceCurrent')}
</MenuItem>
<MenuItem isDisabled={!hasImageState} onClick={saveAsInpaintMask}>
{t('controlLayers.newInpaintMask')} {t('controlLayers.newInpaintMask')}
</MenuItem> </MenuItem>
<MenuItem isDisabled={!hasImageState} onClick={saveAsRegionalGuidance}> <MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsRegionalGuidance}>
{t('controlLayers.newRegionalGuidance')} {t('controlLayers.newRegionalGuidance')}
</MenuItem> </MenuItem>
<MenuItem isDisabled={!hasImageState} onClick={saveAsControlLayer}> <MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsControlLayer}>
{t('controlLayers.newControlLayer')} {t('controlLayers.newControlLayer')}
</MenuItem> </MenuItem>
<MenuItem isDisabled={!hasImageState} onClick={saveAsRasterLayer}> <MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsRasterLayer}>
{t('controlLayers.newRasterLayer')} {t('controlLayers.newRasterLayer')}
</MenuItem> </MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
<Button <Button
leftIcon={<PiXBold />}
onClick={adapter.segmentAnything.cancel} onClick={adapter.segmentAnything.cancel}
isLoading={isProcessing} isDisabled={isProcessing}
loadingText={t('common.cancel')} loadingText={t('common.cancel')}
variant="ghost" variant="ghost"
> >

View File

@@ -1,4 +1,4 @@
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library'; import { Button, ButtonGroup, Flex, Heading, Spacer, Spinner } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus'; import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
import { CanvasOperationIsolatedLayerPreviewSwitch } from 'features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch'; import { CanvasOperationIsolatedLayerPreviewSwitch } from 'features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch';
@@ -8,7 +8,6 @@ import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEnt
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useRef } from 'react'; import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiXBold } from 'react-icons/pi';
const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => { const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -62,30 +61,28 @@ const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) =>
<TransformFitToBboxButtons adapter={adapter} /> <TransformFitToBboxButtons adapter={adapter} />
<ButtonGroup isAttached={false} size="sm" w="full"> <ButtonGroup isAttached={false} size="sm" w="full" alignItems="center">
{isProcessing && <Spinner ms={3} boxSize={5} color="base.600" />}
<Spacer /> <Spacer />
<Button <Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.transformer.resetTransform} onClick={adapter.transformer.resetTransform}
isLoading={isProcessing} isDisabled={isProcessing}
loadingText={t('controlLayers.transform.reset')} loadingText={t('controlLayers.transform.reset')}
variant="ghost" variant="ghost"
> >
{t('controlLayers.transform.reset')} {t('controlLayers.transform.reset')}
</Button> </Button>
<Button <Button
leftIcon={<PiCheckBold />}
onClick={adapter.transformer.applyTransform} onClick={adapter.transformer.applyTransform}
isLoading={isProcessing} isDisabled={isProcessing}
loadingText={t('controlLayers.transform.apply')} loadingText={t('controlLayers.transform.apply')}
variant="ghost" variant="ghost"
> >
{t('controlLayers.transform.apply')} {t('controlLayers.transform.apply')}
</Button> </Button>
<Button <Button
leftIcon={<PiXBold />}
onClick={adapter.transformer.stopTransform} onClick={adapter.transformer.stopTransform}
isLoading={isProcessing} isDisabled={isProcessing}
loadingText={t('common.cancel')} loadingText={t('common.cancel')}
variant="ghost" variant="ghost"
> >

View File

@@ -4,7 +4,6 @@ import { useStore } from '@nanostores/react';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiArrowsOutBold } from 'react-icons/pi';
import type { Equals } from 'tsafe'; import type { Equals } from 'tsafe';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import { z } from 'zod'; import { z } from 'zod';
@@ -60,10 +59,9 @@ export const TransformFitToBboxButtons = memo(({ adapter }: { adapter: CanvasEnt
<Combobox options={options} value={value} onChange={onChange} isSearchable={false} isClearable={false} /> <Combobox options={options} value={value} onChange={onChange} isSearchable={false} isClearable={false} />
</FormControl> </FormControl>
<Button <Button
leftIcon={<PiArrowsOutBold />}
size="sm" size="sm"
onClick={onClick} onClick={onClick}
isLoading={isProcessing} isDisabled={isProcessing}
loadingText={t('controlLayers.transform.fitToBbox')} loadingText={t('controlLayers.transform.fitToBbox')}
variant="ghost" variant="ghost"
> >

View File

@@ -46,6 +46,8 @@ import { useCallback } from 'react';
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models'; import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig } from 'services/api/types'; import { isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
export const selectDefaultControlAdapter = createSelector( export const selectDefaultControlAdapter = createSelector(
selectModelConfigsQuery, selectModelConfigsQuery,
@@ -194,18 +196,31 @@ export const useNewCanvasFromImage = () => {
const bboxRect = useAppSelector(selectBboxRect); const bboxRect = useAppSelector(selectBboxRect);
const base = useAppSelector(selectBboxModelBase); const base = useAppSelector(selectBboxModelBase);
const func = useCallback( const func = useCallback(
(imageDTO: ImageDTO) => { (imageDTO: ImageDTO, type: CanvasRasterLayerState['type'] | CanvasControlLayerState['type']) => {
// Calculate the new bbox dimensions to fit the image's aspect ratio at the optimal size // Calculate the new bbox dimensions to fit the image's aspect ratio at the optimal size
const ratio = imageDTO.width / imageDTO.height; const ratio = imageDTO.width / imageDTO.height;
const optimalDimension = getOptimalDimension(base); const optimalDimension = getOptimalDimension(base);
const { width, height } = calculateNewSize(ratio, optimalDimension ** 2, base); const { width, height } = calculateNewSize(ratio, optimalDimension ** 2, base);
// The overrides need to include the layer's ID so we can transform the layer it is initialized // The overrides need to include the layer's ID so we can transform the layer it is initialized
const overrides = { let overrides: Partial<CanvasRasterLayerState> | Partial<CanvasControlLayerState>;
id: getPrefixedId('raster_layer'),
position: { x: bboxRect.x, y: bboxRect.y }, if (type === 'raster_layer') {
objects: [imageDTOToImageObject(imageDTO)], overrides = {
} satisfies Partial<CanvasRasterLayerState>; id: getPrefixedId('raster_layer'),
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageDTOToImageObject(imageDTO)],
} satisfies Partial<CanvasRasterLayerState>;
} else if (type === 'control_layer') {
overrides = {
id: getPrefixedId('control_layer'),
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageDTOToImageObject(imageDTO)],
} satisfies Partial<CanvasControlLayerState>;
} else {
// Catch unhandled types
assert<Equals<typeof type, never>>(false);
}
CanvasEntityAdapterBase.registerInitCallback(async (adapter) => { CanvasEntityAdapterBase.registerInitCallback(async (adapter) => {
// Skip the callback if the adapter is not the one we are creating // Skip the callback if the adapter is not the one we are creating
@@ -222,7 +237,16 @@ export const useNewCanvasFromImage = () => {
dispatch(canvasReset()); dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful! // The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
// The type casts are safe because the type is checked above
if (type === 'raster_layer') {
dispatch(rasterLayerAdded({ overrides: overrides as Partial<CanvasRasterLayerState>, isSelected: true }));
} else if (type === 'control_layer') {
dispatch(controlLayerAdded({ overrides: overrides as Partial<CanvasControlLayerState>, isSelected: true }));
} else {
// Catch unhandled types
assert<Equals<typeof type, never>>(false);
}
}, },
[base, bboxRect.x, bboxRect.y, dispatch] [base, bboxRect.x, bboxRect.y, dispatch]
); );

View File

@@ -52,8 +52,9 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
if (!adapter) { if (!adapter) {
return; return;
} }
imageViewer.close();
await adapter.transformer.startTransform(); await adapter.transformer.startTransform();
}, [isDisabled, entityIdentifier, canvasManager]); }, [isDisabled, entityIdentifier, canvasManager, imageViewer]);
const fitToBbox = useCallback(async () => { const fitToBbox = useCallback(async () => {
if (isDisabled) { if (isDisabled) {

View File

@@ -51,10 +51,16 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
/** /**
* Sync the $isStaging flag with the redux state. $isStaging is used by the manager to determine the global busy * Sync the $isStaging flag with the redux state. $isStaging is used by the manager to determine the global busy
* state of the canvas. * state of the canvas.
*
* We also set the $shouldShowStagedImage flag when we enter staging mode, so that the staged images are shown,
* even if the user disabled this in the last staging session.
*/ */
this.subscriptions.add( this.subscriptions.add(
this.manager.stateApi.createStoreSubscription(selectIsStaging, (isStaging) => { this.manager.stateApi.createStoreSubscription(selectIsStaging, (isStaging, oldIsStaging) => {
this.$isStaging.set(isStaging); this.$isStaging.set(isStaging);
if (isStaging && !oldIsStaging) {
this.$shouldShowStagedImage.set(true);
}
}) })
); );
} }

View File

@@ -32,8 +32,19 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => {
const newRegionalGuidanceFromImage = useNewRegionalGuidanceFromImage(); const newRegionalGuidanceFromImage = useNewRegionalGuidanceFromImage();
const newCanvasFromImage = useNewCanvasFromImage(); const newCanvasFromImage = useNewCanvasFromImage();
const onClickNewCanvasFromImage = useCallback(() => { const onClickNewCanvasWithRasterLayerFromImage = useCallback(() => {
newCanvasFromImage(imageDTO); newCanvasFromImage(imageDTO, 'raster_layer');
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [dispatch, imageDTO, imageViewer, newCanvasFromImage, t]);
const onClickNewCanvasWithControlLayerFromImage = useCallback(() => {
newCanvasFromImage(imageDTO, 'control_layer');
dispatch(setActiveTab('canvas')); dispatch(setActiveTab('canvas'));
imageViewer.close(); imageViewer.close();
toast({ toast({
@@ -98,8 +109,15 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => {
<SubMenuButtonContent label="New from Image" /> <SubMenuButtonContent label="New from Image" />
</MenuButton> </MenuButton>
<MenuList {...subMenu.menuListProps}> <MenuList {...subMenu.menuListProps}>
<MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasFromImage} isDisabled={isBusy}> <MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasWithRasterLayerFromImage} isDisabled={isBusy}>
{t('controlLayers.canvas')} {t('controlLayers.canvasAsRasterLayer')}
</MenuItem>
<MenuItem
icon={<PiFileBold />}
onClickCapture={onClickNewCanvasWithControlLayerFromImage}
isDisabled={isBusy}
>
{t('controlLayers.canvasAsControlLayer')}
</MenuItem> </MenuItem>
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={isBusy}> <MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={isBusy}>
{t('controlLayers.inpaintMask')} {t('controlLayers.inpaintMask')}

View File

@@ -57,7 +57,7 @@ export const ImageViewer = memo(({ closeButton }: Props) => {
{hasImageToCompare && <ImageComparison containerDims={containerDims} />} {hasImageToCompare && <ImageComparison containerDims={containerDims} />}
</Box> </Box>
<ImageComparisonDroppable /> <ImageComparisonDroppable />
<Box position="absolute" top={14} insetInlineEnd={2}> <Box position="absolute" top={14} insetInlineStart={2}>
<CanvasAlertsSendingToCanvas /> <CanvasAlertsSendingToCanvas />
</Box> </Box>
</Flex> </Flex>

View File

@@ -1 +1 @@
__version__ = "5.3.0rc2" __version__ = "5.3.0"