mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): selected entity alert
This commit is contained in:
@@ -1831,11 +1831,12 @@
|
||||
"autoSave": "Auto Save",
|
||||
"entityStatus": {
|
||||
"selectedEntity": "Selected Entity",
|
||||
"filtering": "Filtering",
|
||||
"transforming": "Transforming",
|
||||
"locked": "Locked",
|
||||
"hidden": "Hidden",
|
||||
"disabled": "Disabled",
|
||||
"selectedEntityIs": "Selected Entity is",
|
||||
"isFiltering": "is filtering",
|
||||
"isTransforming": "is transforming",
|
||||
"isLocked": "is locked",
|
||||
"isHidden": "is hidden",
|
||||
"isDisabled": "is disabled",
|
||||
"enabled": "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
import { Grid } from '@invoke-ai/ui-library';
|
||||
import { CanvasHUDItemAutoSave } from 'features/controlLayers/components/HUD/CanvasHUDItemAutoSave';
|
||||
import { CanvasHUDItemBbox } from 'features/controlLayers/components/HUD/CanvasHUDItemBbox';
|
||||
import { CanvasHUDItemScaledBbox } from 'features/controlLayers/components/HUD/CanvasHUDItemScaledBbox';
|
||||
import { CanvasHUDItemSelectedEntityStatus } from 'features/controlLayers/components/HUD/CanvasHUDItemSelectedEntityStatus';
|
||||
import { CanvasHUDItemSnapToGrid } from 'features/controlLayers/components/HUD/CanvasHUDItemSnapToGrid';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const CanvasHUD = memo(() => {
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Grid
|
||||
bg="base.900"
|
||||
borderBottomEndRadius="base"
|
||||
p={2}
|
||||
gap={2}
|
||||
borderRadius="base"
|
||||
templateColumns="1fr 1fr"
|
||||
opacity={0.6}
|
||||
minW={64}
|
||||
>
|
||||
<CanvasHUDItemBbox />
|
||||
<CanvasHUDItemScaledBbox />
|
||||
<CanvasHUDItemSnapToGrid />
|
||||
<CanvasHUDItemAutoSave />
|
||||
<CanvasHUDItemSelectedEntityStatus />
|
||||
</Grid>
|
||||
</CanvasManagerProviderGate>
|
||||
<Grid
|
||||
bg="base.900"
|
||||
borderBottomEndRadius="base"
|
||||
p={2}
|
||||
gap={1}
|
||||
borderRadius="base"
|
||||
templateColumns="1fr 1fr"
|
||||
opacity={0.6}
|
||||
minW={64}
|
||||
>
|
||||
<CanvasHUDItemBbox />
|
||||
<CanvasHUDItemScaledBbox />
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasHUDItem } from 'features/controlLayers/components/HUD/CanvasHUDItem';
|
||||
import { selectAutoSave } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasHUDItemAutoSave = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const autoSave = useAppSelector(selectAutoSave);
|
||||
|
||||
return <CanvasHUDItem label={t('controlLayers.HUD.autoSave')} value={autoSave ? t('common.on') : t('common.off')} />;
|
||||
});
|
||||
|
||||
CanvasHUDItemAutoSave.displayName = 'CanvasHUDItemAutoSave';
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasHUDItem } from 'features/controlLayers/components/HUD/CanvasHUDItem';
|
||||
import { selectSnapToGrid } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasHUDItemSnapToGrid = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const snapToGrid = useAppSelector(selectSnapToGrid);
|
||||
|
||||
return (
|
||||
<CanvasHUDItem
|
||||
label={t('controlLayers.settings.snapToGrid.label')}
|
||||
value={snapToGrid ? t('common.on') : t('common.off')}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasHUDItemSnapToGrid.displayName = 'CanvasHUDItemSnapToGrid';
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Box, Flex, Icon, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import type { Property } from 'csstype';
|
||||
import { CanvasHUDItem } from 'features/controlLayers/components/HUD/CanvasHUDItem';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/hooks/useEntityAdapter';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle';
|
||||
import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden';
|
||||
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'
|
||||
import { atom } from 'nanostores';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiWarningCircleFill } from 'react-icons/pi';
|
||||
|
||||
type ContentProps = {
|
||||
entityIdentifier: CanvasEntityIdentifier;
|
||||
@@ -28,8 +30,9 @@ type EntityStatus = {
|
||||
color?: Property.Color;
|
||||
};
|
||||
|
||||
const CanvasHUDItemSelectedEntityStatusContent = memo(({ entityIdentifier, adapter }: ContentProps) => {
|
||||
const CanvasSelectedEntityStatusAlertContent = memo(({ entityIdentifier, adapter }: ContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
const title = useEntityTitle(entityIdentifier);
|
||||
const selectIsEnabled = useMemo(
|
||||
() => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).isEnabled),
|
||||
[entityIdentifier]
|
||||
@@ -44,61 +47,79 @@ const CanvasHUDItemSelectedEntityStatusContent = memo(({ entityIdentifier, adapt
|
||||
const isFiltering = useStore(adapter.filterer?.$isFiltering ?? $isFilteringFallback);
|
||||
const isTransforming = useStore(adapter.transformer.$isTransforming);
|
||||
|
||||
const status = useMemo<EntityStatus>(() => {
|
||||
const status = useMemo<EntityStatus | null>(() => {
|
||||
if (isFiltering) {
|
||||
return {
|
||||
value: t('controlLayers.HUD.entityStatus.filtering'),
|
||||
color: 'invokeYellow.300',
|
||||
value: t('controlLayers.HUD.entityStatus.isFiltering'),
|
||||
color: 'invokeBlue.300',
|
||||
};
|
||||
}
|
||||
|
||||
if (isTransforming) {
|
||||
return {
|
||||
value: t('controlLayers.HUD.entityStatus.transforming'),
|
||||
color: 'invokeYellow.300',
|
||||
value: t('controlLayers.HUD.entityStatus.isTransforming'),
|
||||
color: 'invokeBlue.300',
|
||||
};
|
||||
}
|
||||
|
||||
if (isHidden) {
|
||||
return {
|
||||
value: t('controlLayers.HUD.entityStatus.hidden'),
|
||||
value: t('controlLayers.HUD.entityStatus.isHidden'),
|
||||
color: 'invokePurple.300',
|
||||
};
|
||||
}
|
||||
|
||||
if (isLocked) {
|
||||
return {
|
||||
value: t('controlLayers.HUD.entityStatus.locked'),
|
||||
value: t('controlLayers.HUD.entityStatus.isLocked'),
|
||||
color: 'invokeRed.300',
|
||||
};
|
||||
}
|
||||
|
||||
if (!isEnabled) {
|
||||
return {
|
||||
value: t('controlLayers.HUD.entityStatus.disabled'),
|
||||
value: t('controlLayers.HUD.entityStatus.isDisabled'),
|
||||
color: 'invokeRed.300',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: t('controlLayers.HUD.entityStatus.enabled'),
|
||||
};
|
||||
return null;
|
||||
}, [isFiltering, isTransforming, isHidden, isLocked, isEnabled, t]);
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CanvasHUDItem
|
||||
label={t('controlLayers.HUD.entityStatus.selectedEntity')}
|
||||
value={status.value}
|
||||
color={status.color}
|
||||
<Box position="relative" shadow="dark-lg">
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
bg={status.color}
|
||||
opacity={0.3}
|
||||
borderRadius="base"
|
||||
borderColor="whiteAlpha.400"
|
||||
borderWidth={1}
|
||||
/>
|
||||
</>
|
||||
<Flex px={6} py={4} gap={6} alignItems="center" justifyContent="center">
|
||||
<Icon as={PiWarningCircleFill} />
|
||||
<Text as="span" h={8}>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
{title}
|
||||
</Text>{' '}
|
||||
{status.value}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasHUDItemSelectedEntityStatusContent.displayName = 'CanvasHUDItemSelectedEntityStatusContent';
|
||||
CanvasSelectedEntityStatusAlertContent.displayName = 'CanvasSelectedEntityStatusAlertContent';
|
||||
|
||||
export const CanvasHUDItemSelectedEntityStatus = memo(() => {
|
||||
export const CanvasSelectedEntityStatusAlert = memo(() => {
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const adapter = useEntityAdapterSafe(selectedEntityIdentifier);
|
||||
|
||||
@@ -106,7 +127,7 @@ export const CanvasHUDItemSelectedEntityStatus = memo(() => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <CanvasHUDItemSelectedEntityStatusContent entityIdentifier={selectedEntityIdentifier} adapter={adapter} />;
|
||||
return <CanvasSelectedEntityStatusAlertContent entityIdentifier={selectedEntityIdentifier} adapter={adapter} />;
|
||||
});
|
||||
|
||||
CanvasHUDItemSelectedEntityStatus.displayName = 'CanvasHUDItemSelectedEntityStatus';
|
||||
CanvasSelectedEntityStatusAlert.displayName = 'CanvasSelectedEntityStatusAlert';
|
||||
@@ -5,6 +5,8 @@ import { logger } from 'app/logging/logger';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
|
||||
import { CanvasSelectedEntityStatusAlert } from 'features/controlLayers/components/HUD/CanvasSelectedEntityStatusAlert';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
@@ -97,11 +99,16 @@ export const StageComponent = memo(() => {
|
||||
overflow="hidden"
|
||||
data-testid="control-layers-canvas"
|
||||
/>
|
||||
{showHUD && (
|
||||
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none">
|
||||
<CanvasHUD />
|
||||
<CanvasManagerProviderGate>
|
||||
{showHUD && (
|
||||
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none">
|
||||
<CanvasHUD />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex position="absolute" top={1} insetInlineEnd={1} pointerEvents="none">
|
||||
<CanvasSelectedEntityStatusAlert />
|
||||
</Flex>
|
||||
)}
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/kon
|
||||
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
|
||||
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
|
||||
import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, memo, useContext, useMemo, useSyncExternalStore } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
@@ -105,3 +106,51 @@ export const useEntityAdapter = ():
|
||||
assert(adapter, 'useEntityAdapter must be used within a CanvasRasterLayerAdapterGate');
|
||||
return adapter;
|
||||
};
|
||||
|
||||
export const useEntityAdapterSafe = (
|
||||
entityIdentifier: CanvasEntityIdentifier | null
|
||||
):
|
||||
| CanvasEntityAdapterRasterLayer
|
||||
| CanvasEntityAdapterControlLayer
|
||||
| CanvasEntityAdapterInpaintMask
|
||||
| CanvasEntityAdapterRegionalGuidance
|
||||
| null => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const regionalGuidanceAdapters = useSyncExternalStore(
|
||||
canvasManager.adapters.regionMasks.subscribe,
|
||||
canvasManager.adapters.regionMasks.getSnapshot
|
||||
);
|
||||
const rasterLayerAdapters = useSyncExternalStore(
|
||||
canvasManager.adapters.rasterLayers.subscribe,
|
||||
canvasManager.adapters.rasterLayers.getSnapshot
|
||||
);
|
||||
const controlLayerAdapters = useSyncExternalStore(
|
||||
canvasManager.adapters.controlLayers.subscribe,
|
||||
canvasManager.adapters.controlLayers.getSnapshot
|
||||
);
|
||||
const inpaintMaskAdapters = useSyncExternalStore(
|
||||
canvasManager.adapters.inpaintMasks.subscribe,
|
||||
canvasManager.adapters.inpaintMasks.getSnapshot
|
||||
);
|
||||
|
||||
const adapter = useMemo(() => {
|
||||
if (!entityIdentifier) {
|
||||
return null;
|
||||
}
|
||||
if (entityIdentifier.type === 'raster_layer') {
|
||||
return rasterLayerAdapters.get(entityIdentifier.id) ?? null;
|
||||
}
|
||||
if (entityIdentifier.type === 'control_layer') {
|
||||
return controlLayerAdapters.get(entityIdentifier.id) ?? null;
|
||||
}
|
||||
if (entityIdentifier.type === 'inpaint_mask') {
|
||||
return inpaintMaskAdapters.get(entityIdentifier.id) ?? null;
|
||||
}
|
||||
if (entityIdentifier.type === 'regional_guidance') {
|
||||
return regionalGuidanceAdapters.get(entityIdentifier.id) ?? null;
|
||||
}
|
||||
return null;
|
||||
}, [controlLayerAdapters, entityIdentifier, inpaintMaskAdapters, rasterLayerAdapters, regionalGuidanceAdapters]);
|
||||
|
||||
return adapter;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user