mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
refactor(ui): layer interaction locking
Previously we maintained an `isInteractable` flag, which was derived from these layer flags: - Locked/unlocked - Enabled/disabled - Layer's type visible/hidden When a layer was not interactable, we blocked all layer actions. After comparing to the behaviour in Affinity and considering user feedback, I've loosened these restrictions while maintaining safety. First, some definitions. There two kinds of layer actions - mutating actions and non-mutating actions. - Mutating actions are drawing on the layer, cropping it, filtering it, converting it, etc. Anything that changes the layer. - Non-mutating actions are copying the layer, saving the layer to gallery, etc. Anything that _uses_ the layer. Then, there are two broad canvas states - busy and not busy. "Busy" means the canvas is actively filtering, staging, compositing layers together, etc - something that is "single-threaded" by nature. And here are the revised restrictions: - When canvas is busy, you cannot initiate any layer actions. - When the canvas is not busy, and the layer is locked, you initiate any mutating actions. - When the canvas is not busy and the layer is not locked, you can initiate any layer action. Besides safely giving users more freedom, it also fixes an issue where the context menu for a layer was disabled if it was not the selected layer.
This commit is contained in:
@@ -2,7 +2,8 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import {
|
||||
controlLayerConvertedToInpaintMask,
|
||||
controlLayerConvertedToRasterLayer,
|
||||
@@ -17,7 +18,8 @@ export const ControlLayerMenuItemsConvertToSubMenu = memo(() => {
|
||||
const subMenu = useSubMenu();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext('control_layer');
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
|
||||
const convertToInpaintMask = useCallback(() => {
|
||||
dispatch(controlLayerConvertedToInpaintMask({ entityIdentifier, replace: true }));
|
||||
@@ -32,19 +34,19 @@ export const ControlLayerMenuItemsConvertToSubMenu = memo(() => {
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />}>
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label={t('controlLayers.convertControlLayerTo')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={convertToRasterLayer} icon={<PiSwapBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={convertToRasterLayer} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import {
|
||||
controlLayerConvertedToInpaintMask,
|
||||
controlLayerConvertedToRasterLayer,
|
||||
@@ -18,7 +18,7 @@ export const ControlLayerMenuItemsCopyToSubMenu = memo(() => {
|
||||
const subMenu = useSubMenu();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext('control_layer');
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const copyToInpaintMask = useCallback(() => {
|
||||
dispatch(controlLayerConvertedToInpaintMask({ entityIdentifier }));
|
||||
@@ -33,20 +33,20 @@ export const ControlLayerMenuItemsCopyToSubMenu = memo(() => {
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />}>
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label={t('controlLayers.copyControlLayerTo')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<CanvasEntityMenuItemsCopyToClipboard />
|
||||
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
{t('controlLayers.newInpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
{t('controlLayers.newRegionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={copyToRasterLayer} icon={<PiCopyBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={copyToRasterLayer} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
{t('controlLayers.newRasterLayer')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@@ -13,7 +13,7 @@ export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext('control_layer');
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
const selectWithTransparencyEffect = useMemo(
|
||||
() =>
|
||||
createSelector(selectCanvasSlice, (canvas) => {
|
||||
@@ -28,7 +28,7 @@ export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onToggle} icon={<PiDropHalfBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={onToggle} icon={<PiDropHalfBold />} isDisabled={isLocked}>
|
||||
{withTransparencyEffect
|
||||
? t('controlLayers.disableTransparencyEffect')
|
||||
: t('controlLayers.enableTransparencyEffect')}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import { inpaintMaskConvertedToRegionalGuidance } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,20 +14,21 @@ export const InpaintMaskMenuItemsConvertToSubMenu = memo(() => {
|
||||
const subMenu = useSubMenu();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
|
||||
const convertToRegionalGuidance = useCallback(() => {
|
||||
dispatch(inpaintMaskConvertedToRegionalGuidance({ entityIdentifier, replace: true }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />}>
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label={t('controlLayers.convertInpaintMaskTo')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { inpaintMaskConvertedToRegionalGuidance } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -14,21 +14,21 @@ export const InpaintMaskMenuItemsCopyToSubMenu = memo(() => {
|
||||
const subMenu = useSubMenu();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const copyToRegionalGuidance = useCallback(() => {
|
||||
dispatch(inpaintMaskConvertedToRegionalGuidance({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />}>
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label={t('controlLayers.copyInpaintMaskTo')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<CanvasEntityMenuItemsCopyToClipboard />
|
||||
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
{t('controlLayers.newRegionalGuidance')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { selectDefaultControlAdapter } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import {
|
||||
rasterLayerConvertedToControlLayer,
|
||||
rasterLayerConvertedToInpaintMask,
|
||||
@@ -20,7 +21,8 @@ export const RasterLayerMenuItemsConvertToSubMenu = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext('raster_layer');
|
||||
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
|
||||
const convertToInpaintMask = useCallback(() => {
|
||||
dispatch(rasterLayerConvertedToInpaintMask({ entityIdentifier, replace: true }));
|
||||
@@ -41,19 +43,19 @@ export const RasterLayerMenuItemsConvertToSubMenu = memo(() => {
|
||||
}, [defaultControlAdapter, dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />}>
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label={t('controlLayers.convertRasterLayerTo')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={convertToControlLayer} icon={<PiSwapBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={convertToControlLayer} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { selectDefaultControlAdapter } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import {
|
||||
rasterLayerConvertedToControlLayer,
|
||||
rasterLayerConvertedToInpaintMask,
|
||||
@@ -21,7 +21,7 @@ export const RasterLayerMenuItemsCopyToSubMenu = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext('raster_layer');
|
||||
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const copyToInpaintMask = useCallback(() => {
|
||||
dispatch(rasterLayerConvertedToInpaintMask({ entityIdentifier }));
|
||||
@@ -41,20 +41,20 @@ export const RasterLayerMenuItemsCopyToSubMenu = memo(() => {
|
||||
}, [defaultControlAdapter, dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />}>
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label={t('controlLayers.copyRasterLayerTo')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<CanvasEntityMenuItemsCopyToClipboard />
|
||||
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
{t('controlLayers.newInpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
{t('controlLayers.newRegionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={copyToControlLayer} icon={<PiCopyBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={copyToControlLayer} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
{t('controlLayers.newControlLayer')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import { rgConvertedToInpaintMask } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,20 +14,21 @@ export const RegionalGuidanceMenuItemsConvertToSubMenu = memo(() => {
|
||||
const subMenu = useSubMenu();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
|
||||
const convertToInpaintMask = useCallback(() => {
|
||||
dispatch(rgConvertedToInpaintMask({ entityIdentifier, replace: true }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />}>
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label={t('controlLayers.convertRegionalGuidanceTo')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { rgConvertedToInpaintMask } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -14,21 +14,21 @@ export const RegionalGuidanceMenuItemsCopyToSubMenu = memo(() => {
|
||||
const subMenu = useSubMenu();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const copyToInpaintMask = useCallback(() => {
|
||||
dispatch(rgConvertedToInpaintMask({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />}>
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label={t('controlLayers.copyRegionalGuidanceTo')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<CanvasEntityMenuItemsCopyToClipboard />
|
||||
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={isBusy}>
|
||||
{t('controlLayers.newInpaintMask')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import {
|
||||
entityArrangedBackwardOne,
|
||||
entityArrangedForwardOne,
|
||||
@@ -56,7 +56,7 @@ export const CanvasEntityMenuItemsArrange = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const selectValidActions = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
@@ -92,28 +92,28 @@ export const CanvasEntityMenuItemsArrange = memo(() => {
|
||||
aria-label={t('controlLayers.moveToFront')}
|
||||
tooltip={t('controlLayers.moveToFront')}
|
||||
onClick={moveToFront}
|
||||
isDisabled={!validActions.canMoveToFront || !isInteractable}
|
||||
isDisabled={!validActions.canMoveToFront || isBusy}
|
||||
icon={<PiArrowLineUpBold />}
|
||||
/>
|
||||
<IconMenuItem
|
||||
aria-label={t('controlLayers.moveForward')}
|
||||
tooltip={t('controlLayers.moveForward')}
|
||||
onClick={moveForwardOne}
|
||||
isDisabled={!validActions.canMoveForwardOne || !isInteractable}
|
||||
isDisabled={!validActions.canMoveForwardOne || isBusy}
|
||||
icon={<PiArrowUpBold />}
|
||||
/>
|
||||
<IconMenuItem
|
||||
aria-label={t('controlLayers.moveBackward')}
|
||||
tooltip={t('controlLayers.moveBackward')}
|
||||
onClick={moveBackwardOne}
|
||||
isDisabled={!validActions.canMoveBackwardOne || !isInteractable}
|
||||
isDisabled={!validActions.canMoveBackwardOne || isBusy}
|
||||
icon={<PiArrowDownBold />}
|
||||
/>
|
||||
<IconMenuItem
|
||||
aria-label={t('controlLayers.moveToBack')}
|
||||
tooltip={t('controlLayers.moveToBack')}
|
||||
onClick={moveToBack}
|
||||
isDisabled={!validActions.canMoveToBack || !isInteractable}
|
||||
isDisabled={!validActions.canMoveToBack || isBusy}
|
||||
icon={<PiArrowLineDownBold />}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useCopyLayerToClipboard } from 'features/controlLayers/hooks/useCopyLayerToClipboard';
|
||||
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCopyBold } from 'react-icons/pi';
|
||||
@@ -12,7 +12,7 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isEmpty = useEntityIsEmpty(entityIdentifier);
|
||||
const copyLayerToClipboard = useCopyLayerToClipboard();
|
||||
|
||||
@@ -21,7 +21,7 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
|
||||
}, [copyLayerToClipboard, adapter]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={!isInteractable || isEmpty}>
|
||||
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={isBusy || isEmpty}>
|
||||
{t('common.clipboard')}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCropBold } from 'react-icons/pi';
|
||||
@@ -10,7 +11,8 @@ export const CanvasEntityMenuItemsCropToBbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
const onClick = useCallback(() => {
|
||||
if (!adapter) {
|
||||
return;
|
||||
@@ -19,7 +21,7 @@ export const CanvasEntityMenuItemsCropToBbox = memo(() => {
|
||||
}, [adapter]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiCropBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={onClick} icon={<PiCropBold />} isDisabled={isBusy || isLocked}>
|
||||
{t('controlLayers.cropLayerToBbox')}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -16,7 +16,7 @@ export const CanvasEntityMenuItemsDelete = memo(({ asIcon = false }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const deleteEntity = useCallback(() => {
|
||||
dispatch(entityDeleted({ entityIdentifier }));
|
||||
@@ -30,13 +30,13 @@ export const CanvasEntityMenuItemsDelete = memo(({ asIcon = false }: Props) => {
|
||||
onClick={deleteEntity}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
isDestructive
|
||||
isDisabled={!isInteractable}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} isDestructive isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} isDestructive isDisabled={isBusy}>
|
||||
{t('common.delete')}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { entityDuplicated } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -11,7 +11,7 @@ export const CanvasEntityMenuItemsDuplicate = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(entityDuplicated({ entityIdentifier }));
|
||||
@@ -23,7 +23,7 @@ export const CanvasEntityMenuItemsDuplicate = memo(() => {
|
||||
tooltip={t('controlLayers.duplicate')}
|
||||
onClick={onClick}
|
||||
icon={<PiCopyFill />}
|
||||
isDisabled={!isInteractable}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useSaveLayerToAssets } from 'features/controlLayers/hooks/useSaveLayerToAssets';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -11,14 +11,14 @@ export const CanvasEntityMenuItemsSave = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const saveLayerToAssets = useSaveLayerToAssets();
|
||||
const onClick = useCallback(() => {
|
||||
saveLayerToAssets(adapter);
|
||||
}, [saveLayerToAssets, adapter]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiFloppyDiskBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={onClick} icon={<PiFloppyDiskBold />} isDisabled={isBusy}>
|
||||
{t('controlLayers.saveLayerToAssets')}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $false } from 'app/store/nanostores/util';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import { entityReset } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { isMaskEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
@@ -14,30 +13,30 @@ import { useCallback, useMemo } from 'react';
|
||||
export function useCanvasResetLayerHotkey() {
|
||||
useAssertSingleton(useCanvasResetLayerHotkey.name);
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const entityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const adapter = useEntityAdapterSafe(selectedEntityIdentifier);
|
||||
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
const resetSelectedLayer = useCallback(() => {
|
||||
if (selectedEntityIdentifier === null || adapter === null) {
|
||||
if (entityIdentifier === null || adapter === null) {
|
||||
return;
|
||||
}
|
||||
adapter.bufferRenderer.clearBuffer();
|
||||
dispatch(entityReset({ entityIdentifier: selectedEntityIdentifier }));
|
||||
}, [adapter, dispatch, selectedEntityIdentifier]);
|
||||
dispatch(entityReset({ entityIdentifier }));
|
||||
}, [adapter, dispatch, entityIdentifier]);
|
||||
|
||||
const isResetEnabled = useMemo(
|
||||
() => selectedEntityIdentifier !== null && isMaskEntityIdentifier(selectedEntityIdentifier),
|
||||
[selectedEntityIdentifier]
|
||||
const isResetAllowed = useMemo(
|
||||
() => entityIdentifier !== null && isMaskEntityIdentifier(entityIdentifier),
|
||||
[entityIdentifier]
|
||||
);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'resetSelected',
|
||||
category: 'canvas',
|
||||
callback: resetSelectedLayer,
|
||||
options: { enabled: isResetEnabled && !isBusy && isInteractable && !imageViewer.isOpen },
|
||||
dependencies: [isResetEnabled, isBusy, isInteractable, resetSelectedLayer, imageViewer.isOpen],
|
||||
options: { enabled: isResetAllowed && !isBusy && !isLocked && !imageViewer.isOpen },
|
||||
dependencies: [isResetAllowed, isBusy, isLocked, resetSelectedLayer, imageViewer.isOpen],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $false } from 'app/store/nanostores/util';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
@@ -13,8 +13,8 @@ export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null)
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const imageViewer = useImageViewer();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
|
||||
const isEmpty = useStore(adapter?.$isEmpty ?? $false);
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
const isEmpty = useEntityIsEmpty(entityIdentifier);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
if (!entityIdentifier) {
|
||||
@@ -29,14 +29,14 @@ export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null)
|
||||
if (isBusy) {
|
||||
return true;
|
||||
}
|
||||
if (!isInteractable) {
|
||||
if (isLocked) {
|
||||
return true;
|
||||
}
|
||||
if (isEmpty) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [entityIdentifier, adapter, isBusy, isInteractable, isEmpty]);
|
||||
}, [entityIdentifier, adapter, isBusy, isLocked, isEmpty]);
|
||||
|
||||
const start = useCallback(() => {
|
||||
if (isDisabled) {
|
||||
|
||||
@@ -3,8 +3,11 @@ import { buildSelectHasObjects } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useEntityIsEmpty = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
const selectHasObjects = useMemo(() => buildSelectHasObjects(entityIdentifier), [entityIdentifier]);
|
||||
export const useEntityIsEmpty = (entityIdentifier: CanvasEntityIdentifier | null) => {
|
||||
const selectHasObjects = useMemo(
|
||||
() => (entityIdentifier ? buildSelectHasObjects(entityIdentifier) : () => false),
|
||||
[entityIdentifier]
|
||||
);
|
||||
const hasObjects = useAppSelector(selectHasObjects);
|
||||
|
||||
return !hasObjects;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $true } from 'app/store/nanostores/util';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
|
||||
export const useIsEntityInteractable = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const isInteractable = useStore(adapter?.$isInteractable ?? $true);
|
||||
|
||||
return !isBusy && isInteractable;
|
||||
};
|
||||
@@ -4,10 +4,13 @@ import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/se
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useEntityIsLocked = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
export const useEntityIsLocked = (entityIdentifier: CanvasEntityIdentifier | null) => {
|
||||
const selectIsLocked = useMemo(
|
||||
() =>
|
||||
createSelector(selectCanvasSlice, (canvas) => {
|
||||
if (!entityIdentifier) {
|
||||
return false;
|
||||
}
|
||||
const entity = selectEntity(canvas, entityIdentifier);
|
||||
if (!entity) {
|
||||
return false;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $false } from 'app/store/nanostores/util';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { isSegmentableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
@@ -13,8 +13,8 @@ export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifie
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const imageViewer = useImageViewer();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
|
||||
const isEmpty = useStore(adapter?.$isEmpty ?? $false);
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
const isEmpty = useEntityIsEmpty(entityIdentifier);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
if (!entityIdentifier) {
|
||||
@@ -29,14 +29,14 @@ export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifie
|
||||
if (isBusy) {
|
||||
return true;
|
||||
}
|
||||
if (!isInteractable) {
|
||||
if (isLocked) {
|
||||
return true;
|
||||
}
|
||||
if (isEmpty) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [entityIdentifier, adapter, isBusy, isInteractable, isEmpty]);
|
||||
}, [entityIdentifier, adapter, isBusy, isLocked, isEmpty]);
|
||||
|
||||
const start = useCallback(() => {
|
||||
if (isDisabled) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $false } from 'app/store/nanostores/util';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
@@ -13,8 +13,8 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const imageViewer = useImageViewer();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
|
||||
const isEmpty = useStore(adapter?.$isEmpty ?? $false);
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
const isEmpty = useEntityIsEmpty(entityIdentifier);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
if (!entityIdentifier) {
|
||||
@@ -29,14 +29,14 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
|
||||
if (isBusy) {
|
||||
return true;
|
||||
}
|
||||
if (!isInteractable) {
|
||||
if (isLocked) {
|
||||
return true;
|
||||
}
|
||||
if (isEmpty) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [entityIdentifier, adapter, isBusy, isInteractable, isEmpty]);
|
||||
}, [entityIdentifier, adapter, isBusy, isLocked, isEmpty]);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
if (isDisabled) {
|
||||
|
||||
@@ -12,17 +12,26 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import type { CanvasSegmentAnythingModule } from 'features/controlLayers/konva/CanvasSegmentAnythingModule';
|
||||
import { getKonvaNodeDebugAttrs, getRectIntersection } from 'features/controlLayers/konva/util';
|
||||
import { selectIsolatedLayerPreview } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import {
|
||||
buildSelectIsHidden,
|
||||
selectIsolatedLayerPreview,
|
||||
selectIsolatedStagingPreview,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import {
|
||||
buildSelectIsSelected,
|
||||
getSelectIsTypeHidden,
|
||||
selectBboxRect,
|
||||
selectCanvasSlice,
|
||||
selectEntity,
|
||||
} from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasEntityIdentifier, CanvasRenderableEntityState, Rect } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
type CanvasEntityIdentifier,
|
||||
type CanvasRenderableEntityState,
|
||||
isRasterLayerEntityIdentifier,
|
||||
type Rect,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import { atom } from 'nanostores';
|
||||
import rafThrottle from 'raf-throttle';
|
||||
import type { Logger } from 'roarr';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@@ -175,7 +184,14 @@ export abstract class CanvasEntityAdapterBase<
|
||||
}
|
||||
};
|
||||
|
||||
selectIsHidden: Selector<RootState, boolean>;
|
||||
/**
|
||||
* A selector that selects whether the entity type is hidden.
|
||||
*/
|
||||
selectIsTypeHidden: Selector<RootState, boolean>;
|
||||
|
||||
/**
|
||||
* A selector that selects whether the entity is selected.
|
||||
*/
|
||||
selectIsSelected: Selector<RootState, boolean>;
|
||||
|
||||
/**
|
||||
@@ -209,17 +225,11 @@ export abstract class CanvasEntityAdapterBase<
|
||||
/**
|
||||
* Whether this entity is hidden. This is synced with the entity's group type visibility.
|
||||
*/
|
||||
$isHidden = atom(false);
|
||||
$isEntityTypeHidden = atom(false);
|
||||
/**
|
||||
* Whether this entity is empty. This is computed based on the entity's objects.
|
||||
*/
|
||||
$isEmpty = atom(true);
|
||||
/**
|
||||
* Whether this entity is interactable. This is computed based on the entity's locked, disabled, and hidden states.
|
||||
*/
|
||||
$isInteractable = computed([this.$isLocked, this.$isDisabled, this.$isHidden], (isLocked, isDisabled, isHidden) => {
|
||||
return !isLocked && !isDisabled && !isHidden;
|
||||
});
|
||||
/**
|
||||
* A cache of the entity's canvas element. This is generated from a clone of the entity's Konva layer.
|
||||
*/
|
||||
@@ -260,19 +270,18 @@ export abstract class CanvasEntityAdapterBase<
|
||||
assert(state !== undefined, 'Missing entity state on creation');
|
||||
this.state = state;
|
||||
|
||||
this.selectIsHidden = buildSelectIsHidden(this.entityIdentifier);
|
||||
this.selectIsTypeHidden = getSelectIsTypeHidden(this.entityIdentifier.type);
|
||||
this.selectIsSelected = buildSelectIsSelected(this.entityIdentifier);
|
||||
|
||||
/**
|
||||
* There are a number of reason we may need to show or hide a layer:
|
||||
* - The entity is enabled/disabled
|
||||
* - The entity type is hidden/shown
|
||||
* - Staging status changes and `isolatedStagingPreview` is enabled
|
||||
* - Global filtering status changes and `isolatedFilteringPreview` is enabled
|
||||
* - Global transforming status changes and `isolatedTransformingPreview` is enabled
|
||||
* - The entity is selected or deselected (only selected and onscreen entities are rendered)
|
||||
* - `isolatedStagingPreview` is enabled and we start or stop staging
|
||||
* - `isolatedLayerPreview` is enabled and we start or stop filtering, transforming, select-object-ing
|
||||
* - The entity is selected or deselected (only selected and onscreen entities are rendered as a perf optimization)
|
||||
*/
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectIsHidden, this.syncVisibility));
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectIsTypeHidden, this.syncVisibility));
|
||||
this.subscriptions.add(
|
||||
this.manager.stateApi.createStoreSubscription(selectIsolatedLayerPreview, this.syncVisibility)
|
||||
);
|
||||
@@ -285,7 +294,9 @@ export abstract class CanvasEntityAdapterBase<
|
||||
* The tool preview may need to be updated when the entity is locked or disabled. For example, when we disable the
|
||||
* entity, we should hide the tool preview & change the cursor.
|
||||
*/
|
||||
this.subscriptions.add(this.$isInteractable.subscribe(this.manager.tool.render));
|
||||
this.subscriptions.add(this.$isDisabled.subscribe(this.manager.tool.render));
|
||||
this.subscriptions.add(this.$isLocked.subscribe(this.manager.tool.render));
|
||||
this.subscriptions.add(this.$isEntityTypeHidden.subscribe(this.manager.tool.render));
|
||||
|
||||
/**
|
||||
* When the stage is transformed in any way (panning, zooming, resizing) or the entity is moved, we need to update
|
||||
@@ -404,10 +415,9 @@ export abstract class CanvasEntityAdapterBase<
|
||||
*/
|
||||
syncIsEnabled = () => {
|
||||
this.log.trace('Updating visibility');
|
||||
this.konva.layer.visible(this.state.isEnabled);
|
||||
this.renderer.syncKonvaCache(this.state.isEnabled);
|
||||
this.transformer.syncInteractionState();
|
||||
this.$isDisabled.set(!this.state.isEnabled);
|
||||
this.syncVisibility();
|
||||
this.transformer.syncInteractionState();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -419,6 +429,7 @@ export abstract class CanvasEntityAdapterBase<
|
||||
if (didRender) {
|
||||
// If the objects have changed, we need to recalculate the transformer's bounding box.
|
||||
this.transformer.requestRectCalculation();
|
||||
this.transformer.syncInteractionState();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -437,45 +448,70 @@ export abstract class CanvasEntityAdapterBase<
|
||||
};
|
||||
|
||||
syncVisibility = rafThrottle(() => {
|
||||
// Handle the base hidden state
|
||||
if (this.manager.stateApi.runSelector(this.selectIsHidden)) {
|
||||
/**
|
||||
* If the entity type is hidden, so should the entity be hidden.
|
||||
*/
|
||||
if (this.manager.stateApi.runSelector(this.selectIsTypeHidden)) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isolatedLayerPreview = this.manager.stateApi.runSelector(selectIsolatedLayerPreview);
|
||||
|
||||
// Handle isolated preview modes - if another entity is filtering or transforming, we may need to hide this entity.
|
||||
if (isolatedLayerPreview) {
|
||||
const filteringEntityIdentifier = this.manager.stateApi.$filteringAdapter.get()?.entityIdentifier;
|
||||
if (filteringEntityIdentifier && filteringEntityIdentifier.id !== this.id) {
|
||||
if (this.manager.stateApi.runSelector(selectIsolatedStagingPreview)) {
|
||||
/**
|
||||
* When staging w/ isolatedStagingPreview enabled, we only show raster layers.
|
||||
*
|
||||
* This allows the user to easily see how the new generation fits in with the rest of the canvas without the
|
||||
* other layer types getting in the way.
|
||||
*/
|
||||
const isStaging = this.manager.stateApi.runSelector(selectIsStaging);
|
||||
const isRasterLayer = isRasterLayerEntityIdentifier(this.entityIdentifier);
|
||||
if (isStaging && !isRasterLayer) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isolatedLayerPreview) {
|
||||
const transformingEntity = this.manager.stateApi.$transformingAdapter.get();
|
||||
if (this.manager.stateApi.runSelector(selectIsolatedLayerPreview)) {
|
||||
/**
|
||||
* Handle isolated preview modes - if another entity is filtering, transforming, or select-object-ing, we may need
|
||||
* to hide this entity.
|
||||
*/
|
||||
const filteringAdapter = this.manager.stateApi.$filteringAdapter.get();
|
||||
if (filteringAdapter && filteringAdapter !== this) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const transformingAdapter = this.manager.stateApi.$transformingAdapter.get();
|
||||
if (
|
||||
transformingEntity &&
|
||||
transformingEntity.entityIdentifier.id !== this.id &&
|
||||
transformingAdapter &&
|
||||
transformingAdapter !== this &&
|
||||
// Silent transforms should be transparent to the user, so we don't need to hide the entity.
|
||||
!transformingEntity.transformer.$silentTransform.get()
|
||||
!transformingAdapter.transformer.$silentTransform.get()
|
||||
) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isolatedLayerPreview) {
|
||||
const segmentingEntity = this.manager.stateApi.$segmentingAdapter.get();
|
||||
if (segmentingEntity && segmentingEntity.entityIdentifier.id !== this.id) {
|
||||
const segmentingAdapter = this.manager.stateApi.$segmentingAdapter.get();
|
||||
if (segmentingAdapter && segmentingAdapter !== this) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the entity is not selected and offscreen, we can hide it
|
||||
/**
|
||||
* Disabled entities should be hidden.
|
||||
*/
|
||||
if (this.$isDisabled.get()) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the entity is offscreen and not selected, we should hide it. If it is selected and offscreen, it still needs
|
||||
* to be visible so the user can interact with it.
|
||||
*/
|
||||
if (!this.$isOnScreen.get() && !this.manager.stateApi.getIsSelected(this.entityIdentifier.id)) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
@@ -485,17 +521,14 @@ export abstract class CanvasEntityAdapterBase<
|
||||
});
|
||||
|
||||
setVisibility = (isVisible: boolean) => {
|
||||
const isHidden = this.$isHidden.get();
|
||||
const isLayerVisible = this.konva.layer.visible();
|
||||
|
||||
if (isHidden === !isVisible && isLayerVisible === isVisible) {
|
||||
if (isLayerVisible === isVisible) {
|
||||
// No change
|
||||
return;
|
||||
}
|
||||
this.log.trace(isVisible ? 'Showing' : 'Hiding');
|
||||
this.$isHidden.set(!isVisible);
|
||||
this.konva.layer.visible(isVisible);
|
||||
|
||||
this.renderer.syncKonvaCache();
|
||||
};
|
||||
|
||||
@@ -505,8 +538,8 @@ export abstract class CanvasEntityAdapterBase<
|
||||
syncIsLocked = () => {
|
||||
// The only thing we need to do is update the transformer's interaction state. For tool interactions, like drawing
|
||||
// shapes, we defer to the CanvasToolModule to handle the locked state.
|
||||
this.transformer.syncInteractionState();
|
||||
this.$isLocked.set(this.state.isLocked);
|
||||
this.transformer.syncInteractionState();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -566,9 +599,8 @@ export abstract class CanvasEntityAdapterBase<
|
||||
hasCache: this.$canvasCache.get() !== null,
|
||||
isLocked: this.$isLocked.get(),
|
||||
isDisabled: this.$isDisabled.get(),
|
||||
isHidden: this.$isHidden.get(),
|
||||
isEntityTypeHidden: this.$isEntityTypeHidden.get(),
|
||||
isEmpty: this.$isEmpty.get(),
|
||||
isInteractable: this.$isInteractable.get(),
|
||||
isOnScreen: this.$isOnScreen.get(),
|
||||
intersectsBbox: this.$intersectsBbox.get(),
|
||||
konva: getKonvaNodeDebugAttrs(this.konva.layer),
|
||||
|
||||
@@ -219,6 +219,10 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.parent.konva.layer.visible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.trace('Updating compositing rect fill');
|
||||
|
||||
assert(this.konva.compositing, 'Missing compositing rect');
|
||||
@@ -244,6 +248,10 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.parent.konva.layer.visible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.trace('Updating compositing rect size');
|
||||
|
||||
assert(this.konva.compositing, 'Missing compositing rect');
|
||||
@@ -262,6 +270,10 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.parent.konva.layer.visible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.trace('Updating compositing rect position');
|
||||
|
||||
assert(this.konva.compositing, 'Missing compositing rect');
|
||||
@@ -272,6 +284,10 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
updateOpacity = throttle(() => {
|
||||
if (!this.parent.konva.layer.visible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.trace('Updating opacity');
|
||||
|
||||
const opacity = this.parent.state.opacity;
|
||||
|
||||
@@ -640,6 +640,13 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.parent.$isLocked.get()) {
|
||||
// The layer is locked, it should not be interactable
|
||||
this.parent.konva.layer.listening(false);
|
||||
this._setInteractionMode('off');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.$isTransforming.get() && tool === 'move') {
|
||||
// We are moving this layer, it must be listening
|
||||
this.parent.konva.layer.listening(true);
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
selectGridSize,
|
||||
} from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasEntityType,
|
||||
CanvasState,
|
||||
EntityBrushLineAddedPayload,
|
||||
EntityEraserLineAddedPayload,
|
||||
@@ -546,24 +545,6 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
return this.getCanvasState().selectedEntityIdentifier?.id === id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if an entity type is hidden. Individual entities are not hidden; the entire entity type is hidden.
|
||||
*/
|
||||
getIsTypeHidden = (type: CanvasEntityType): boolean => {
|
||||
switch (type) {
|
||||
case 'raster_layer':
|
||||
return this.getRasterLayersState().isHidden;
|
||||
case 'control_layer':
|
||||
return this.getControlLayersState().isHidden;
|
||||
case 'inpaint_mask':
|
||||
return this.getInpaintMasksState().isHidden;
|
||||
case 'regional_guidance':
|
||||
return this.getRegionsState().isHidden;
|
||||
default:
|
||||
assert(false, 'Unhandled entity type');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the number of entities that are currently rendered on the canvas.
|
||||
*/
|
||||
|
||||
@@ -161,6 +161,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
const tool = this.$tool.get();
|
||||
const segmentingAdapter = this.manager.stateApi.$segmentingAdapter.get();
|
||||
const transformingAdapter = this.manager.stateApi.$transformingAdapter.get();
|
||||
const selectedEntityAdapter = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (this.manager.stage.getIsDragging()) {
|
||||
this.tools.view.syncCursorStyle();
|
||||
@@ -178,7 +179,11 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
this.tools.bbox.syncCursorStyle();
|
||||
} else if (this.manager.stateApi.getRenderedEntityCount() === 0) {
|
||||
stage.setCursor('not-allowed');
|
||||
} else if (!this.manager.stateApi.getSelectedEntityAdapter()?.$isInteractable.get()) {
|
||||
} else if (selectedEntityAdapter?.$isDisabled.get()) {
|
||||
stage.setCursor('not-allowed');
|
||||
} else if (selectedEntityAdapter?.$isEntityTypeHidden.get()) {
|
||||
stage.setCursor('not-allowed');
|
||||
} else if (selectedEntityAdapter?.$isLocked.get()) {
|
||||
stage.setCursor('not-allowed');
|
||||
} else if (tool === 'brush') {
|
||||
this.tools.brush.syncCursorStyle();
|
||||
@@ -301,7 +306,15 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!selectedEntity.$isInteractable.get()) {
|
||||
if (selectedEntity.$isDisabled.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedEntity.$isEntityTypeHidden.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedEntity.$isLocked.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { selectIsolatedStagingPreview } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasEntityState,
|
||||
CanvasEntityType,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasMetadata,
|
||||
CanvasRasterLayerState,
|
||||
CanvasRegionalGuidanceState,
|
||||
CanvasRenderableEntityIdentifier,
|
||||
CanvasRenderableEntityState,
|
||||
CanvasRenderableEntityType,
|
||||
CanvasState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { isRasterLayerEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { getGridSize, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
@@ -325,15 +323,18 @@ export const selectSelectedEntityFill = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
const selectRasterLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.rasterLayers.isHidden);
|
||||
const selectControlLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.controlLayers.isHidden);
|
||||
const selectInpaintMasksIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.inpaintMasks.isHidden);
|
||||
const selectRegionalGuidanceIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.regionalGuidance.isHidden);
|
||||
export const selectRasterLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.rasterLayers.isHidden);
|
||||
export const selectControlLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.controlLayers.isHidden);
|
||||
export const selectInpaintMasksIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.inpaintMasks.isHidden);
|
||||
export const selectRegionalGuidanceIsHidden = createSelector(
|
||||
selectCanvasSlice,
|
||||
(canvas) => canvas.regionalGuidance.isHidden
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the hidden selector for the given entity type.
|
||||
*/
|
||||
const getSelectIsTypeHidden = (type: CanvasEntityType) => {
|
||||
export const getSelectIsTypeHidden = (type: CanvasRenderableEntityType) => {
|
||||
switch (type) {
|
||||
case 'raster_layer':
|
||||
return selectRasterLayersIsHidden;
|
||||
@@ -344,44 +345,10 @@ const getSelectIsTypeHidden = (type: CanvasEntityType) => {
|
||||
case 'regional_guidance':
|
||||
return selectRegionalGuidanceIsHidden;
|
||||
default:
|
||||
assert(false, 'Unhandled entity type');
|
||||
assert<Equals<typeof type, never>>(false, 'Unhandled entity type');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a selector taht selects if the entity is hidden.
|
||||
*/
|
||||
export const buildSelectIsHidden = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
const selectIsTypeHidden = getSelectIsTypeHidden(entityIdentifier.type);
|
||||
return createSelector(
|
||||
[selectCanvasSlice, selectIsTypeHidden, selectIsStaging, selectIsolatedStagingPreview],
|
||||
(canvas, isTypeHidden, isStaging, isolatedStagingPreview) => {
|
||||
const entity = selectEntity(canvas, entityIdentifier);
|
||||
|
||||
// An entity is hidden if:
|
||||
// - The entity type is hidden
|
||||
// - The entity is disabled
|
||||
// - The entity is not a raster layer and we are staging and the option to show only raster layers is enabled
|
||||
if (!entity) {
|
||||
return true;
|
||||
}
|
||||
if (isTypeHidden) {
|
||||
return true;
|
||||
}
|
||||
if (!entity.isEnabled) {
|
||||
return true;
|
||||
}
|
||||
if (isStaging && isolatedStagingPreview) {
|
||||
// When staging, we only show raster layers. This allows the user to easily see how the new generation fits in
|
||||
// with the rest of the canvas without the masks and control layers getting in the way.
|
||||
return !isRasterLayerEntityIdentifier(entityIdentifier);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a selector taht selects if the entity is selected.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user