diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index d923980c6d..e163194aa4 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1666,25 +1666,37 @@ "addPositivePrompt": "Add $t(common.positivePrompt)", "addNegativePrompt": "Add $t(common.negativePrompt)", "addIPAdapter": "Add $t(common.ipAdapter)", - "regionalGuidance": "Regional Guidance", "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", "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", - "regionalGuidance_withCount": "Regional Guidance ({{count}})", - "controlAdapters_withCount": "Control Adapters ({{count}})", - "controlLayer": "Control Layer", - "controlLayers_withCount": "Control Layers ({{count}})", - "rasterLayers_withCount": "Raster Layers ({{count}})", - "ipAdapters_withCount": "IP Adapters ({{count}})", - "inpaintMasks_withCount": "Inpaint Masks ({{count}})", + "regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)", + "controlAdapters_withCount_hidden": "Control Adapters ({{count}} hidden)", + "controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)", + "rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)", + "ipAdapters_withCount_hidden": "IP Adapters ({{count}} hidden)", + "inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)", + "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)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalIPAdapter": "Global $t(common.ipAdapter)", "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)", "globalInitialImage": "Global Initial Image", "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", - "inpaintMask": "Inpaint Mask", "layer": "Layer", "opacityFilter": "Opacity Filter", "clearProcessor": "Clear Processor", @@ -1699,6 +1711,8 @@ "convertToRasterLayer": "Convert to Raster Layer", "enableTransparencyEffect": "Enable Transparency Effect", "disableTransparencyEffect": "Disable Transparency Effect", + "hidingType": "Hiding {{type}}", + "showingType": "Showing {{type}}", "fill": { "fillStyle": "Fill Style", "solid": "Solid", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx index 848d4598fc..f19e4ca6f3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx @@ -5,14 +5,12 @@ import { ControlLayer } from 'features/controlLayers/components/ControlLayer/Con import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { return canvasV2.controlLayers.entities.map(mapId).reverse(); }); export const ControlLayerEntityList = memo(() => { - const { t } = useTranslation(); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_layer')); const layerIds = useAppSelector(selectEntityIds); @@ -22,11 +20,7 @@ export const ControlLayerEntityList = memo(() => { if (layerIds.length > 0) { return ( - + {layerIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx index fca5420ceb..b0ad9108a8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx @@ -6,14 +6,12 @@ import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { return canvasV2.ipAdapters.entities.map(mapId).reverse(); }); export const IPAdapterList = memo(() => { - const { t } = useTranslation(); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'ip_adapter')); const ipaIds = useAppSelector(selectEntityIds); @@ -23,11 +21,7 @@ export const IPAdapterList = memo(() => { if (ipaIds.length > 0) { return ( - + {ipaIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx index b5d72bc675..42351a0dab 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx @@ -5,14 +5,12 @@ import { InpaintMask } from 'features/controlLayers/components/InpaintMask/Inpai import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { return canvasV2.inpaintMasks.entities.map(mapId).reverse(); }); export const InpaintMaskList = memo(() => { - const { t } = useTranslation(); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask')); const entityIds = useAppSelector(selectEntityIds); @@ -22,11 +20,7 @@ export const InpaintMaskList = memo(() => { if (entityIds.length > 0) { return ( - + {entityIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx index c7dc342067..5c5ee557dc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx @@ -5,14 +5,12 @@ import { RasterLayer } from 'features/controlLayers/components/RasterLayer/Raste import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { return canvasV2.rasterLayers.entities.map(mapId).reverse(); }); export const RasterLayerEntityList = memo(() => { - const { t } = useTranslation(); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'raster_layer')); const layerIds = useAppSelector(selectEntityIds); @@ -22,11 +20,7 @@ export const RasterLayerEntityList = memo(() => { if (layerIds.length > 0) { return ( - + {layerIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx index b03a1df635..8e4a5f588a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx @@ -5,14 +5,12 @@ import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuid import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { return canvasV2.regions.entities.map(mapId).reverse(); }); export const RegionalGuidanceEntityList = memo(() => { - const { t } = useTranslation(); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'regional_guidance')); const rgIds = useAppSelector(selectEntityIds); @@ -22,11 +20,7 @@ export const RegionalGuidanceEntityList = memo(() => { if (rgIds.length > 0) { return ( - + {rgIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx index c51410413b..5e6d89a44d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx @@ -1,6 +1,5 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { stopPropagation } from 'common/util/stopPropagation'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; @@ -22,7 +21,8 @@ export const CanvasEntityDeleteButton = memo(() => { tooltip={t('common.delete')} icon={} onClick={onClick} - onDoubleClick={stopPropagation} // double click expands the layer + variant="link" + alignSelf="stretch" /> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx index df33252be9..fbe310521b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx @@ -26,7 +26,7 @@ export const CanvasEntityEnabledToggle = memo(() => { variant="outline" icon={isEnabled ? : undefined} onClick={onClick} - colorScheme="base" + colorScheme={isEnabled ? 'invokeBlue' : 'base'} onDoubleClick={stopPropagation} // double click expands the layer /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx index 5bc840f9f5..51d8ed0017 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx @@ -1,41 +1,25 @@ -import { Flex, Switch, Text } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - allEntitiesOfTypeToggled, - selectAllEntitiesOfType, - selectCanvasV2Slice, -} from 'features/controlLayers/store/canvasV2Slice'; +import { Flex, Spacer, Text } from '@invoke-ai/ui-library'; +import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle'; +import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import type { PropsWithChildren } from 'react'; -import { memo, useCallback, useMemo } from 'react'; +import { memo } from 'react'; type Props = PropsWithChildren<{ - title: string; isSelected: boolean; type: CanvasEntityIdentifier['type']; }>; -export const CanvasEntityGroupList = memo(({ title, isSelected, type, children }: Props) => { - const dispatch = useAppDispatch(); - 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]); +export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props) => { + const title = useEntityTypeTitle(type); return ( - + {title} - + + {type !== 'ip_adapter' && } {children} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx new file mode 100644 index 0000000000..1e13042d3c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx @@ -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 ( + : } + onClick={onClick} + alignSelf="stretch" + /> + ); +}); + +CanvasEntityTypeIsHiddenToggle.displayName = 'CanvasEntityTypeIsHiddenToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityCount.ts new file mode 100644 index 0000000000..9c73cf32b5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityCount.ts @@ -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; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index 50f8bd8c8c..1c9586ce99 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -29,15 +29,15 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { const parts: string[] = []; if (entityIdentifier.type === 'inpaint_mask') { - parts.push(t('controlLayers.inpaintMask')); + parts.push(t('controlLayers.inpaintMask', { count: 1 })); } else if (entityIdentifier.type === 'control_layer') { - parts.push(t('controlLayers.controlLayer')); + parts.push(t('controlLayers.controlLayer', { count: 1 })); } else if (entityIdentifier.type === 'raster_layer') { - parts.push(t('controlLayers.rasterLayer')); + parts.push(t('controlLayers.rasterLayer', { count: 1 })); } else if (entityIdentifier.type === 'ip_adapter') { - parts.push(t('common.ipAdapter')); + parts.push(t('common.ipAdapter', { count: 1 })); } else if (entityIdentifier.type === 'regional_guidance') { - parts.push(t('controlLayers.regionalGuidance')); + parts.push(t('controlLayers.regionalGuidance', { count: 1 })); } else { assert(false, 'Unexpected entity type'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts new file mode 100644 index 0000000000..d35bf9efa0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts @@ -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; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts new file mode 100644 index 0000000000..5c1682cfe5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts @@ -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; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts new file mode 100644 index 0000000000..80a0210f26 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts @@ -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; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 7dc625df31..4a2456ba95 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -319,6 +319,12 @@ export class CanvasManager { 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) { 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) { 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 ( isFirstRender || 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 ( isFirstRender || state.inpaintMasks.entities !== prevState.inpaintMasks.entities || @@ -634,10 +658,10 @@ export class CanvasManager { this.log.warn({ id }, 'Raster layer adapter not found'); continue; } - this.log.trace({ id }, 'Drawing raster layer to composite canvas'); - const adapterCanvas = adapter.getCanvas(rect); - ctx.drawImage(adapterCanvas, 0, 0); - } + this.log.trace({ id }, 'Drawing raster layer to composite canvas'); + const adapterCanvas = adapter.getCanvas(rect); + ctx.drawImage(adapterCanvas, 0, 0); + } this.canvasCache.set(hash, canvas); return canvas; }; @@ -666,10 +690,10 @@ export class CanvasManager { this.log.warn({ id }, 'Inpaint mask adapter not found'); continue; } - this.log.trace({ id }, 'Drawing inpaint mask to composite canvas'); - const adapterCanvas = adapter.getCanvas(rect); - ctx.drawImage(adapterCanvas, 0, 0); - } + this.log.trace({ id }, 'Drawing inpaint mask to composite canvas'); + const adapterCanvas = adapter.getCanvas(rect); + ctx.drawImage(adapterCanvas, 0, 0); + } this.canvasCache.set(hash, canvas); return canvas; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index e1e66ac07d..2c4037242d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -398,3 +398,13 @@ export const getRectUnion = (...rects: Rect[]): Rect => { }, getEmptyRect()); 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}`); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 6f12af2ee3..fd373c9bcf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -3,6 +3,7 @@ import { createAction, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; +import { exhaustiveCheck } from 'features/controlLayers/konva/util'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers'; @@ -24,12 +25,8 @@ import { atom } from 'nanostores'; import { assert } from 'tsafe'; import type { - CanvasControlLayerState, CanvasEntityIdentifier, CanvasEntityState, - CanvasInpaintMaskState, - CanvasRasterLayerState, - CanvasRegionalGuidanceState, CanvasV2State, Coordinate, EntityBrushLineAddedPayload, @@ -46,15 +43,19 @@ const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: null, rasterLayers: { + isHidden: false, entities: [], }, controlLayers: { + isHidden: false, entities: [], }, inpaintMasks: { + isHidden: false, entities: [], }, regions: { + isHidden: false, entities: [], }, loras: [], @@ -408,35 +409,28 @@ export const canvasV2Slice = createSlice({ } entity.opacity = opacity; }, - allEntitiesOfTypeToggled: (state, action: PayloadAction<{ type: CanvasEntityIdentifier['type'] }>) => { + allEntitiesOfTypeIsHiddenToggled: (state, action: PayloadAction<{ type: CanvasEntityIdentifier['type'] }>) => { const { type } = action.payload; - let entities: ( - | CanvasRasterLayerState - | CanvasControlLayerState - | CanvasInpaintMaskState - | CanvasRegionalGuidanceState - )[]; switch (type) { case 'raster_layer': - entities = state.rasterLayers.entities; + state.rasterLayers.isHidden = !state.rasterLayers.isHidden; break; case 'control_layer': - entities = state.controlLayers.entities; + state.controlLayers.isHidden = !state.controlLayers.isHidden; break; case 'inpaint_mask': - entities = state.inpaintMasks.entities; + state.inpaintMasks.isHidden = !state.inpaintMasks.isHidden; break; case 'regional_guidance': - entities = state.regions.entities; + state.regions.isHidden = !state.regions.isHidden; break; - default: - assert(false, 'Not implemented'); - } - - const allEnabled = entities.every((entity) => entity.isEnabled); - for (const entity of entities) { - entity.isEnabled = !allEnabled; + case 'ip_adapter': + // no-op + break; + default: { + exhaustiveCheck(type); + } } }, allEntitiesDeleted: (state) => { @@ -496,7 +490,7 @@ export const { entityArrangedBackwardOne, entityArrangedToBack, entityOpacityChanged, - allEntitiesOfTypeToggled, + allEntitiesOfTypeIsHiddenToggled, // bbox bboxChanged, bboxScaledSizeChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 1058df3401..d0d291ef13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -843,15 +843,19 @@ export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; inpaintMasks: { + isHidden: boolean; entities: CanvasInpaintMaskState[]; }; rasterLayers: { + isHidden: boolean; entities: CanvasRasterLayerState[]; }; controlLayers: { + isHidden: boolean; entities: CanvasControlLayerState[]; }; regions: { + isHidden: boolean; entities: CanvasRegionalGuidanceState[]; }; ipAdapters: {