feat(ui): add entity group hiding

This commit is contained in:
psychedelicious 2024-08-22 09:26:46 +10:00
parent 27e829b955
commit 3136d89d52
19 changed files with 263 additions and 109 deletions

View File

@ -1666,25 +1666,37 @@
"addPositivePrompt": "Add $t(common.positivePrompt)", "addPositivePrompt": "Add $t(common.positivePrompt)",
"addNegativePrompt": "Add $t(common.negativePrompt)", "addNegativePrompt": "Add $t(common.negativePrompt)",
"addIPAdapter": "Add $t(common.ipAdapter)", "addIPAdapter": "Add $t(common.ipAdapter)",
"regionalGuidance": "Regional Guidance",
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
"raster": "Raster", "raster": "Raster",
"rasterLayer": "$t(controlLayers.raster) $t(unifiedCanvas.layer)", "rasterLayer_one": "Raster Layer",
"controlLayer_one": "Control Layer",
"inpaintMask_one": "Inpaint Mask",
"regionalGuidance_one": "Regional Guidance",
"ipAdapter_one": "IP Adapter",
"rasterLayer_other": "Raster Layers",
"controlLayer_other": "Control Layers",
"inpaintMask_other": "Inpaint Masks",
"regionalGuidance_other": "Regional Guidance",
"ipAdapter_other": "IP Adapters",
"opacity": "Opacity", "opacity": "Opacity",
"regionalGuidance_withCount": "Regional Guidance ({{count}})", "regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
"controlAdapters_withCount": "Control Adapters ({{count}})", "controlAdapters_withCount_hidden": "Control Adapters ({{count}} hidden)",
"controlLayer": "Control Layer", "controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
"controlLayers_withCount": "Control Layers ({{count}})", "rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
"rasterLayers_withCount": "Raster Layers ({{count}})", "ipAdapters_withCount_hidden": "IP Adapters ({{count}} hidden)",
"ipAdapters_withCount": "IP Adapters ({{count}})", "inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
"inpaintMasks_withCount": "Inpaint Masks ({{count}})", "regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
"controlAdapters_withCount_visible": "Control Adapters ({{count}})",
"controlLayers_withCount_visible": "Control Layers ({{count}})",
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
"ipAdapters_withCount_visible": "IP Adapters ({{count}})",
"inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})",
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
"globalIPAdapter": "Global $t(common.ipAdapter)", "globalIPAdapter": "Global $t(common.ipAdapter)",
"globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)", "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)",
"globalInitialImage": "Global Initial Image", "globalInitialImage": "Global Initial Image",
"globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)",
"inpaintMask": "Inpaint Mask",
"layer": "Layer", "layer": "Layer",
"opacityFilter": "Opacity Filter", "opacityFilter": "Opacity Filter",
"clearProcessor": "Clear Processor", "clearProcessor": "Clear Processor",
@ -1699,6 +1711,8 @@
"convertToRasterLayer": "Convert to Raster Layer", "convertToRasterLayer": "Convert to Raster Layer",
"enableTransparencyEffect": "Enable Transparency Effect", "enableTransparencyEffect": "Enable Transparency Effect",
"disableTransparencyEffect": "Disable Transparency Effect", "disableTransparencyEffect": "Disable Transparency Effect",
"hidingType": "Hiding {{type}}",
"showingType": "Showing {{type}}",
"fill": { "fill": {
"fillStyle": "Fill Style", "fillStyle": "Fill Style",
"solid": "Solid", "solid": "Solid",

View File

@ -5,14 +5,12 @@ import { ControlLayer } from 'features/controlLayers/components/ControlLayer/Con
import { mapId } from 'features/controlLayers/konva/util'; import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
return canvasV2.controlLayers.entities.map(mapId).reverse(); return canvasV2.controlLayers.entities.map(mapId).reverse();
}); });
export const ControlLayerEntityList = memo(() => { export const ControlLayerEntityList = memo(() => {
const { t } = useTranslation();
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_layer')); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_layer'));
const layerIds = useAppSelector(selectEntityIds); const layerIds = useAppSelector(selectEntityIds);
@ -22,11 +20,7 @@ export const ControlLayerEntityList = memo(() => {
if (layerIds.length > 0) { if (layerIds.length > 0) {
return ( return (
<CanvasEntityGroupList <CanvasEntityGroupList type="control_layer" isSelected={isSelected}>
type="control_layer"
title={t('controlLayers.controlLayers_withCount', { count: layerIds.length })}
isSelected={isSelected}
>
{layerIds.map((id) => ( {layerIds.map((id) => (
<ControlLayer key={id} id={id} /> <ControlLayer key={id} id={id} />
))} ))}

View File

@ -6,14 +6,12 @@ import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter
import { mapId } from 'features/controlLayers/konva/util'; import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
return canvasV2.ipAdapters.entities.map(mapId).reverse(); return canvasV2.ipAdapters.entities.map(mapId).reverse();
}); });
export const IPAdapterList = memo(() => { export const IPAdapterList = memo(() => {
const { t } = useTranslation();
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'ip_adapter')); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'ip_adapter'));
const ipaIds = useAppSelector(selectEntityIds); const ipaIds = useAppSelector(selectEntityIds);
@ -23,11 +21,7 @@ export const IPAdapterList = memo(() => {
if (ipaIds.length > 0) { if (ipaIds.length > 0) {
return ( return (
<CanvasEntityGroupList <CanvasEntityGroupList type="ip_adapter" isSelected={isSelected}>
type="ip_adapter"
title={t('controlLayers.ipAdapters_withCount', { count: ipaIds.length })}
isSelected={isSelected}
>
{ipaIds.map((id) => ( {ipaIds.map((id) => (
<IPAdapter key={id} id={id} /> <IPAdapter key={id} id={id} />
))} ))}

View File

@ -5,14 +5,12 @@ import { InpaintMask } from 'features/controlLayers/components/InpaintMask/Inpai
import { mapId } from 'features/controlLayers/konva/util'; import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
return canvasV2.inpaintMasks.entities.map(mapId).reverse(); return canvasV2.inpaintMasks.entities.map(mapId).reverse();
}); });
export const InpaintMaskList = memo(() => { export const InpaintMaskList = memo(() => {
const { t } = useTranslation();
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask')); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask'));
const entityIds = useAppSelector(selectEntityIds); const entityIds = useAppSelector(selectEntityIds);
@ -22,11 +20,7 @@ export const InpaintMaskList = memo(() => {
if (entityIds.length > 0) { if (entityIds.length > 0) {
return ( return (
<CanvasEntityGroupList <CanvasEntityGroupList type="inpaint_mask" isSelected={isSelected}>
type="inpaint_mask"
title={t('controlLayers.inpaintMasks_withCount', { count: entityIds.length })}
isSelected={isSelected}
>
{entityIds.map((id) => ( {entityIds.map((id) => (
<InpaintMask key={id} id={id} /> <InpaintMask key={id} id={id} />
))} ))}

View File

@ -5,14 +5,12 @@ import { RasterLayer } from 'features/controlLayers/components/RasterLayer/Raste
import { mapId } from 'features/controlLayers/konva/util'; import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
return canvasV2.rasterLayers.entities.map(mapId).reverse(); return canvasV2.rasterLayers.entities.map(mapId).reverse();
}); });
export const RasterLayerEntityList = memo(() => { export const RasterLayerEntityList = memo(() => {
const { t } = useTranslation();
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'raster_layer')); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'raster_layer'));
const layerIds = useAppSelector(selectEntityIds); const layerIds = useAppSelector(selectEntityIds);
@ -22,11 +20,7 @@ export const RasterLayerEntityList = memo(() => {
if (layerIds.length > 0) { if (layerIds.length > 0) {
return ( return (
<CanvasEntityGroupList <CanvasEntityGroupList type="raster_layer" isSelected={isSelected}>
type="raster_layer"
title={t('controlLayers.rasterLayers_withCount', { count: layerIds.length })}
isSelected={isSelected}
>
{layerIds.map((id) => ( {layerIds.map((id) => (
<RasterLayer key={id} id={id} /> <RasterLayer key={id} id={id} />
))} ))}

View File

@ -5,14 +5,12 @@ import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuid
import { mapId } from 'features/controlLayers/konva/util'; import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
return canvasV2.regions.entities.map(mapId).reverse(); return canvasV2.regions.entities.map(mapId).reverse();
}); });
export const RegionalGuidanceEntityList = memo(() => { export const RegionalGuidanceEntityList = memo(() => {
const { t } = useTranslation();
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'regional_guidance')); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'regional_guidance'));
const rgIds = useAppSelector(selectEntityIds); const rgIds = useAppSelector(selectEntityIds);
@ -22,11 +20,7 @@ export const RegionalGuidanceEntityList = memo(() => {
if (rgIds.length > 0) { if (rgIds.length > 0) {
return ( return (
<CanvasEntityGroupList <CanvasEntityGroupList type="regional_guidance" isSelected={isSelected}>
type="regional_guidance"
title={t('controlLayers.regionalGuidance_withCount', { count: rgIds.length })}
isSelected={isSelected}
>
{rgIds.map((id) => ( {rgIds.map((id) => (
<RegionalGuidance key={id} id={id} /> <RegionalGuidance key={id} id={id} />
))} ))}

View File

@ -1,6 +1,5 @@
import { IconButton } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice'; import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
@ -22,7 +21,8 @@ export const CanvasEntityDeleteButton = memo(() => {
tooltip={t('common.delete')} tooltip={t('common.delete')}
icon={<PiTrashSimpleBold />} icon={<PiTrashSimpleBold />}
onClick={onClick} onClick={onClick}
onDoubleClick={stopPropagation} // double click expands the layer variant="link"
alignSelf="stretch"
/> />
); );
}); });

View File

@ -26,7 +26,7 @@ export const CanvasEntityEnabledToggle = memo(() => {
variant="outline" variant="outline"
icon={isEnabled ? <PiCheckBold /> : undefined} icon={isEnabled ? <PiCheckBold /> : undefined}
onClick={onClick} onClick={onClick}
colorScheme="base" colorScheme={isEnabled ? 'invokeBlue' : 'base'}
onDoubleClick={stopPropagation} // double click expands the layer onDoubleClick={stopPropagation} // double click expands the layer
/> />
); );

View File

@ -1,41 +1,25 @@
import { Flex, Switch, Text } from '@invoke-ai/ui-library'; import { Flex, Spacer, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit'; import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
import {
allEntitiesOfTypeToggled,
selectAllEntitiesOfType,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { memo, useCallback, useMemo } from 'react'; import { memo } from 'react';
type Props = PropsWithChildren<{ type Props = PropsWithChildren<{
title: string;
isSelected: boolean; isSelected: boolean;
type: CanvasEntityIdentifier['type']; type: CanvasEntityIdentifier['type'];
}>; }>;
export const CanvasEntityGroupList = memo(({ title, isSelected, type, children }: Props) => { export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props) => {
const dispatch = useAppDispatch(); const title = useEntityTypeTitle(type);
const selectAreAllEnabled = useMemo(
() =>
createSelector(selectCanvasV2Slice, (canvasV2) => {
return selectAllEntitiesOfType(canvasV2, type).every((entity) => entity.isEnabled);
}),
[type]
);
const areAllEnabled = useAppSelector(selectAreAllEnabled);
const onChange = useCallback(() => {
dispatch(allEntitiesOfTypeToggled({ type }));
}, [dispatch, type]);
return ( return (
<Flex flexDir="column" gap={2}> <Flex flexDir="column" gap={2}>
<Flex justifyContent="space-between" alignItems="center"> <Flex justifyContent="space-between" alignItems="center" gap={3}>
<Text color={isSelected ? 'base.200' : 'base.500'} fontWeight="semibold" userSelect="none"> <Text color={isSelected ? 'base.200' : 'base.500'} fontWeight="semibold" userSelect="none">
{title} {title}
</Text> </Text>
<Switch size="sm" isChecked={areAllEnabled} onChange={onChange} pe={1} /> <Spacer />
{type !== 'ip_adapter' && <CanvasEntityTypeIsHiddenToggle type={type} />}
</Flex> </Flex>
{children} {children}
</Flex> </Flex>

View File

@ -0,0 +1,37 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden';
import { useEntityTypeString } from 'features/controlLayers/hooks/useEntityTypeString';
import { allEntitiesOfTypeIsHiddenToggled } from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiEyeBold, PiEyeClosedBold } from 'react-icons/pi';
type Props = {
type: CanvasEntityIdentifier['type'];
};
export const CanvasEntityTypeIsHiddenToggle = memo(({ type }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isHidden = useEntityTypeIsHidden(type);
const typeString = useEntityTypeString(type);
const onClick = useCallback(() => {
dispatch(allEntitiesOfTypeIsHiddenToggled({ type }));
}, [dispatch, type]);
return (
<IconButton
size="sm"
aria-label={t(isHidden ? 'controlLayers.hidingType' : 'controlLayers.showingType', { type: typeString })}
tooltip={t(isHidden ? 'controlLayers.hidingType' : 'controlLayers.showingType', { type: typeString })}
variant="link"
icon={isHidden ? <PiEyeClosedBold /> : <PiEyeBold />}
onClick={onClick}
alignSelf="stretch"
/>
);
});
CanvasEntityTypeIsHiddenToggle.displayName = 'CanvasEntityTypeIsHiddenToggle';

View File

@ -0,0 +1,30 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntityCount = (type: CanvasEntityIdentifier['type']): number => {
const selectEntityCount = useMemo(
() =>
createSelector(selectCanvasV2Slice, (canvasV2) => {
switch (type) {
case 'control_layer':
return canvasV2.controlLayers.entities.length;
case 'raster_layer':
return canvasV2.rasterLayers.entities.length;
case 'inpaint_mask':
return canvasV2.inpaintMasks.entities.length;
case 'regional_guidance':
return canvasV2.regions.entities.length;
case 'ip_adapter':
return canvasV2.ipAdapters.entities.length;
default:
return 0;
}
}),
[type]
);
const entityCount = useAppSelector(selectEntityCount);
return entityCount;
};

View File

@ -29,15 +29,15 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
const parts: string[] = []; const parts: string[] = [];
if (entityIdentifier.type === 'inpaint_mask') { if (entityIdentifier.type === 'inpaint_mask') {
parts.push(t('controlLayers.inpaintMask')); parts.push(t('controlLayers.inpaintMask', { count: 1 }));
} else if (entityIdentifier.type === 'control_layer') { } else if (entityIdentifier.type === 'control_layer') {
parts.push(t('controlLayers.controlLayer')); parts.push(t('controlLayers.controlLayer', { count: 1 }));
} else if (entityIdentifier.type === 'raster_layer') { } else if (entityIdentifier.type === 'raster_layer') {
parts.push(t('controlLayers.rasterLayer')); parts.push(t('controlLayers.rasterLayer', { count: 1 }));
} else if (entityIdentifier.type === 'ip_adapter') { } else if (entityIdentifier.type === 'ip_adapter') {
parts.push(t('common.ipAdapter')); parts.push(t('common.ipAdapter', { count: 1 }));
} else if (entityIdentifier.type === 'regional_guidance') { } else if (entityIdentifier.type === 'regional_guidance') {
parts.push(t('controlLayers.regionalGuidance')); parts.push(t('controlLayers.regionalGuidance', { count: 1 }));
} else { } else {
assert(false, 'Unexpected entity type'); assert(false, 'Unexpected entity type');
} }

View File

@ -0,0 +1,29 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntityTypeIsHidden = (type: CanvasEntityIdentifier['type']): boolean => {
const selectIsHidden = useMemo(
() =>
createSelector(selectCanvasV2Slice, (canvasV2) => {
switch (type) {
case 'control_layer':
return canvasV2.controlLayers.isHidden;
case 'raster_layer':
return canvasV2.rasterLayers.isHidden;
case 'inpaint_mask':
return canvasV2.inpaintMasks.isHidden;
case 'regional_guidance':
return canvasV2.regions.isHidden;
case 'ip_adapter':
default:
return false;
}
}),
[type]
);
const isHidden = useAppSelector(selectIsHidden);
return isHidden;
};

View File

@ -0,0 +1,26 @@
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const useEntityTypeString = (type: CanvasEntityIdentifier['type']): string => {
const { t } = useTranslation();
const typeString = useMemo(() => {
switch (type) {
case 'control_layer':
return t('controlLayers.controlLayer', { count: 0 });
case 'raster_layer':
return t('controlLayers.rasterLayer', { count: 0 });
case 'inpaint_mask':
return t('controlLayers.inpaintMask', { count: 0 });
case 'regional_guidance':
return t('controlLayers.regionalGuidance', { count: 0 });
case 'ip_adapter':
return t('controlLayers.ipAdapter', { count: 0 });
default:
return '';
}
}, [type, t]);
return typeString;
};

View File

@ -0,0 +1,32 @@
import { useEntityCount } from 'features/controlLayers/hooks/useEntityCount';
import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const useEntityTypeTitle = (type: CanvasEntityIdentifier['type']): string => {
const { t } = useTranslation();
const isHidden = useEntityTypeIsHidden(type);
const count = useEntityCount(type);
const title = useMemo(() => {
const context = isHidden ? 'hidden' : 'visible';
switch (type) {
case 'control_layer':
return t('controlLayers.controlLayers_withCount', { count, context });
case 'raster_layer':
return t('controlLayers.rasterLayers_withCount', { count, context });
case 'inpaint_mask':
return t('controlLayers.inpaintMasks_withCount', { count, context });
case 'regional_guidance':
return t('controlLayers.regionalGuidance_withCount', { count, context });
case 'ip_adapter':
return t('controlLayers.ipAdapters_withCount', { count, context });
default:
return '';
}
}, [type, t, count, isHidden]);
return title;
};

View File

@ -319,6 +319,12 @@ export class CanvasManager {
this.background.render(); this.background.render();
} }
if (isFirstRender || state.rasterLayers.isHidden !== prevState.rasterLayers.isHidden) {
for (const adapter of this.rasterLayerAdapters.values()) {
adapter.renderer.updateOpacity(state.rasterLayers.isHidden ? 0 : adapter.state.opacity);
}
}
if (isFirstRender || state.rasterLayers.entities !== prevState.rasterLayers.entities) { if (isFirstRender || state.rasterLayers.entities !== prevState.rasterLayers.entities) {
this.log.debug('Rendering raster layers'); this.log.debug('Rendering raster layers');
@ -344,6 +350,12 @@ export class CanvasManager {
} }
} }
if (isFirstRender || state.controlLayers.isHidden !== prevState.controlLayers.isHidden) {
for (const adapter of this.controlLayerAdapters.values()) {
adapter.renderer.updateOpacity(state.controlLayers.isHidden ? 0 : adapter.state.opacity);
}
}
if (isFirstRender || state.controlLayers.entities !== prevState.controlLayers.entities) { if (isFirstRender || state.controlLayers.entities !== prevState.controlLayers.entities) {
this.log.debug('Rendering control layers'); this.log.debug('Rendering control layers');
@ -369,6 +381,12 @@ export class CanvasManager {
} }
} }
if (isFirstRender || state.regions.isHidden !== prevState.regions.isHidden) {
for (const adapter of this.regionalGuidanceAdapters.values()) {
adapter.renderer.updateOpacity(state.regions.isHidden ? 0 : adapter.state.opacity);
}
}
if ( if (
isFirstRender || isFirstRender ||
state.regions.entities !== prevState.regions.entities || state.regions.entities !== prevState.regions.entities ||
@ -400,6 +418,12 @@ export class CanvasManager {
} }
} }
if (isFirstRender || state.inpaintMasks.isHidden !== prevState.inpaintMasks.isHidden) {
for (const adapter of this.inpaintMaskAdapters.values()) {
adapter.renderer.updateOpacity(state.inpaintMasks.isHidden ? 0 : adapter.state.opacity);
}
}
if ( if (
isFirstRender || isFirstRender ||
state.inpaintMasks.entities !== prevState.inpaintMasks.entities || state.inpaintMasks.entities !== prevState.inpaintMasks.entities ||

View File

@ -398,3 +398,13 @@ export const getRectUnion = (...rects: Rect[]): Rect => {
}, getEmptyRect()); }, getEmptyRect());
return rect; return rect;
}; };
/**
* Asserts that the value is never reached. Used for exhaustive checks in switch statements or conditional logic to ensure
* that all possible values are handled.
* @param value The value that should never be reached
* @throws An error with the value that was not handled
*/
export const exhaustiveCheck = (value: never): never => {
assert(false, `Unhandled value: ${value}`);
};

View File

@ -3,6 +3,7 @@ import { createAction, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store'; import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { deepClone } from 'common/util/deepClone'; import { deepClone } from 'common/util/deepClone';
import { exhaustiveCheck } from 'features/controlLayers/konva/util';
import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers'; import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers';
@ -24,12 +25,8 @@ import { atom } from 'nanostores';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import type { import type {
CanvasControlLayerState,
CanvasEntityIdentifier, CanvasEntityIdentifier,
CanvasEntityState, CanvasEntityState,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasRegionalGuidanceState,
CanvasV2State, CanvasV2State,
Coordinate, Coordinate,
EntityBrushLineAddedPayload, EntityBrushLineAddedPayload,
@ -46,15 +43,19 @@ const initialState: CanvasV2State = {
_version: 3, _version: 3,
selectedEntityIdentifier: null, selectedEntityIdentifier: null,
rasterLayers: { rasterLayers: {
isHidden: false,
entities: [], entities: [],
}, },
controlLayers: { controlLayers: {
isHidden: false,
entities: [], entities: [],
}, },
inpaintMasks: { inpaintMasks: {
isHidden: false,
entities: [], entities: [],
}, },
regions: { regions: {
isHidden: false,
entities: [], entities: [],
}, },
loras: [], loras: [],
@ -408,35 +409,28 @@ export const canvasV2Slice = createSlice({
} }
entity.opacity = opacity; entity.opacity = opacity;
}, },
allEntitiesOfTypeToggled: (state, action: PayloadAction<{ type: CanvasEntityIdentifier['type'] }>) => { allEntitiesOfTypeIsHiddenToggled: (state, action: PayloadAction<{ type: CanvasEntityIdentifier['type'] }>) => {
const { type } = action.payload; const { type } = action.payload;
let entities: (
| CanvasRasterLayerState
| CanvasControlLayerState
| CanvasInpaintMaskState
| CanvasRegionalGuidanceState
)[];
switch (type) { switch (type) {
case 'raster_layer': case 'raster_layer':
entities = state.rasterLayers.entities; state.rasterLayers.isHidden = !state.rasterLayers.isHidden;
break; break;
case 'control_layer': case 'control_layer':
entities = state.controlLayers.entities; state.controlLayers.isHidden = !state.controlLayers.isHidden;
break; break;
case 'inpaint_mask': case 'inpaint_mask':
entities = state.inpaintMasks.entities; state.inpaintMasks.isHidden = !state.inpaintMasks.isHidden;
break; break;
case 'regional_guidance': case 'regional_guidance':
entities = state.regions.entities; state.regions.isHidden = !state.regions.isHidden;
break; break;
default: case 'ip_adapter':
assert(false, 'Not implemented'); // no-op
break;
default: {
exhaustiveCheck(type);
} }
const allEnabled = entities.every((entity) => entity.isEnabled);
for (const entity of entities) {
entity.isEnabled = !allEnabled;
} }
}, },
allEntitiesDeleted: (state) => { allEntitiesDeleted: (state) => {
@ -496,7 +490,7 @@ export const {
entityArrangedBackwardOne, entityArrangedBackwardOne,
entityArrangedToBack, entityArrangedToBack,
entityOpacityChanged, entityOpacityChanged,
allEntitiesOfTypeToggled, allEntitiesOfTypeIsHiddenToggled,
// bbox // bbox
bboxChanged, bboxChanged,
bboxScaledSizeChanged, bboxScaledSizeChanged,

View File

@ -843,15 +843,19 @@ export type CanvasV2State = {
_version: 3; _version: 3;
selectedEntityIdentifier: CanvasEntityIdentifier | null; selectedEntityIdentifier: CanvasEntityIdentifier | null;
inpaintMasks: { inpaintMasks: {
isHidden: boolean;
entities: CanvasInpaintMaskState[]; entities: CanvasInpaintMaskState[];
}; };
rasterLayers: { rasterLayers: {
isHidden: boolean;
entities: CanvasRasterLayerState[]; entities: CanvasRasterLayerState[];
}; };
controlLayers: { controlLayers: {
isHidden: boolean;
entities: CanvasControlLayerState[]; entities: CanvasControlLayerState[];
}; };
regions: { regions: {
isHidden: boolean;
entities: CanvasRegionalGuidanceState[]; entities: CanvasRegionalGuidanceState[];
}; };
ipAdapters: { ipAdapters: {