feat(ui): selected entity alert

This commit is contained in:
psychedelicious
2024-09-09 22:46:17 +10:00
parent 1a53e8dc5c
commit 2a022a811c
7 changed files with 124 additions and 88 deletions

View File

@@ -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"
}
}

View File

@@ -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>
);
});

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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>
);
});

View File

@@ -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;
};