mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): layer opacity via caching
This commit is contained in:
parent
5f2a7feeee
commit
0839eac0f7
@ -1,4 +1,4 @@
|
|||||||
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
|
import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
|
||||||
import { controlLayerAdded, ipaAdded, rasterLayerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice';
|
import { controlLayerAdded, ipaAdded, rasterLayerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
@ -27,13 +27,12 @@ export const AddLayerButton = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={IconButton}
|
||||||
leftIcon={<PiPlusBold />}
|
aria-label={t('controlLayers.addLayer')}
|
||||||
variant="ghost"
|
icon={<PiPlusBold />}
|
||||||
|
variant="link"
|
||||||
data-testid="control-layers-add-layer-menu-button"
|
data-testid="control-layers-add-layer-menu-button"
|
||||||
>
|
/>
|
||||||
{t('controlLayers.addLayer')}
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem onClick={addRGLayer}>{t('controlLayers.regionalGuidanceLayer')}</MenuItem>
|
<MenuItem onClick={addRGLayer}>{t('controlLayers.regionalGuidanceLayer')}</MenuItem>
|
||||||
<MenuItem onClick={addRasterLayer}>{t('controlLayers.rasterLayer')}</MenuItem>
|
<MenuItem onClick={addRasterLayer}>{t('controlLayers.rasterLayer')}</MenuItem>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
|
import { CanvasEntityOpacity } from 'features/controlLayers/components/common/CanvasEntityOpacity';
|
||||||
import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList';
|
import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList';
|
||||||
import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask';
|
import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask';
|
||||||
import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList';
|
import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList';
|
||||||
@ -10,7 +11,8 @@ import { memo } from 'react';
|
|||||||
export const CanvasEntityList = memo(() => {
|
export const CanvasEntityList = memo(() => {
|
||||||
return (
|
return (
|
||||||
<ScrollableContent>
|
<ScrollableContent>
|
||||||
<Flex flexDir="column" gap={2} data-testid="control-layers-layer-list">
|
<Flex flexDir="column" gap={4} pt={2} data-testid="control-layers-layer-list">
|
||||||
|
<CanvasEntityOpacity />
|
||||||
<InpaintMask />
|
<InpaintMask />
|
||||||
<RegionalGuidanceEntityList />
|
<RegionalGuidanceEntityList />
|
||||||
<IPAdapterList />
|
<IPAdapterList />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle';
|
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||||
import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer';
|
import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer';
|
||||||
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';
|
||||||
@ -22,15 +22,15 @@ export const ControlLayerEntityList = memo(() => {
|
|||||||
|
|
||||||
if (layerIds.length > 0) {
|
if (layerIds.length > 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<CanvasEntityGroupList
|
||||||
<CanvasEntityGroupTitle
|
type="control_layer"
|
||||||
title={t('controlLayers.controlLayers_withCount', { count: layerIds.length })}
|
title={t('controlLayers.controlLayers_withCount', { count: layerIds.length })}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
/>
|
>
|
||||||
{layerIds.map((id) => (
|
{layerIds.map((id) => (
|
||||||
<ControlLayer key={id} id={id} />
|
<ControlLayer key={id} id={id} />
|
||||||
))}
|
))}
|
||||||
</>
|
</CanvasEntityGroupList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
/* eslint-disable i18next/no-literal-string */
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { Flex } from '@invoke-ai/ui-library';
|
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton';
|
|
||||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList';
|
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList';
|
||||||
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
||||||
import { ResetAllEntitiesButton } from 'features/controlLayers/components/ResetAllEntitiesButton';
|
|
||||||
import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice';
|
import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@ -15,13 +12,7 @@ export const ControlLayersPanelContent = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<PanelGroup direction="vertical">
|
<PanelGroup direction="vertical">
|
||||||
<Panel id="canvas-entity-list-panel" order={0}>
|
<Panel id="canvas-entity-list-panel" order={0}>
|
||||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
<CanvasEntityList />
|
||||||
<Flex justifyContent="space-around">
|
|
||||||
<AddLayerButton />
|
|
||||||
<ResetAllEntitiesButton />
|
|
||||||
</Flex>
|
|
||||||
<CanvasEntityList />
|
|
||||||
</Flex>
|
|
||||||
</Panel>
|
</Panel>
|
||||||
{Boolean(filteringEntity) && (
|
{Boolean(filteringEntity) && (
|
||||||
<>
|
<>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable i18next/no-literal-string */
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle';
|
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||||
import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter';
|
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';
|
||||||
@ -23,15 +23,15 @@ export const IPAdapterList = memo(() => {
|
|||||||
|
|
||||||
if (ipaIds.length > 0) {
|
if (ipaIds.length > 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<CanvasEntityGroupList
|
||||||
<CanvasEntityGroupTitle
|
type="ip_adapter"
|
||||||
title={t('controlLayers.ipAdapters_withCount', { count: ipaIds.length })}
|
title={t('controlLayers.ipAdapters_withCount', { count: ipaIds.length })}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
/>
|
>
|
||||||
{ipaIds.map((id) => (
|
{ipaIds.map((id) => (
|
||||||
<IPAdapter key={id} id={id} />
|
<IPAdapter key={id} id={id} />
|
||||||
))}
|
))}
|
||||||
</>
|
</CanvasEntityGroupList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Spacer } from '@invoke-ai/ui-library';
|
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||||
import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle';
|
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||||
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
|
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
|
||||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
@ -18,8 +18,8 @@ export const InpaintMask = memo(() => {
|
|||||||
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask'));
|
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Flex flexDir="column" gap={2}>
|
||||||
<CanvasEntityGroupTitle title={t('controlLayers.inpaintMask')} isSelected={isSelected} />
|
<CanvasEntityGroupList title={t('controlLayers.inpaintMask')} isSelected={isSelected} type="inpaint_mask" />
|
||||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||||
<CanvasEntityContainer>
|
<CanvasEntityContainer>
|
||||||
<CanvasEntityHeader>
|
<CanvasEntityHeader>
|
||||||
@ -30,7 +30,7 @@ export const InpaintMask = memo(() => {
|
|||||||
</CanvasEntityHeader>
|
</CanvasEntityHeader>
|
||||||
</CanvasEntityContainer>
|
</CanvasEntityContainer>
|
||||||
</EntityIdentifierContext.Provider>
|
</EntityIdentifierContext.Provider>
|
||||||
</>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle';
|
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||||
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
|
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
|
||||||
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';
|
||||||
@ -22,15 +22,15 @@ export const RasterLayerEntityList = memo(() => {
|
|||||||
|
|
||||||
if (layerIds.length > 0) {
|
if (layerIds.length > 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<CanvasEntityGroupList
|
||||||
<CanvasEntityGroupTitle
|
type="raster_layer"
|
||||||
title={t('controlLayers.rasterLayers_withCount', { count: layerIds.length })}
|
title={t('controlLayers.rasterLayers_withCount', { count: layerIds.length })}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
/>
|
>
|
||||||
{layerIds.map((id) => (
|
{layerIds.map((id) => (
|
||||||
<RasterLayer key={id} id={id} />
|
<RasterLayer key={id} id={id} />
|
||||||
))}
|
))}
|
||||||
</>
|
</CanvasEntityGroupList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle';
|
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||||
import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidance';
|
import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidance';
|
||||||
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';
|
||||||
@ -22,15 +22,15 @@ export const RegionalGuidanceEntityList = memo(() => {
|
|||||||
|
|
||||||
if (rgIds.length > 0) {
|
if (rgIds.length > 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<CanvasEntityGroupList
|
||||||
<CanvasEntityGroupTitle
|
type="regional_guidance"
|
||||||
title={t('controlLayers.regionalGuidance_withCount', { count: rgIds.length })}
|
title={t('controlLayers.regionalGuidance_withCount', { count: rgIds.length })}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
/>
|
>
|
||||||
{rgIds.map((id) => (
|
{rgIds.map((id) => (
|
||||||
<RegionalGuidance key={id} id={id} />
|
<RegionalGuidance key={id} id={id} />
|
||||||
))}
|
))}
|
||||||
</>
|
</CanvasEntityGroupList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
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 type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { memo, useCallback, useMemo } 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]);
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" gap={2}>
|
||||||
|
<Flex justifyContent="space-between" alignItems="center">
|
||||||
|
<Text color={isSelected ? 'base.200' : 'base.500'} fontWeight="semibold" userSelect="none">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Switch size="sm" isChecked={areAllEnabled} onChange={onChange} pe={1} />
|
||||||
|
</Flex>
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CanvasEntityGroupList.displayName = 'CanvasEntityGroupList';
|
@ -1,17 +0,0 @@
|
|||||||
import { Text } from '@invoke-ai/ui-library';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string;
|
|
||||||
isSelected: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CanvasEntityGroupTitle = memo(({ title, isSelected }: Props) => {
|
|
||||||
return (
|
|
||||||
<Text color={isSelected ? 'base.200' : 'base.500'} fontWeight="semibold" userSelect="none">
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CanvasEntityGroupTitle.displayName = 'CanvasEntityGroupTitle';
|
|
@ -0,0 +1,180 @@
|
|||||||
|
import {
|
||||||
|
$shift,
|
||||||
|
CompositeSlider,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { snapToNearest } from 'features/controlLayers/konva/util';
|
||||||
|
import { entityOpacityChanged, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
|
import { isDrawableEntity } from 'features/controlLayers/store/types';
|
||||||
|
import { clamp, round } from 'lodash-es';
|
||||||
|
import type { KeyboardEvent } from 'react';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiCaretDownBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
function formatPct(v: number | string) {
|
||||||
|
if (isNaN(Number(v))) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${round(Number(v), 2).toLocaleString()}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapSliderValueToOpacity(value: number) {
|
||||||
|
return value / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapOpacityToSliderValue(opacity: number) {
|
||||||
|
return opacity * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSliderValue(value: number) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const marks = [
|
||||||
|
mapOpacityToSliderValue(0),
|
||||||
|
mapOpacityToSliderValue(0.25),
|
||||||
|
mapOpacityToSliderValue(0.5),
|
||||||
|
mapOpacityToSliderValue(0.75),
|
||||||
|
mapOpacityToSliderValue(1),
|
||||||
|
];
|
||||||
|
|
||||||
|
const sliderDefaultValue = mapOpacityToSliderValue(100);
|
||||||
|
|
||||||
|
const snapCandidates = marks.slice(1, marks.length - 1);
|
||||||
|
|
||||||
|
export const CanvasEntityOpacity = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier);
|
||||||
|
const opacity = useAppSelector((s) => {
|
||||||
|
const selectedEntityIdentifier = s.canvasV2.selectedEntityIdentifier;
|
||||||
|
if (!selectedEntityIdentifier) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const selectedEntity = selectEntity(s.canvasV2, selectedEntityIdentifier);
|
||||||
|
if (!selectedEntity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isDrawableEntity(selectedEntity)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return selectedEntity.opacity;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [localOpacity, setLocalOpacity] = useState((opacity ?? 1) * 100);
|
||||||
|
|
||||||
|
const onChangeSlider = useCallback(
|
||||||
|
(opacity: number) => {
|
||||||
|
if (!selectedEntityIdentifier) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let snappedOpacity = opacity;
|
||||||
|
// Do not snap if shift key is held
|
||||||
|
if (!$shift.get()) {
|
||||||
|
snappedOpacity = snapToNearest(opacity, snapCandidates, 2);
|
||||||
|
}
|
||||||
|
const mappedOpacity = mapSliderValueToOpacity(snappedOpacity);
|
||||||
|
|
||||||
|
dispatch(entityOpacityChanged({ entityIdentifier: selectedEntityIdentifier, opacity: mappedOpacity }));
|
||||||
|
},
|
||||||
|
[dispatch, selectedEntityIdentifier]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onBlur = useCallback(() => {
|
||||||
|
if (!selectedEntityIdentifier) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNaN(Number(localOpacity))) {
|
||||||
|
setLocalOpacity(100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(
|
||||||
|
entityOpacityChanged({ entityIdentifier: selectedEntityIdentifier, opacity: clamp(localOpacity / 100, 0, 1) })
|
||||||
|
);
|
||||||
|
}, [dispatch, localOpacity, selectedEntityIdentifier]);
|
||||||
|
|
||||||
|
const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => {
|
||||||
|
setLocalOpacity(valueAsNumber);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onBlur();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onBlur]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalOpacity((opacity ?? 1) * 100);
|
||||||
|
}, [opacity]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<FormControl w="min-content" gap={2}>
|
||||||
|
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
|
||||||
|
<PopoverAnchor>
|
||||||
|
<NumberInput
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={localOpacity}
|
||||||
|
onChange={onChangeNumberInput}
|
||||||
|
onBlur={onBlur}
|
||||||
|
w="76px"
|
||||||
|
format={formatPct}
|
||||||
|
defaultValue={1}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
clampValueOnBlur={false}
|
||||||
|
>
|
||||||
|
<NumberInputField paddingInlineEnd={7} />
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton
|
||||||
|
aria-label="open-slider"
|
||||||
|
icon={<PiCaretDownBold />}
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
position="absolute"
|
||||||
|
insetInlineEnd={0}
|
||||||
|
h="full"
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</NumberInput>
|
||||||
|
</PopoverAnchor>
|
||||||
|
</FormControl>
|
||||||
|
<PopoverContent w={200} pt={0} pb={2} px={4}>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody>
|
||||||
|
<CompositeSlider
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
value={localOpacity}
|
||||||
|
onChange={onChangeSlider}
|
||||||
|
defaultValue={sliderDefaultValue}
|
||||||
|
marks={marks}
|
||||||
|
formatValue={formatSliderValue}
|
||||||
|
alwaysShowMarks
|
||||||
|
/>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CanvasEntityOpacity.displayName = 'CanvasEntityOpacity';
|
@ -1,27 +0,0 @@
|
|||||||
import type { JSONObject } from 'common/types';
|
|
||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
|
||||||
import type { Logger } from 'roarr';
|
|
||||||
|
|
||||||
export abstract class CanvasEntity {
|
|
||||||
id: string;
|
|
||||||
manager: CanvasManager;
|
|
||||||
log: Logger;
|
|
||||||
|
|
||||||
constructor(id: string, manager: CanvasManager) {
|
|
||||||
this.id = id;
|
|
||||||
this.manager = manager;
|
|
||||||
this.log = this.manager.buildLogger(this.getLoggingContext);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Get a serializable representation of the entity.
|
|
||||||
*/
|
|
||||||
abstract repr(): JSONObject;
|
|
||||||
|
|
||||||
getLoggingContext = (extra?: Record<string, unknown>) => {
|
|
||||||
return {
|
|
||||||
...this.manager.getLoggingContext(),
|
|
||||||
layerId: this.id,
|
|
||||||
...extra,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
@ -4,11 +4,12 @@ import { CanvasFilter } from 'features/controlLayers/konva/CanvasFilter';
|
|||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
||||||
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
||||||
import type {
|
import {
|
||||||
CanvasControlLayerState,
|
type CanvasControlLayerState,
|
||||||
CanvasEntityIdentifier,
|
type CanvasEntityIdentifier,
|
||||||
CanvasRasterLayerState,
|
type CanvasRasterLayerState,
|
||||||
CanvasV2State,
|
type CanvasV2State,
|
||||||
|
getEntityIdentifier,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
@ -61,7 +62,7 @@ export class CanvasLayerAdapter {
|
|||||||
* Get this entity's entity identifier
|
* Get this entity's entity identifier
|
||||||
*/
|
*/
|
||||||
getEntityIdentifier = (): CanvasEntityIdentifier => {
|
getEntityIdentifier = (): CanvasEntityIdentifier => {
|
||||||
return { id: this.id, type: this.state.type };
|
return getEntityIdentifier(this.state);
|
||||||
};
|
};
|
||||||
|
|
||||||
destroy = (): void => {
|
destroy = (): void => {
|
||||||
@ -97,7 +98,7 @@ export class CanvasLayerAdapter {
|
|||||||
this.transformer.updatePosition({ position });
|
this.transformer.updatePosition({ position });
|
||||||
}
|
}
|
||||||
if (this.isFirstRender || opacity !== this.state.opacity) {
|
if (this.isFirstRender || opacity !== this.state.opacity) {
|
||||||
this.updateOpacity({ opacity });
|
this.renderer.updateOpacity(opacity);
|
||||||
}
|
}
|
||||||
// this.transformer.syncInteractionState();
|
// this.transformer.syncInteractionState();
|
||||||
|
|
||||||
@ -113,6 +114,7 @@ export class CanvasLayerAdapter {
|
|||||||
this.log.trace('Updating visibility');
|
this.log.trace('Updating visibility');
|
||||||
const isEnabled = get(arg, 'isEnabled', this.state.isEnabled);
|
const isEnabled = get(arg, 'isEnabled', this.state.isEnabled);
|
||||||
this.konva.layer.visible(isEnabled);
|
this.konva.layer.visible(isEnabled);
|
||||||
|
this.renderer.syncCache(isEnabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateObjects = async (arg?: { objects: CanvasRasterLayerState['objects'] }) => {
|
updateObjects = async (arg?: { objects: CanvasRasterLayerState['objects'] }) => {
|
||||||
|
@ -316,10 +316,10 @@ export class CanvasManager {
|
|||||||
if (this._isFirstRender || state.rasterLayers.entities !== this._prevState.rasterLayers.entities) {
|
if (this._isFirstRender || state.rasterLayers.entities !== this._prevState.rasterLayers.entities) {
|
||||||
this.log.debug('Rendering raster layers');
|
this.log.debug('Rendering raster layers');
|
||||||
|
|
||||||
for (const canvasLayer of this.rasterLayerAdapters.values()) {
|
for (const entityAdapter of this.rasterLayerAdapters.values()) {
|
||||||
if (!state.rasterLayers.entities.find((l) => l.id === canvasLayer.id)) {
|
if (!state.rasterLayers.entities.find((l) => l.id === entityAdapter.id)) {
|
||||||
await canvasLayer.destroy();
|
await entityAdapter.destroy();
|
||||||
this.rasterLayerAdapters.delete(canvasLayer.id);
|
this.rasterLayerAdapters.delete(entityAdapter.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,10 +341,10 @@ export class CanvasManager {
|
|||||||
if (this._isFirstRender || state.controlLayers.entities !== this._prevState.controlLayers.entities) {
|
if (this._isFirstRender || state.controlLayers.entities !== this._prevState.controlLayers.entities) {
|
||||||
this.log.debug('Rendering control layers');
|
this.log.debug('Rendering control layers');
|
||||||
|
|
||||||
for (const canvasLayer of this.controlLayerAdapters.values()) {
|
for (const entityAdapter of this.controlLayerAdapters.values()) {
|
||||||
if (!state.controlLayers.entities.find((l) => l.id === canvasLayer.id)) {
|
if (!state.controlLayers.entities.find((l) => l.id === entityAdapter.id)) {
|
||||||
await canvasLayer.destroy();
|
await entityAdapter.destroy();
|
||||||
this.controlLayerAdapters.delete(canvasLayer.id);
|
this.controlLayerAdapters.delete(entityAdapter.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,11 +3,12 @@ import { deepClone } from 'common/util/deepClone';
|
|||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
||||||
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
||||||
import type {
|
import {
|
||||||
CanvasEntityIdentifier,
|
type CanvasEntityIdentifier,
|
||||||
CanvasInpaintMaskState,
|
type CanvasInpaintMaskState,
|
||||||
CanvasRegionalGuidanceState,
|
type CanvasRegionalGuidanceState,
|
||||||
CanvasV2State,
|
type CanvasV2State,
|
||||||
|
getEntityIdentifier,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
@ -59,7 +60,7 @@ export class CanvasMaskAdapter {
|
|||||||
* Get this entity's entity identifier
|
* Get this entity's entity identifier
|
||||||
*/
|
*/
|
||||||
getEntityIdentifier = (): CanvasEntityIdentifier => {
|
getEntityIdentifier = (): CanvasEntityIdentifier => {
|
||||||
return { id: this.id, type: this.state.type };
|
return getEntityIdentifier(this.state)
|
||||||
};
|
};
|
||||||
|
|
||||||
destroy = (): void => {
|
destroy = (): void => {
|
||||||
@ -99,7 +100,11 @@ export class CanvasMaskAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.isFirstRender || state.fill !== this.state.fill) {
|
if (this.isFirstRender || state.fill !== this.state.fill) {
|
||||||
this.renderer.updateCompositingRect(state.fill);
|
this.renderer.updateCompositingRectFill(state.fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isFirstRender) {
|
||||||
|
this.renderer.updateCompositingRectSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.transformer.syncInteractionState();
|
// this.transformer.syncInteractionState();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { JSONObject } from 'common/types';
|
import type { JSONObject } from 'common/types';
|
||||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
|
import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
|
||||||
import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
|
import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
|
||||||
@ -21,7 +21,6 @@ import type {
|
|||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { RectConfig } from 'konva/lib/shapes/Rect';
|
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
import { getImageDTO, uploadImage } from 'services/api/endpoints/images';
|
import { getImageDTO, uploadImage } from 'services/api/endpoints/images';
|
||||||
@ -149,7 +148,8 @@ export class CanvasObjectRenderer {
|
|||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.manager.stateApi.$stageAttrs.listen(() => {
|
this.manager.stateApi.$stageAttrs.listen(() => {
|
||||||
if (this.konva.compositing && this.parent.type === 'mask_adapter') {
|
if (this.konva.compositing && this.parent.type === 'mask_adapter') {
|
||||||
this.updateCompositingRect(this.parent.state.fill);
|
this.updateCompositingRectFill(this.parent.state.fill);
|
||||||
|
this.updateCompositingRectSize();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -162,6 +162,7 @@ export class CanvasObjectRenderer {
|
|||||||
*/
|
*/
|
||||||
render = async (objectStates: AnyObjectState[]): Promise<boolean> => {
|
render = async (objectStates: AnyObjectState[]): Promise<boolean> => {
|
||||||
let didRender = false;
|
let didRender = false;
|
||||||
|
|
||||||
const objectIds = objectStates.map((objectState) => objectState.id);
|
const objectIds = objectStates.map((objectState) => objectState.id);
|
||||||
|
|
||||||
for (const renderer of this.renderers.values()) {
|
for (const renderer of this.renderers.values()) {
|
||||||
@ -180,32 +181,61 @@ export class CanvasObjectRenderer {
|
|||||||
didRender = (await this.renderObject(this.buffer)) || didRender;
|
didRender = (await this.renderObject(this.buffer)) || didRender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.syncCache(didRender);
|
||||||
|
|
||||||
return didRender;
|
return didRender;
|
||||||
};
|
};
|
||||||
|
|
||||||
updateCompositingRect = (fill: Fill) => {
|
syncCache = (force: boolean = false) => {
|
||||||
this.log.trace('Updating compositing rect');
|
if (this.renderers.size === 0) {
|
||||||
|
this.log.trace('Clearing object group cache');
|
||||||
|
this.konva.objectGroup.clearCache();
|
||||||
|
} else if (force || !this.konva.objectGroup.isCached()) {
|
||||||
|
this.log.trace('Caching object group');
|
||||||
|
this.konva.objectGroup.clearCache();
|
||||||
|
this.konva.objectGroup.cache();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCompositingRectFill = (fill: Fill) => {
|
||||||
|
this.log.trace('Updating compositing rect fill');
|
||||||
|
assert(this.konva.compositing, 'Missing compositing rect');
|
||||||
|
|
||||||
|
if (fill.style === 'solid') {
|
||||||
|
this.konva.compositing.rect.setAttrs({
|
||||||
|
fill: rgbColorToString(fill.color),
|
||||||
|
fillPriority: 'color',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.konva.compositing.rect.setAttrs({
|
||||||
|
fillPriority: 'pattern',
|
||||||
|
});
|
||||||
|
setFillPatternImage(this.konva.compositing.rect, fill.style, fill.color);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCompositingRectSize = () => {
|
||||||
|
this.log.trace('Updating compositing rect size');
|
||||||
assert(this.konva.compositing, 'Missing compositing rect');
|
assert(this.konva.compositing, 'Missing compositing rect');
|
||||||
|
|
||||||
const { x, y, width, height, scale } = this.manager.stateApi.$stageAttrs.get();
|
const { x, y, width, height, scale } = this.manager.stateApi.$stageAttrs.get();
|
||||||
|
|
||||||
const attrs: RectConfig = {
|
this.konva.compositing.rect.setAttrs({
|
||||||
x: -x / scale,
|
x: -x / scale,
|
||||||
y: -y / scale,
|
y: -y / scale,
|
||||||
width: width / scale,
|
width: width / scale,
|
||||||
height: height / scale,
|
height: height / scale,
|
||||||
};
|
fillPatternScaleX: 1 / scale,
|
||||||
|
fillPatternScaleY: 1 / scale,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (fill.style === 'solid') {
|
updateOpacity = (opacity: number) => {
|
||||||
attrs.fill = rgbaColorToString(fill.color);
|
this.log.trace('Updating opacity');
|
||||||
attrs.fillPriority = 'color';
|
if (this.konva.compositing) {
|
||||||
this.konva.compositing.rect.setAttrs(attrs);
|
this.konva.compositing.group.opacity(opacity);
|
||||||
} else {
|
} else {
|
||||||
attrs.fillPatternScaleX = 1 / scale;
|
this.konva.objectGroup.opacity(opacity);
|
||||||
attrs.fillPatternScaleY = 1 / scale;
|
|
||||||
attrs.fillPriority = 'pattern';
|
|
||||||
this.konva.compositing.rect.setAttrs(attrs);
|
|
||||||
setFillPatternImage(this.konva.compositing.rect, fill.style, fill.color);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -266,6 +296,10 @@ export class CanvasObjectRenderer {
|
|||||||
didRender = await renderer.update(objectState, force || isFirstRender);
|
didRender = await renderer.update(objectState, force || isFirstRender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (didRender && this.konva.objectGroup.isCached()) {
|
||||||
|
this.konva.objectGroup.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
return didRender;
|
return didRender;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -421,7 +455,7 @@ export class CanvasObjectRenderer {
|
|||||||
imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true);
|
imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true);
|
||||||
const imageObject = imageDTOToImageObject(imageDTO);
|
const imageObject = imageDTOToImageObject(imageDTO);
|
||||||
if (replaceObjects) {
|
if (replaceObjects) {
|
||||||
await this.renderObject(imageObject, true);
|
await this.renderObject(imageObject, true);
|
||||||
}
|
}
|
||||||
this.manager.stateApi.rasterizeEntity({
|
this.manager.stateApi.rasterizeEntity({
|
||||||
entityIdentifier: this.parent.getEntityIdentifier(),
|
entityIdentifier: this.parent.getEntityIdentifier(),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import type { FillStyle, RgbaColor } from 'features/controlLayers/store/types';
|
import type { FillStyle, RgbColor } from 'features/controlLayers/store/types';
|
||||||
|
|
||||||
import crosshatch from './pattern-crosshatch.svg?raw';
|
import crosshatch from './pattern-crosshatch.svg?raw';
|
||||||
import diagonal from './pattern-diagonal.svg?raw';
|
import diagonal from './pattern-diagonal.svg?raw';
|
||||||
@ -7,7 +7,7 @@ import grid from './pattern-grid.svg?raw';
|
|||||||
import horizontal from './pattern-horizontal.svg?raw';
|
import horizontal from './pattern-horizontal.svg?raw';
|
||||||
import vertical from './pattern-vertical.svg?raw';
|
import vertical from './pattern-vertical.svg?raw';
|
||||||
|
|
||||||
export function getPatternSVG(pattern: Exclude<FillStyle, 'solid'>, color: RgbaColor) {
|
export function getPatternSVG(pattern: Exclude<FillStyle, 'solid'>, color: RgbColor) {
|
||||||
let content: string = 'data:image/svg+xml;utf8,';
|
let content: string = 'data:image/svg+xml;utf8,';
|
||||||
if (pattern === 'crosshatch') {
|
if (pattern === 'crosshatch') {
|
||||||
content += crosshatch;
|
content += crosshatch;
|
||||||
@ -21,7 +21,7 @@ export function getPatternSVG(pattern: Exclude<FillStyle, 'solid'>, color: RgbaC
|
|||||||
content += grid;
|
content += grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
content = content.replaceAll('stroke:black', `stroke:${rgbaColorToString(color)}`);
|
content = content.replaceAll('stroke:black', `stroke:${rgbColorToString(color)}`);
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import { assert } from 'tsafe';
|
|||||||
import type {
|
import type {
|
||||||
CanvasControlLayerState,
|
CanvasControlLayerState,
|
||||||
CanvasEntityIdentifier,
|
CanvasEntityIdentifier,
|
||||||
|
CanvasEntityState,
|
||||||
CanvasInpaintMaskState,
|
CanvasInpaintMaskState,
|
||||||
CanvasRasterLayerState,
|
CanvasRasterLayerState,
|
||||||
CanvasRegionalGuidanceState,
|
CanvasRegionalGuidanceState,
|
||||||
@ -163,6 +164,22 @@ export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIde
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectAllEntitiesOfType(state: CanvasV2State, type: CanvasEntityState['type']): CanvasEntityState[] {
|
||||||
|
if (type === 'raster_layer') {
|
||||||
|
return state.rasterLayers.entities;
|
||||||
|
} else if (type === 'control_layer') {
|
||||||
|
return state.controlLayers.entities;
|
||||||
|
} else if (type === 'inpaint_mask') {
|
||||||
|
return [state.inpaintMask];
|
||||||
|
} else if (type === 'regional_guidance') {
|
||||||
|
return state.regions.entities;
|
||||||
|
} else if (type === 'ip_adapter') {
|
||||||
|
return state.ipAdapters.entities;
|
||||||
|
} else {
|
||||||
|
assert(false, 'Not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const invalidateRasterizationCaches = (
|
const invalidateRasterizationCaches = (
|
||||||
entity: CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState,
|
entity: CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState,
|
||||||
state: CanvasV2State
|
state: CanvasV2State
|
||||||
@ -410,6 +427,48 @@ export const canvasV2Slice = createSlice({
|
|||||||
moveToStart(state.regions.entities, entity);
|
moveToStart(state.regions.entities, entity);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
entityOpacityChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ opacity: number }>>) => {
|
||||||
|
const { entityIdentifier, opacity } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entity.type === 'ip_adapter') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.opacity = opacity;
|
||||||
|
},
|
||||||
|
allEntitiesOfTypeToggled: (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;
|
||||||
|
break;
|
||||||
|
case 'control_layer':
|
||||||
|
entities = state.controlLayers.entities;
|
||||||
|
break;
|
||||||
|
case 'inpaint_mask':
|
||||||
|
entities = [state.inpaintMask];
|
||||||
|
break;
|
||||||
|
case 'regional_guidance':
|
||||||
|
entities = state.regions.entities;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert(false, 'Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
const allEnabled = entities.every((entity) => entity.isEnabled);
|
||||||
|
for (const entity of entities) {
|
||||||
|
entity.isEnabled = !allEnabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
allEntitiesDeleted: (state) => {
|
allEntitiesDeleted: (state) => {
|
||||||
state.ipAdapters = deepClone(initialState.ipAdapters);
|
state.ipAdapters = deepClone(initialState.ipAdapters);
|
||||||
state.rasterLayers = deepClone(initialState.rasterLayers);
|
state.rasterLayers = deepClone(initialState.rasterLayers);
|
||||||
@ -490,6 +549,8 @@ export const {
|
|||||||
entityArrangedToFront,
|
entityArrangedToFront,
|
||||||
entityArrangedBackwardOne,
|
entityArrangedBackwardOne,
|
||||||
entityArrangedToBack,
|
entityArrangedToBack,
|
||||||
|
entityOpacityChanged,
|
||||||
|
allEntitiesOfTypeToggled,
|
||||||
// bbox
|
// bbox
|
||||||
bboxChanged,
|
bboxChanged,
|
||||||
bboxScaledSizeChanged,
|
bboxScaledSizeChanged,
|
||||||
|
@ -644,7 +644,7 @@ const zMaskObject = z
|
|||||||
const zFillStyle = z.enum(['solid', 'grid', 'crosshatch', 'diagonal', 'horizontal', 'vertical']);
|
const zFillStyle = z.enum(['solid', 'grid', 'crosshatch', 'diagonal', 'horizontal', 'vertical']);
|
||||||
export type FillStyle = z.infer<typeof zFillStyle>;
|
export type FillStyle = z.infer<typeof zFillStyle>;
|
||||||
export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success;
|
export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success;
|
||||||
const zFill = z.object({ style: zFillStyle, color: zRgbaColor });
|
const zFill = z.object({ style: zFillStyle, color: zRgbColor });
|
||||||
export type Fill = z.infer<typeof zFill>;
|
export type Fill = z.infer<typeof zFill>;
|
||||||
|
|
||||||
const zImageCache = z.object({
|
const zImageCache = z.object({
|
||||||
@ -670,6 +670,7 @@ export const zCanvasRegionalGuidanceState = z.object({
|
|||||||
type: z.literal('regional_guidance'),
|
type: z.literal('regional_guidance'),
|
||||||
isEnabled: z.boolean(),
|
isEnabled: z.boolean(),
|
||||||
position: zCoordinate,
|
position: zCoordinate,
|
||||||
|
opacity: zOpacity,
|
||||||
objects: z.array(zCanvasObjectState),
|
objects: z.array(zCanvasObjectState),
|
||||||
fill: zFill,
|
fill: zFill,
|
||||||
positivePrompt: zParameterPositivePrompt.nullable(),
|
positivePrompt: zParameterPositivePrompt.nullable(),
|
||||||
@ -686,6 +687,7 @@ const zCanvasInpaintMaskState = z.object({
|
|||||||
isEnabled: z.boolean(),
|
isEnabled: z.boolean(),
|
||||||
position: zCoordinate,
|
position: zCoordinate,
|
||||||
fill: zFill,
|
fill: zFill,
|
||||||
|
opacity: zOpacity,
|
||||||
objects: z.array(zCanvasObjectState),
|
objects: z.array(zCanvasObjectState),
|
||||||
rasterizationCache: z.array(zImageCache),
|
rasterizationCache: z.array(zImageCache),
|
||||||
});
|
});
|
||||||
@ -946,20 +948,16 @@ export type PositionChangedArg = { id: string; position: Coordinate };
|
|||||||
export type ScaleChangedArg = { id: string; scale: Coordinate; position: Coordinate };
|
export type ScaleChangedArg = { id: string; scale: Coordinate; position: Coordinate };
|
||||||
export type BboxChangedArg = { id: string; bbox: Rect | null };
|
export type BboxChangedArg = { id: string; bbox: Rect | null };
|
||||||
|
|
||||||
export type EntityIdentifierPayload = { entityIdentifier: CanvasEntityIdentifier };
|
export type EntityIdentifierPayload<T = object> = { entityIdentifier: CanvasEntityIdentifier } & T;
|
||||||
export type EntityMovedPayload = { entityIdentifier: CanvasEntityIdentifier; position: Coordinate };
|
export type EntityMovedPayload = EntityIdentifierPayload<{ position: Coordinate }>;
|
||||||
export type EntityBrushLineAddedPayload = { entityIdentifier: CanvasEntityIdentifier; brushLine: CanvasBrushLineState };
|
export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{ brushLine: CanvasBrushLineState }>;
|
||||||
export type EntityEraserLineAddedPayload = {
|
export type EntityEraserLineAddedPayload = EntityIdentifierPayload<{ eraserLine: CanvasEraserLineState }>;
|
||||||
entityIdentifier: CanvasEntityIdentifier;
|
export type EntityRectAddedPayload = EntityIdentifierPayload<{ rect: CanvasRectState }>;
|
||||||
eraserLine: CanvasEraserLineState;
|
export type EntityRasterizedPayload = EntityIdentifierPayload<{
|
||||||
};
|
|
||||||
export type EntityRectAddedPayload = { entityIdentifier: CanvasEntityIdentifier; rect: CanvasRectState };
|
|
||||||
export type EntityRasterizedPayload = {
|
|
||||||
entityIdentifier: CanvasEntityIdentifier;
|
|
||||||
imageObject: CanvasImageState;
|
imageObject: CanvasImageState;
|
||||||
rect: Rect;
|
rect: Rect;
|
||||||
replaceObjects: boolean;
|
replaceObjects: boolean;
|
||||||
};
|
}>;
|
||||||
export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate };
|
export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate };
|
||||||
|
|
||||||
//#region Type guards
|
//#region Type guards
|
||||||
@ -1000,3 +998,7 @@ export function isDrawableEntityAdapter(
|
|||||||
): adapter is CanvasLayerAdapter | CanvasMaskAdapter {
|
): adapter is CanvasLayerAdapter | CanvasMaskAdapter {
|
||||||
return adapter instanceof CanvasLayerAdapter || adapter instanceof CanvasMaskAdapter;
|
return adapter instanceof CanvasLayerAdapter || adapter instanceof CanvasMaskAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getEntityIdentifier = (entity: CanvasEntityState): CanvasEntityIdentifier => {
|
||||||
|
return { id: entity.id, type: entity.type };
|
||||||
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||||
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
import { Box, Flex, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
|
import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton';
|
||||||
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
||||||
import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice';
|
import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { selectEntityCount } from 'features/controlLayers/store/selectors';
|
import { selectEntityCount } from 'features/controlLayers/store/selectors';
|
||||||
@ -89,7 +90,7 @@ const ParametersPanelTextToImage = () => {
|
|||||||
gap={2}
|
gap={2}
|
||||||
onChange={onChangeTabs}
|
onChange={onChangeTabs}
|
||||||
>
|
>
|
||||||
<TabList gap={2} fontSize="sm" borderColor="base.800">
|
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full">
|
||||||
<Tab sx={baseStyles} _selected={selectedStyles} data-testid="generation-tab-settings-tab-button">
|
<Tab sx={baseStyles} _selected={selectedStyles} data-testid="generation-tab-settings-tab-button">
|
||||||
{t('common.settingsLabel')}
|
{t('common.settingsLabel')}
|
||||||
</Tab>
|
</Tab>
|
||||||
@ -100,6 +101,8 @@ const ParametersPanelTextToImage = () => {
|
|||||||
>
|
>
|
||||||
{controlLayersTitle}
|
{controlLayersTitle}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Spacer />
|
||||||
|
<AddLayerButton />
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels w="full" h="full">
|
<TabPanels w="full" h="full">
|
||||||
<TabPanel p={0} w="full" h="full">
|
<TabPanel p={0} w="full" h="full">
|
||||||
|
Loading…
Reference in New Issue
Block a user