mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): raster layer logic
- Deduplicate shared logic - Split up giant renderers file into separate cohesive files - Tons of cleanup - Progress on raster layer functionality
This commit is contained in:
parent
d79fafc5f5
commit
d540e2c0d3
@ -10,8 +10,8 @@ import {
|
|||||||
caLayerProcessorConfigChanged,
|
caLayerProcessorConfigChanged,
|
||||||
caLayerProcessorPendingBatchIdChanged,
|
caLayerProcessorPendingBatchIdChanged,
|
||||||
caLayerRecalled,
|
caLayerRecalled,
|
||||||
isControlAdapterLayer,
|
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isControlAdapterLayer } from 'features/controlLayers/store/types';
|
||||||
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
|
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
@ -8,13 +8,13 @@ import {
|
|||||||
selectControlAdapterAll,
|
selectControlAdapterAll,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||||
|
import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import {
|
import {
|
||||||
isControlAdapterLayer,
|
isControlAdapterLayer,
|
||||||
isInitialImageLayer,
|
isInitialImageLayer,
|
||||||
isIPAdapterLayer,
|
isIPAdapterLayer,
|
||||||
isRegionalGuidanceLayer,
|
isRegionalGuidanceLayer,
|
||||||
layerDeleted,
|
} from 'features/controlLayers/store/types';
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||||
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
|
@ -3,11 +3,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
|
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
import {
|
import {
|
||||||
isRegionalGuidanceLayer,
|
|
||||||
rgLayerNegativePromptChanged,
|
rgLayerNegativePromptChanged,
|
||||||
rgLayerPositivePromptChanged,
|
rgLayerPositivePromptChanged,
|
||||||
selectControlLayersSlice,
|
selectControlLayersSlice,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
@ -6,7 +6,8 @@ import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMe
|
|||||||
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
||||||
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
||||||
import { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
|
import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isControlAdapterLayer } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
import CALayerOpacity from './CALayerOpacity';
|
import CALayerOpacity from './CALayerOpacity';
|
||||||
@ -17,7 +18,9 @@ type Props = {
|
|||||||
|
|
||||||
export const CALayer = memo(({ layerId }: Props) => {
|
export const CALayer = memo(({ layerId }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected);
|
const isSelected = useAppSelector(
|
||||||
|
(s) => selectLayerOrThrow(s.controlLayers.present, layerId, isControlAdapterLayer).isSelected
|
||||||
|
);
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(layerSelected(layerId));
|
dispatch(layerSelected(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
|
@ -8,8 +8,9 @@ import {
|
|||||||
caLayerProcessorConfigChanged,
|
caLayerProcessorConfigChanged,
|
||||||
caOrIPALayerBeginEndStepPctChanged,
|
caOrIPALayerBeginEndStepPctChanged,
|
||||||
caOrIPALayerWeightChanged,
|
caOrIPALayerWeightChanged,
|
||||||
selectCALayerOrThrow,
|
selectLayerOrThrow,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isControlAdapterLayer } from 'features/controlLayers/store/types';
|
||||||
import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
|
import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
|
||||||
import type { CALayerImageDropData } from 'features/dnd/types';
|
import type { CALayerImageDropData } from 'features/dnd/types';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
@ -26,7 +27,9 @@ type Props = {
|
|||||||
|
|
||||||
export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
|
export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const controlAdapter = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).controlAdapter);
|
const controlAdapter = useAppSelector(
|
||||||
|
(s) => selectLayerOrThrow(s.controlLayers.present, layerId, isControlAdapterLayer).controlAdapter
|
||||||
|
);
|
||||||
|
|
||||||
const onChangeBeginEndStepPct = useCallback(
|
const onChangeBeginEndStepPct = useCallback(
|
||||||
(beginEndStepPct: [number, number]) => {
|
(beginEndStepPct: [number, number]) => {
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { stopPropagation } from 'common/util/stopPropagation';
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { useCALayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
|
import { useCALayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
|
||||||
import { caLayerIsFilterEnabledChanged, caLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice';
|
import { caLayerIsFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -34,7 +34,7 @@ const CALayerOpacity = ({ layerId }: Props) => {
|
|||||||
const { opacity, isFilterEnabled } = useCALayerOpacity(layerId);
|
const { opacity, isFilterEnabled } = useCALayerOpacity(layerId);
|
||||||
const onChangeOpacity = useCallback(
|
const onChangeOpacity = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(caLayerOpacityChanged({ layerId, opacity: v / 100 }));
|
dispatch(layerOpacityChanged({ layerId, opacity: v / 100 }));
|
||||||
},
|
},
|
||||||
[dispatch, layerId]
|
[dispatch, layerId]
|
||||||
);
|
);
|
||||||
|
@ -11,8 +11,9 @@ import { IILayer } from 'features/controlLayers/components/IILayer/IILayer';
|
|||||||
import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer';
|
import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer';
|
||||||
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
|
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
|
||||||
import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer';
|
import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer';
|
||||||
import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type { Layer } from 'features/controlLayers/store/types';
|
import type { Layer } from 'features/controlLayers/store/types';
|
||||||
|
import { isRenderableLayer } from 'features/controlLayers/store/types';
|
||||||
import { partition } from 'lodash-es';
|
import { partition } from 'lodash-es';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IILayerOpacity from 'features/controlLayers/components/IILayer/IILayerOpacity';
|
|
||||||
import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview';
|
import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview';
|
||||||
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
||||||
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
|
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
|
||||||
|
import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity';
|
||||||
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
||||||
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
||||||
@ -11,8 +11,9 @@ import {
|
|||||||
iiLayerDenoisingStrengthChanged,
|
iiLayerDenoisingStrengthChanged,
|
||||||
iiLayerImageChanged,
|
iiLayerImageChanged,
|
||||||
layerSelected,
|
layerSelected,
|
||||||
selectIILayerOrThrow,
|
selectLayerOrThrow,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isInitialImageLayer } from 'features/controlLayers/store/types';
|
||||||
import type { IILayerImageDropData } from 'features/dnd/types';
|
import type { IILayerImageDropData } from 'features/dnd/types';
|
||||||
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
|
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
@ -24,7 +25,7 @@ type Props = {
|
|||||||
|
|
||||||
export const IILayer = memo(({ layerId }: Props) => {
|
export const IILayer = memo(({ layerId }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const layer = useAppSelector((s) => selectIILayerOrThrow(s.controlLayers.present, layerId));
|
const layer = useAppSelector((s) => selectLayerOrThrow(s.controlLayers.present, layerId, isInitialImageLayer));
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(layerSelected(layerId));
|
dispatch(layerSelected(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
@ -69,7 +70,7 @@ export const IILayer = memo(({ layerId }: Props) => {
|
|||||||
<LayerIsEnabledToggle layerId={layerId} />
|
<LayerIsEnabledToggle layerId={layerId} />
|
||||||
<LayerTitle type="initial_image_layer" />
|
<LayerTitle type="initial_image_layer" />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<IILayerOpacity layerId={layerId} />
|
<LayerOpacity layerId={layerId} />
|
||||||
<LayerMenu layerId={layerId} />
|
<LayerMenu layerId={layerId} />
|
||||||
<LayerDeleteButton layerId={layerId} />
|
<LayerDeleteButton layerId={layerId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -5,7 +5,8 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon
|
|||||||
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
||||||
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
||||||
import { layerSelected, selectIPALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
|
import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isIPAdapterLayer } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -14,7 +15,9 @@ type Props = {
|
|||||||
|
|
||||||
export const IPALayer = memo(({ layerId }: Props) => {
|
export const IPALayer = memo(({ layerId }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isSelected = useAppSelector((s) => selectIPALayerOrThrow(s.controlLayers.present, layerId).isSelected);
|
const isSelected = useAppSelector(
|
||||||
|
(s) => selectLayerOrThrow(s.controlLayers.present, layerId, isIPAdapterLayer).isSelected
|
||||||
|
);
|
||||||
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(layerSelected(layerId));
|
dispatch(layerSelected(layerId));
|
||||||
|
@ -7,8 +7,9 @@ import {
|
|||||||
ipaLayerImageChanged,
|
ipaLayerImageChanged,
|
||||||
ipaLayerMethodChanged,
|
ipaLayerMethodChanged,
|
||||||
ipaLayerModelChanged,
|
ipaLayerModelChanged,
|
||||||
selectIPALayerOrThrow,
|
selectLayerOrThrow,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isIPAdapterLayer } from 'features/controlLayers/store/types';
|
||||||
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
|
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
|
||||||
import type { IPALayerImageDropData } from 'features/dnd/types';
|
import type { IPALayerImageDropData } from 'features/dnd/types';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
@ -20,7 +21,9 @@ type Props = {
|
|||||||
|
|
||||||
export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => {
|
export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const ipAdapter = useAppSelector((s) => selectIPALayerOrThrow(s.controlLayers.present, layerId).ipAdapter);
|
const ipAdapter = useAppSelector(
|
||||||
|
(s) => selectLayerOrThrow(s.controlLayers.present, layerId, isIPAdapterLayer).ipAdapter
|
||||||
|
);
|
||||||
|
|
||||||
const onChangeBeginEndStepPct = useCallback(
|
const onChangeBeginEndStepPct = useCallback(
|
||||||
(beginEndStepPct: [number, number]) => {
|
(beginEndStepPct: [number, number]) => {
|
||||||
|
@ -2,13 +2,13 @@ import { MenuItem } from '@invoke-ai/ui-library';
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import {
|
||||||
isRenderableLayer,
|
|
||||||
layerMovedBackward,
|
layerMovedBackward,
|
||||||
layerMovedForward,
|
layerMovedForward,
|
||||||
layerMovedToBack,
|
layerMovedToBack,
|
||||||
layerMovedToFront,
|
layerMovedToFront,
|
||||||
selectControlLayersSlice,
|
selectControlLayersSlice,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isRenderableLayer } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiArrowDownBold, PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold } from 'react-icons/pi';
|
import { PiArrowDownBold, PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold } from 'react-icons/pi';
|
||||||
|
@ -3,11 +3,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
|
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
import {
|
import {
|
||||||
isRegionalGuidanceLayer,
|
|
||||||
rgLayerNegativePromptChanged,
|
rgLayerNegativePromptChanged,
|
||||||
rgLayerPositivePromptChanged,
|
rgLayerPositivePromptChanged,
|
||||||
selectControlLayersSlice,
|
selectControlLayersSlice,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
@ -15,14 +15,14 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { stopPropagation } from 'common/util/stopPropagation';
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import {
|
import {
|
||||||
iiLayerOpacityChanged,
|
layerOpacityChanged,
|
||||||
isInitialImageLayer,
|
|
||||||
selectControlLayersSlice,
|
selectControlLayersSlice,
|
||||||
|
selectLayerOrThrow,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isLayerWithOpacity } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiDropHalfFill } from 'react-icons/pi';
|
import { PiDropHalfFill } from 'react-icons/pi';
|
||||||
import { assert } from 'tsafe';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
@ -31,14 +31,13 @@ type Props = {
|
|||||||
const marks = [0, 25, 50, 75, 100];
|
const marks = [0, 25, 50, 75, 100];
|
||||||
const formatPct = (v: number | string) => `${v} %`;
|
const formatPct = (v: number | string) => `${v} %`;
|
||||||
|
|
||||||
const IILayerOpacity = ({ layerId }: Props) => {
|
export const LayerOpacity = memo(({ layerId }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectOpacity = useMemo(
|
const selectOpacity = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createSelector(selectControlLayersSlice, (controlLayers) => {
|
createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
const layer = controlLayers.present.layers.filter(isInitialImageLayer).find((l) => l.id === layerId);
|
const layer = selectLayerOrThrow(controlLayers.present, layerId, isLayerWithOpacity);
|
||||||
assert(layer, `Layer ${layerId} not found`);
|
|
||||||
return Math.round(layer.opacity * 100);
|
return Math.round(layer.opacity * 100);
|
||||||
}),
|
}),
|
||||||
[layerId]
|
[layerId]
|
||||||
@ -46,7 +45,7 @@ const IILayerOpacity = ({ layerId }: Props) => {
|
|||||||
const opacity = useAppSelector(selectOpacity);
|
const opacity = useAppSelector(selectOpacity);
|
||||||
const onChangeOpacity = useCallback(
|
const onChangeOpacity = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(iiLayerOpacityChanged({ layerId, opacity: v / 100 }));
|
dispatch(layerOpacityChanged({ layerId, opacity: v / 100 }));
|
||||||
},
|
},
|
||||||
[dispatch, layerId]
|
[dispatch, layerId]
|
||||||
);
|
);
|
||||||
@ -93,6 +92,6 @@ const IILayerOpacity = ({ layerId }: Props) => {
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default memo(IILayerOpacity);
|
LayerOpacity.displayName = 'LayerOpacity';
|
@ -8,11 +8,8 @@ import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMe
|
|||||||
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
||||||
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
||||||
import {
|
import { layerSelected, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
isRegionalGuidanceLayer,
|
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
layerSelected,
|
|
||||||
selectControlLayersSlice,
|
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import { rgLayerAutoNegativeChanged, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
isRegionalGuidanceLayer,
|
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
rgLayerAutoNegativeChanged,
|
|
||||||
selectControlLayersSlice,
|
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -4,11 +4,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import RgbColorPicker from 'common/components/RgbColorPicker';
|
import RgbColorPicker from 'common/components/RgbColorPicker';
|
||||||
import { stopPropagation } from 'common/util/stopPropagation';
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
import {
|
import { rgLayerPreviewColorChanged, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
isRegionalGuidanceLayer,
|
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
rgLayerPreviewColorChanged,
|
|
||||||
selectControlLayersSlice,
|
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { RgbColor } from 'react-colorful';
|
import type { RgbColor } from 'react-colorful';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -2,7 +2,8 @@ import { Divider, Flex } from '@invoke-ai/ui-library';
|
|||||||
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 { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper';
|
import { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper';
|
||||||
import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
@ -2,21 +2,23 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
||||||
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
|
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
|
||||||
|
import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity';
|
||||||
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
||||||
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
||||||
import { layerSelected, selectRasterLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
|
import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isRasterLayer } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
import { RasterLayerOpacity } from './RasterLayerOpacity';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RasterLayer = memo(({ layerId }: Props) => {
|
export const RasterLayer = memo(({ layerId }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isSelected = useAppSelector((s) => selectRasterLayerOrThrow(s.controlLayers.present, layerId).isSelected);
|
const isSelected = useAppSelector(
|
||||||
|
(s) => selectLayerOrThrow(s.controlLayers.present, layerId, isRasterLayer).isSelected
|
||||||
|
);
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(layerSelected(layerId));
|
dispatch(layerSelected(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
@ -28,7 +30,7 @@ export const RasterLayer = memo(({ layerId }: Props) => {
|
|||||||
<LayerIsEnabledToggle layerId={layerId} />
|
<LayerIsEnabledToggle layerId={layerId} />
|
||||||
<LayerTitle type="raster_layer" />
|
<LayerTitle type="raster_layer" />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<RasterLayerOpacity layerId={layerId} />
|
<LayerOpacity layerId={layerId} />
|
||||||
<LayerMenu layerId={layerId} />
|
<LayerMenu layerId={layerId} />
|
||||||
<LayerDeleteButton layerId={layerId} />
|
<LayerDeleteButton layerId={layerId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
import {
|
|
||||||
CompositeNumberInput,
|
|
||||||
CompositeSlider,
|
|
||||||
Flex,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
IconButton,
|
|
||||||
Popover,
|
|
||||||
PopoverArrow,
|
|
||||||
PopoverBody,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from '@invoke-ai/ui-library';
|
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import { stopPropagation } from 'common/util/stopPropagation';
|
|
||||||
import { useRasterLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
|
|
||||||
import { rasterLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiDropHalfFill } from 'react-icons/pi';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
layerId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const marks = [0, 25, 50, 75, 100];
|
|
||||||
const formatPct = (v: number | string) => `${v} %`;
|
|
||||||
|
|
||||||
export const RasterLayerOpacity = memo(({ layerId }: Props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const opacity = useRasterLayerOpacity(layerId);
|
|
||||||
const onChangeOpacity = useCallback(
|
|
||||||
(v: number) => {
|
|
||||||
dispatch(rasterLayerOpacityChanged({ layerId, opacity: v / 100 }));
|
|
||||||
},
|
|
||||||
[dispatch, layerId]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Popover isLazy>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<IconButton
|
|
||||||
aria-label={t('controlLayers.opacity')}
|
|
||||||
size="sm"
|
|
||||||
icon={<PiDropHalfFill size={16} />}
|
|
||||||
variant="ghost"
|
|
||||||
onDoubleClick={stopPropagation}
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent onDoubleClick={stopPropagation}>
|
|
||||||
<PopoverArrow />
|
|
||||||
<PopoverBody>
|
|
||||||
<Flex direction="column" gap={2}>
|
|
||||||
<FormControl orientation="horizontal">
|
|
||||||
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
|
|
||||||
<CompositeSlider
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={opacity}
|
|
||||||
defaultValue={100}
|
|
||||||
onChange={onChangeOpacity}
|
|
||||||
marks={marks}
|
|
||||||
w={48}
|
|
||||||
/>
|
|
||||||
<CompositeNumberInput
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={opacity}
|
|
||||||
defaultValue={100}
|
|
||||||
onChange={onChangeOpacity}
|
|
||||||
w={24}
|
|
||||||
format={formatPct}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</Flex>
|
|
||||||
</PopoverBody>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
RasterLayerOpacity.displayName = 'RasterLayerOpacity';
|
|
@ -6,7 +6,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { BRUSH_SPACING_PCT, MAX_BRUSH_SPACING_PX, MIN_BRUSH_SPACING_PX } from 'features/controlLayers/konva/constants';
|
import { BRUSH_SPACING_PCT, MAX_BRUSH_SPACING_PX, MIN_BRUSH_SPACING_PX } from 'features/controlLayers/konva/constants';
|
||||||
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
||||||
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers';
|
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers';
|
||||||
import {
|
import {
|
||||||
$brushColor,
|
$brushColor,
|
||||||
$brushSize,
|
$brushSize,
|
||||||
@ -21,7 +21,6 @@ import {
|
|||||||
brushLineAdded,
|
brushLineAdded,
|
||||||
brushSizeChanged,
|
brushSizeChanged,
|
||||||
eraserLineAdded,
|
eraserLineAdded,
|
||||||
isRegionalGuidanceLayer,
|
|
||||||
layerBboxChanged,
|
layerBboxChanged,
|
||||||
layerTranslated,
|
layerTranslated,
|
||||||
linePointsAdded,
|
linePointsAdded,
|
||||||
@ -34,6 +33,7 @@ import type {
|
|||||||
AddPointToLineArg,
|
AddPointToLineArg,
|
||||||
AddRectShapeArg,
|
AddRectShapeArg,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
|
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
|
@ -3,9 +3,9 @@ import {
|
|||||||
caLayerAdded,
|
caLayerAdded,
|
||||||
iiLayerAdded,
|
iiLayerAdded,
|
||||||
ipaLayerAdded,
|
ipaLayerAdded,
|
||||||
isInitialImageLayer,
|
|
||||||
rgLayerIPAdapterAdded,
|
rgLayerIPAdapterAdded,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { isInitialImageLayer } from 'features/controlLayers/store/types';
|
||||||
import {
|
import {
|
||||||
buildControlNet,
|
buildControlNet,
|
||||||
buildIPAdapter,
|
buildIPAdapter,
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
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 {
|
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
isControlAdapterLayer,
|
import { isControlAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
isRasterLayer,
|
|
||||||
isRegionalGuidanceLayer,
|
|
||||||
selectControlLayersSlice,
|
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
@ -81,17 +77,3 @@ export const useCALayerOpacity = (layerId: string) => {
|
|||||||
const opacity = useAppSelector(selectLayer);
|
const opacity = useAppSelector(selectLayer);
|
||||||
return opacity;
|
return opacity;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRasterLayerOpacity = (layerId: string) => {
|
|
||||||
const selectLayer = useMemo(
|
|
||||||
() =>
|
|
||||||
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
|
||||||
const layer = controlLayers.present.layers.filter(isRasterLayer).find((l) => l.id === layerId);
|
|
||||||
assert(layer, `Layer ${layerId} not found`);
|
|
||||||
return Math.round(layer.opacity * 100);
|
|
||||||
}),
|
|
||||||
[layerId]
|
|
||||||
);
|
|
||||||
const opacity = useAppSelector(selectLayer);
|
|
||||||
return opacity;
|
|
||||||
};
|
|
||||||
|
@ -34,3 +34,8 @@ export const MIN_BRUSH_SPACING_PX = 5;
|
|||||||
* The maximum brush spacing in pixels.
|
* The maximum brush spacing in pixels.
|
||||||
*/
|
*/
|
||||||
export const MAX_BRUSH_SPACING_PX = 15;
|
export const MAX_BRUSH_SPACING_PX = 15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The debounce time in milliseconds for debounced renderers.
|
||||||
|
*/
|
||||||
|
export const DEBOUNCE_MS = 300;
|
||||||
|
@ -67,6 +67,7 @@ export const setStageEventHandlers = ({
|
|||||||
onRectShapeAdded,
|
onRectShapeAdded,
|
||||||
onBrushSizeChanged,
|
onBrushSizeChanged,
|
||||||
}: SetStageEventHandlersArg): (() => void) => {
|
}: SetStageEventHandlersArg): (() => void) => {
|
||||||
|
//#region mouseenter
|
||||||
stage.on('mouseenter', (e) => {
|
stage.on('mouseenter', (e) => {
|
||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (!stage) {
|
if (!stage) {
|
||||||
@ -76,6 +77,7 @@ export const setStageEventHandlers = ({
|
|||||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//#region mousedown
|
||||||
stage.on('mousedown', (e) => {
|
stage.on('mousedown', (e) => {
|
||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (!stage) {
|
if (!stage) {
|
||||||
@ -110,6 +112,7 @@ export const setStageEventHandlers = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//#region mouseup
|
||||||
stage.on('mouseup', (e) => {
|
stage.on('mouseup', (e) => {
|
||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (!stage) {
|
if (!stage) {
|
||||||
@ -143,6 +146,7 @@ export const setStageEventHandlers = ({
|
|||||||
$lastMouseDownPos.set(null);
|
$lastMouseDownPos.set(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//#region mousemove
|
||||||
stage.on('mousemove', (e) => {
|
stage.on('mousemove', (e) => {
|
||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (!stage) {
|
if (!stage) {
|
||||||
@ -191,6 +195,7 @@ export const setStageEventHandlers = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//#region mouseleave
|
||||||
stage.on('mouseleave', (e) => {
|
stage.on('mouseleave', (e) => {
|
||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (!stage) {
|
if (!stage) {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,67 @@
|
|||||||
|
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
||||||
|
import { BACKGROUND_LAYER_ID, BACKGROUND_RECT_ID } from 'features/controlLayers/konva/naming';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stage background is a semi-transparent checkerboard pattern. We use konva's `fillPatternImage` to apply the
|
||||||
|
* a data URL of the pattern image to the background rect. Some scaling and positioning is required to ensure the
|
||||||
|
* everything lines up correctly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the background layer for the stage.
|
||||||
|
* @param stage The konva stage
|
||||||
|
*/
|
||||||
|
const createBackgroundLayer = (stage: Konva.Stage): Konva.Layer => {
|
||||||
|
const layer = new Konva.Layer({
|
||||||
|
id: BACKGROUND_LAYER_ID,
|
||||||
|
});
|
||||||
|
const background = new Konva.Rect({
|
||||||
|
id: BACKGROUND_RECT_ID,
|
||||||
|
x: stage.x(),
|
||||||
|
y: 0,
|
||||||
|
width: stage.width() / stage.scaleX(),
|
||||||
|
height: stage.height() / stage.scaleY(),
|
||||||
|
listening: false,
|
||||||
|
opacity: 0.2,
|
||||||
|
});
|
||||||
|
layer.add(background);
|
||||||
|
stage.add(layer);
|
||||||
|
const image = new Image();
|
||||||
|
image.onload = () => {
|
||||||
|
background.fillPatternImage(image);
|
||||||
|
};
|
||||||
|
image.src = TRANSPARENCY_CHECKER_PATTERN;
|
||||||
|
return layer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the background layer for the stage.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param width The unscaled width of the canvas
|
||||||
|
* @param height The unscaled height of the canvas
|
||||||
|
*/
|
||||||
|
export const renderBackground = (stage: Konva.Stage, width: number, height: number): void => {
|
||||||
|
const layer = stage.findOne<Konva.Layer>(`#${BACKGROUND_LAYER_ID}`) ?? createBackgroundLayer(stage);
|
||||||
|
|
||||||
|
const background = layer.findOne<Konva.Rect>(`#${BACKGROUND_RECT_ID}`);
|
||||||
|
assert(background, 'Background rect not found');
|
||||||
|
// ensure background rect is in the top-left of the canvas
|
||||||
|
background.absolutePosition({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// set the dimensions of the background rect to match the canvas - not the stage!!!
|
||||||
|
background.size({
|
||||||
|
width: width / stage.scaleX(),
|
||||||
|
height: height / stage.scaleY(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate the amount the stage is moved - including the effect of scaling
|
||||||
|
const stagePos = {
|
||||||
|
x: -stage.x() / stage.scaleX(),
|
||||||
|
y: -stage.y() / stage.scaleY(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply that movement to the fill pattern
|
||||||
|
background.fillPatternOffset(stagePos);
|
||||||
|
};
|
@ -1,10 +1,17 @@
|
|||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||||
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||||
|
import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants';
|
||||||
|
import { getLayerBboxId, LAYER_BBOX_NAME, RG_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/konva/naming';
|
||||||
|
import type { Layer, Tool } from 'features/controlLayers/store/types';
|
||||||
|
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
import { RG_LAYER_OBJECT_GROUP_NAME } from './naming';
|
/**
|
||||||
|
* Logic to create and render bounding boxes for layers.
|
||||||
|
* Some utils are included for calculating bounding boxes.
|
||||||
|
*/
|
||||||
|
|
||||||
type Extents = {
|
type Extents = {
|
||||||
minX: number;
|
minX: number;
|
||||||
@ -15,7 +22,6 @@ type Extents = {
|
|||||||
|
|
||||||
const GET_CLIENT_RECT_CONFIG = { skipTransform: true };
|
const GET_CLIENT_RECT_CONFIG = { skipTransform: true };
|
||||||
|
|
||||||
//#region getImageDataBbox
|
|
||||||
/**
|
/**
|
||||||
* Get the bounding box of an image.
|
* Get the bounding box of an image.
|
||||||
* @param imageData The ImageData object to get the bounding box of.
|
* @param imageData The ImageData object to get the bounding box of.
|
||||||
@ -53,9 +59,7 @@ const getImageDataBbox = (imageData: ImageData): Extents | null => {
|
|||||||
|
|
||||||
return isEmpty ? null : { minX, minY, maxX, maxY };
|
return isEmpty ? null : { minX, minY, maxX, maxY };
|
||||||
};
|
};
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region getIsolatedRGLayerClone
|
|
||||||
/**
|
/**
|
||||||
* Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer
|
* Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer
|
||||||
* to be captured, manipulated or analyzed without interference from other layers.
|
* to be captured, manipulated or analyzed without interference from other layers.
|
||||||
@ -92,15 +96,13 @@ const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage;
|
|||||||
|
|
||||||
return { stageClone, layerClone };
|
return { stageClone, layerClone };
|
||||||
};
|
};
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region getLayerBboxPixels
|
|
||||||
/**
|
/**
|
||||||
* Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers.
|
* Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers.
|
||||||
* @param layer The konva layer to get the bounding box of.
|
* @param layer The konva layer to get the bounding box of.
|
||||||
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
|
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
|
||||||
*/
|
*/
|
||||||
export const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect | null => {
|
const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect | null => {
|
||||||
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
|
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
|
||||||
//
|
//
|
||||||
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
|
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
|
||||||
@ -143,9 +145,7 @@ export const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false)
|
|||||||
|
|
||||||
return correctedLayerBbox;
|
return correctedLayerBbox;
|
||||||
};
|
};
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region getLayerBboxFast
|
|
||||||
/**
|
/**
|
||||||
* Get the bounding box of a konva layer. This function is faster than `getLayerBboxPixels` but less accurate. It
|
* Get the bounding box of a konva layer. This function is faster than `getLayerBboxPixels` but less accurate. It
|
||||||
* should only be used when there are no eraser strokes or shapes in the layer.
|
* should only be used when there are no eraser strokes or shapes in the layer.
|
||||||
@ -161,4 +161,94 @@ export const getLayerBboxFast = (layer: Konva.Layer): IRect => {
|
|||||||
height: Math.floor(bbox.height),
|
height: Math.floor(bbox.height),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
//#endregion
|
|
||||||
|
/**
|
||||||
|
* Creates a bounding box rect for a layer.
|
||||||
|
* @param layerState The layer state for the layer to create the bounding box for
|
||||||
|
* @param konvaLayer The konva layer to attach the bounding box to
|
||||||
|
*/
|
||||||
|
const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect => {
|
||||||
|
const rect = new Konva.Rect({
|
||||||
|
id: getLayerBboxId(layerState.id),
|
||||||
|
name: LAYER_BBOX_NAME,
|
||||||
|
strokeWidth: 1,
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
konvaLayer.add(rect);
|
||||||
|
return rect;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerStates An array of layers to calculate bboxes for
|
||||||
|
* @param onBboxChanged Callback for when the bounding box changes
|
||||||
|
*/
|
||||||
|
export const updateBboxes = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
layerStates: Layer[],
|
||||||
|
onBboxChanged: (layerId: string, bbox: IRect | null) => void
|
||||||
|
): void => {
|
||||||
|
for (const rgLayer of layerStates.filter(isRegionalGuidanceLayer)) {
|
||||||
|
const konvaLayer = stage.findOne<Konva.Layer>(`#${rgLayer.id}`);
|
||||||
|
assert(konvaLayer, `Layer ${rgLayer.id} not found in stage`);
|
||||||
|
// We only need to recalculate the bbox if the layer has changed
|
||||||
|
if (rgLayer.bboxNeedsUpdate) {
|
||||||
|
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rgLayer, konvaLayer);
|
||||||
|
|
||||||
|
// Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation
|
||||||
|
const visible = bboxRect.visible();
|
||||||
|
bboxRect.visible(false);
|
||||||
|
|
||||||
|
if (rgLayer.objects.length === 0) {
|
||||||
|
// No objects - no bbox to calculate
|
||||||
|
onBboxChanged(rgLayer.id, null);
|
||||||
|
} else {
|
||||||
|
// Calculate the bbox by rendering the layer and checking its pixels
|
||||||
|
onBboxChanged(rgLayer.id, getLayerBboxPixels(konvaLayer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the visibility of the bbox
|
||||||
|
bboxRect.visible(visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the bounding boxes for the layers.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerStates An array of layers to draw bboxes for
|
||||||
|
* @param tool The current tool
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const renderBboxes = (stage: Konva.Stage, layerStates: Layer[], tool: Tool): void => {
|
||||||
|
// Hide all bboxes so they don't interfere with getClientRect
|
||||||
|
for (const bboxRect of stage.find<Konva.Rect>(`.${LAYER_BBOX_NAME}`)) {
|
||||||
|
bboxRect.visible(false);
|
||||||
|
bboxRect.listening(false);
|
||||||
|
}
|
||||||
|
// No selected layer or not using the move tool - nothing more to do here
|
||||||
|
if (tool !== 'move') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const layer of layerStates.filter(isRegionalGuidanceLayer)) {
|
||||||
|
if (!layer.bbox) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const konvaLayer = stage.findOne<Konva.Layer>(`#${layer.id}`);
|
||||||
|
assert(konvaLayer, `Layer ${layer.id} not found in stage`);
|
||||||
|
|
||||||
|
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layer, konvaLayer);
|
||||||
|
|
||||||
|
bboxRect.setAttrs({
|
||||||
|
visible: !layer.bboxNeedsUpdate,
|
||||||
|
listening: layer.isSelected,
|
||||||
|
x: layer.bbox.x,
|
||||||
|
y: layer.bbox.y,
|
||||||
|
width: layer.bbox.width,
|
||||||
|
height: layer.bbox.height,
|
||||||
|
stroke: layer.isSelected ? BBOX_SELECTED_STROKE : '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,162 @@
|
|||||||
|
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters';
|
||||||
|
import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming';
|
||||||
|
import type { ControlAdapterLayer } from 'features/controlLayers/store/types';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logic for creating and rendering control adapter (control net & t2i adapter) layers. These layers have image objects
|
||||||
|
* and require some special handling to update the source and attributes as control images are swapped or processed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a control adapter layer.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerState The control adapter layer state
|
||||||
|
*/
|
||||||
|
const createCALayer = (stage: Konva.Stage, layerState: ControlAdapterLayer): Konva.Layer => {
|
||||||
|
const konvaLayer = new Konva.Layer({
|
||||||
|
id: layerState.id,
|
||||||
|
name: CA_LAYER_NAME,
|
||||||
|
imageSmoothingEnabled: true,
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
stage.add(konvaLayer);
|
||||||
|
return konvaLayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a control adapter layer image.
|
||||||
|
* @param konvaLayer The konva layer
|
||||||
|
* @param imageEl The image element
|
||||||
|
*/
|
||||||
|
const createCALayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement): Konva.Image => {
|
||||||
|
const konvaImage = new Konva.Image({
|
||||||
|
name: CA_LAYER_IMAGE_NAME,
|
||||||
|
image: imageEl,
|
||||||
|
});
|
||||||
|
konvaLayer.add(konvaImage);
|
||||||
|
return konvaImage;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the image source for a control adapter layer. This includes loading the image from the server and updating
|
||||||
|
* the konva image.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param konvaLayer The konva layer
|
||||||
|
* @param layerState The control adapter layer state
|
||||||
|
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
|
||||||
|
*/
|
||||||
|
const updateCALayerImageSource = async (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
konvaLayer: Konva.Layer,
|
||||||
|
layerState: ControlAdapterLayer,
|
||||||
|
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
|
||||||
|
): Promise<void> => {
|
||||||
|
const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image;
|
||||||
|
if (image) {
|
||||||
|
const imageName = image.name;
|
||||||
|
const imageDTO = await getImageDTO(imageName);
|
||||||
|
if (!imageDTO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const imageEl = new Image();
|
||||||
|
const imageId = getCALayerImageId(layerState.id, imageName);
|
||||||
|
imageEl.onload = () => {
|
||||||
|
// Find the existing image or create a new one - must find using the name, bc the id may have just changed
|
||||||
|
const konvaImage =
|
||||||
|
konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`) ?? createCALayerImage(konvaLayer, imageEl);
|
||||||
|
|
||||||
|
// Update the image's attributes
|
||||||
|
konvaImage.setAttrs({
|
||||||
|
id: imageId,
|
||||||
|
image: imageEl,
|
||||||
|
});
|
||||||
|
updateCALayerImageAttrs(stage, konvaImage, layerState);
|
||||||
|
// Must cache after this to apply the filters
|
||||||
|
konvaImage.cache();
|
||||||
|
imageEl.id = imageId;
|
||||||
|
};
|
||||||
|
imageEl.src = imageDTO.image_url;
|
||||||
|
} else {
|
||||||
|
konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`)?.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the image attributes for a control adapter layer's image (width, height, visibility, opacity, filters).
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param konvaImage The konva image
|
||||||
|
* @param layerState The control adapter layer state
|
||||||
|
*/
|
||||||
|
|
||||||
|
const updateCALayerImageAttrs = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
konvaImage: Konva.Image,
|
||||||
|
layerState: ControlAdapterLayer
|
||||||
|
): void => {
|
||||||
|
let needsCache = false;
|
||||||
|
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching,
|
||||||
|
// but it doesn't seem to break anything.
|
||||||
|
// TODO(psyche): Investigate and report upstream.
|
||||||
|
const newWidth = stage.width() / stage.scaleX();
|
||||||
|
const newHeight = stage.height() / stage.scaleY();
|
||||||
|
const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0;
|
||||||
|
if (
|
||||||
|
konvaImage.width() !== newWidth ||
|
||||||
|
konvaImage.height() !== newHeight ||
|
||||||
|
konvaImage.visible() !== layerState.isEnabled ||
|
||||||
|
hasFilter !== layerState.isFilterEnabled
|
||||||
|
) {
|
||||||
|
konvaImage.setAttrs({
|
||||||
|
opacity: layerState.opacity,
|
||||||
|
scaleX: 1,
|
||||||
|
scaleY: 1,
|
||||||
|
width: stage.width() / stage.scaleX(),
|
||||||
|
height: stage.height() / stage.scaleY(),
|
||||||
|
visible: layerState.isEnabled,
|
||||||
|
filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [],
|
||||||
|
});
|
||||||
|
needsCache = true;
|
||||||
|
}
|
||||||
|
if (konvaImage.opacity() !== layerState.opacity) {
|
||||||
|
konvaImage.opacity(layerState.opacity);
|
||||||
|
}
|
||||||
|
if (needsCache) {
|
||||||
|
konvaImage.cache();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a control adapter layer. If the layer doesn't already exist, it is created. Otherwise, the layer is updated
|
||||||
|
* with the current image source and attributes.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerState The control adapter layer state
|
||||||
|
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
|
||||||
|
*/
|
||||||
|
export const renderCALayer = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
layerState: ControlAdapterLayer,
|
||||||
|
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
|
||||||
|
): void => {
|
||||||
|
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createCALayer(stage, layerState);
|
||||||
|
const konvaImage = konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`);
|
||||||
|
const canvasImageSource = konvaImage?.image();
|
||||||
|
let imageSourceNeedsUpdate = false;
|
||||||
|
if (canvasImageSource instanceof HTMLImageElement) {
|
||||||
|
const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image;
|
||||||
|
if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) {
|
||||||
|
imageSourceNeedsUpdate = true;
|
||||||
|
} else if (!image) {
|
||||||
|
imageSourceNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
} else if (!canvasImageSource) {
|
||||||
|
imageSourceNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageSourceNeedsUpdate) {
|
||||||
|
updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO);
|
||||||
|
} else if (konvaImage) {
|
||||||
|
updateCALayerImageAttrs(stage, konvaImage, layerState);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,149 @@
|
|||||||
|
import {
|
||||||
|
getCALayerImageId,
|
||||||
|
getIILayerImageId,
|
||||||
|
INITIAL_IMAGE_LAYER_IMAGE_NAME,
|
||||||
|
INITIAL_IMAGE_LAYER_NAME,
|
||||||
|
} from 'features/controlLayers/konva/naming';
|
||||||
|
import type { InitialImageLayer } from 'features/controlLayers/store/types';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logic for creating and rendering initial image layers. Well, just the one, actually, because it's a singleton.
|
||||||
|
* TODO(psyche): Raster layers effectively supersede the initial image layer type.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an initial image konva layer.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerState The initial image layer state
|
||||||
|
*/
|
||||||
|
const createIILayer = (stage: Konva.Stage, layerState: InitialImageLayer): Konva.Layer => {
|
||||||
|
const konvaLayer = new Konva.Layer({
|
||||||
|
id: layerState.id,
|
||||||
|
name: INITIAL_IMAGE_LAYER_NAME,
|
||||||
|
imageSmoothingEnabled: true,
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
stage.add(konvaLayer);
|
||||||
|
return konvaLayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the konva image for an initial image layer.
|
||||||
|
* @param konvaLayer The konva layer
|
||||||
|
* @param imageEl The image element
|
||||||
|
*/
|
||||||
|
const createIILayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement): Konva.Image => {
|
||||||
|
const konvaImage = new Konva.Image({
|
||||||
|
name: INITIAL_IMAGE_LAYER_IMAGE_NAME,
|
||||||
|
image: imageEl,
|
||||||
|
});
|
||||||
|
konvaLayer.add(konvaImage);
|
||||||
|
return konvaImage;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an initial image layer's attributes (width, height, opacity, visibility).
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param konvaImage The konva image
|
||||||
|
* @param layerState The initial image layer state
|
||||||
|
*/
|
||||||
|
const updateIILayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, layerState: InitialImageLayer): void => {
|
||||||
|
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching,
|
||||||
|
// but it doesn't seem to break anything.
|
||||||
|
// TODO(psyche): Investigate and report upstream.
|
||||||
|
const newWidth = stage.width() / stage.scaleX();
|
||||||
|
const newHeight = stage.height() / stage.scaleY();
|
||||||
|
if (
|
||||||
|
konvaImage.width() !== newWidth ||
|
||||||
|
konvaImage.height() !== newHeight ||
|
||||||
|
konvaImage.visible() !== layerState.isEnabled
|
||||||
|
) {
|
||||||
|
konvaImage.setAttrs({
|
||||||
|
opacity: layerState.opacity,
|
||||||
|
scaleX: 1,
|
||||||
|
scaleY: 1,
|
||||||
|
width: stage.width() / stage.scaleX(),
|
||||||
|
height: stage.height() / stage.scaleY(),
|
||||||
|
visible: layerState.isEnabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (konvaImage.opacity() !== layerState.opacity) {
|
||||||
|
konvaImage.opacity(layerState.opacity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an initial image layer's image source when the image changes.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param konvaLayer The konva layer
|
||||||
|
* @param layerState The initial image layer state
|
||||||
|
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
|
||||||
|
*/
|
||||||
|
const updateIILayerImageSource = async (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
konvaLayer: Konva.Layer,
|
||||||
|
layerState: InitialImageLayer,
|
||||||
|
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
|
||||||
|
): Promise<void> => {
|
||||||
|
if (layerState.image) {
|
||||||
|
const imageName = layerState.image.name;
|
||||||
|
const imageDTO = await getImageDTO(imageName);
|
||||||
|
if (!imageDTO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const imageEl = new Image();
|
||||||
|
const imageId = getIILayerImageId(layerState.id, imageName);
|
||||||
|
imageEl.onload = () => {
|
||||||
|
// Find the existing image or create a new one - must find using the name, bc the id may have just changed
|
||||||
|
const konvaImage =
|
||||||
|
konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`) ??
|
||||||
|
createIILayerImage(konvaLayer, imageEl);
|
||||||
|
|
||||||
|
// Update the image's attributes
|
||||||
|
konvaImage.setAttrs({
|
||||||
|
id: imageId,
|
||||||
|
image: imageEl,
|
||||||
|
});
|
||||||
|
updateIILayerImageAttrs(stage, konvaImage, layerState);
|
||||||
|
imageEl.id = imageId;
|
||||||
|
};
|
||||||
|
imageEl.src = imageDTO.image_url;
|
||||||
|
} else {
|
||||||
|
konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`)?.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an initial image layer.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerState The initial image layer state
|
||||||
|
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
|
||||||
|
*/
|
||||||
|
export const renderIILayer = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
layerState: InitialImageLayer,
|
||||||
|
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
|
||||||
|
): void => {
|
||||||
|
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createIILayer(stage, layerState);
|
||||||
|
const konvaImage = konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`);
|
||||||
|
const canvasImageSource = konvaImage?.image();
|
||||||
|
let imageSourceNeedsUpdate = false;
|
||||||
|
if (canvasImageSource instanceof HTMLImageElement) {
|
||||||
|
const image = layerState.image;
|
||||||
|
if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) {
|
||||||
|
imageSourceNeedsUpdate = true;
|
||||||
|
} else if (!image) {
|
||||||
|
imageSourceNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
} else if (!canvasImageSource) {
|
||||||
|
imageSourceNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageSourceNeedsUpdate) {
|
||||||
|
updateIILayerImageSource(stage, konvaLayer, layerState, getImageDTO);
|
||||||
|
} else if (konvaImage) {
|
||||||
|
updateIILayerImageAttrs(stage, konvaImage, layerState);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,118 @@
|
|||||||
|
import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants';
|
||||||
|
import { BACKGROUND_LAYER_ID, TOOL_PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming';
|
||||||
|
import { renderBackground } from 'features/controlLayers/konva/renderers/background';
|
||||||
|
import { renderBboxes, updateBboxes } from 'features/controlLayers/konva/renderers/bbox';
|
||||||
|
import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer';
|
||||||
|
import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer';
|
||||||
|
import { renderNoLayersMessage } from 'features/controlLayers/konva/renderers/noLayersMessage';
|
||||||
|
import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer';
|
||||||
|
import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer';
|
||||||
|
import { renderToolPreview } from 'features/controlLayers/konva/renderers/toolPreview';
|
||||||
|
import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util';
|
||||||
|
import type { Layer, Tool } from 'features/controlLayers/store/types';
|
||||||
|
import {
|
||||||
|
isControlAdapterLayer,
|
||||||
|
isInitialImageLayer,
|
||||||
|
isRasterLayer,
|
||||||
|
isRegionalGuidanceLayer,
|
||||||
|
isRenderableLayer,
|
||||||
|
} from 'features/controlLayers/store/types';
|
||||||
|
import type Konva from 'konva';
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logic for rendering arranging and rendering all layers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arranges all layers in the z-axis by updating their z-indices.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerIds An array of redux layer ids, in their z-index order
|
||||||
|
*/
|
||||||
|
const arrangeLayers = (stage: Konva.Stage, layerIds: string[]): void => {
|
||||||
|
let nextZIndex = 0;
|
||||||
|
// Background is the first layer
|
||||||
|
stage.findOne<Konva.Layer>(`#${BACKGROUND_LAYER_ID}`)?.zIndex(nextZIndex++);
|
||||||
|
// Then arrange the redux layers in order
|
||||||
|
for (const layerId of layerIds) {
|
||||||
|
stage.findOne<Konva.Layer>(`#${layerId}`)?.zIndex(nextZIndex++);
|
||||||
|
}
|
||||||
|
// Finally, the tool preview layer is always on top
|
||||||
|
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(nextZIndex++);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the layers on the stage.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerStates Array of all layer states
|
||||||
|
* @param globalMaskLayerOpacity The global mask layer opacity
|
||||||
|
* @param tool The current tool
|
||||||
|
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
|
||||||
|
* @param onLayerPosChanged Callback for when the layer's position changes
|
||||||
|
*/
|
||||||
|
const renderLayers = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
layerStates: Layer[],
|
||||||
|
globalMaskLayerOpacity: number,
|
||||||
|
tool: Tool,
|
||||||
|
getImageDTO: (imageName: string) => Promise<ImageDTO | null>,
|
||||||
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
|
): void => {
|
||||||
|
const layerIds = layerStates.filter(isRenderableLayer).map(mapId);
|
||||||
|
// Remove un-rendered layers
|
||||||
|
for (const konvaLayer of stage.find<Konva.Layer>(selectRenderableLayers)) {
|
||||||
|
if (!layerIds.includes(konvaLayer.id())) {
|
||||||
|
konvaLayer.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const layer of layerStates) {
|
||||||
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
|
renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, onLayerPosChanged);
|
||||||
|
}
|
||||||
|
if (isControlAdapterLayer(layer)) {
|
||||||
|
renderCALayer(stage, layer, getImageDTO);
|
||||||
|
}
|
||||||
|
if (isInitialImageLayer(layer)) {
|
||||||
|
renderIILayer(stage, layer, getImageDTO);
|
||||||
|
}
|
||||||
|
if (isRasterLayer(layer)) {
|
||||||
|
renderRasterLayer(stage, layer, tool, onLayerPosChanged);
|
||||||
|
}
|
||||||
|
// IP Adapter layers are not rendered
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All the renderers for the Konva stage.
|
||||||
|
*/
|
||||||
|
export const renderers = {
|
||||||
|
renderToolPreview,
|
||||||
|
renderLayers,
|
||||||
|
renderBboxes,
|
||||||
|
renderBackground,
|
||||||
|
renderNoLayersMessage,
|
||||||
|
arrangeLayers,
|
||||||
|
updateBboxes,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the renderers with debouncing applied.
|
||||||
|
* @param ms The debounce time in milliseconds
|
||||||
|
* @returns The renderers with debouncing applied
|
||||||
|
*/
|
||||||
|
const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({
|
||||||
|
renderToolPreview: debounce(renderToolPreview, ms),
|
||||||
|
renderLayers: debounce(renderLayers, ms),
|
||||||
|
renderBboxes: debounce(renderBboxes, ms),
|
||||||
|
renderBackground: debounce(renderBackground, ms),
|
||||||
|
renderNoLayersMessage: debounce(renderNoLayersMessage, ms),
|
||||||
|
arrangeLayers: debounce(arrangeLayers, ms),
|
||||||
|
updateBboxes: debounce(updateBboxes, ms),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All the renderers for the Konva stage, debounced.
|
||||||
|
*/
|
||||||
|
export const debouncedRenderers: typeof renderers = getDebouncedRenderers();
|
@ -0,0 +1,53 @@
|
|||||||
|
import { NO_LAYERS_MESSAGE_LAYER_ID } from 'features/controlLayers/konva/naming';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import Konva from 'konva';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logic for creating and rendering a fallback message when there are no layers to render.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the "no layers" fallback layer
|
||||||
|
* @param stage The konva stage
|
||||||
|
*/
|
||||||
|
const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => {
|
||||||
|
const noLayersMessageLayer = new Konva.Layer({
|
||||||
|
id: NO_LAYERS_MESSAGE_LAYER_ID,
|
||||||
|
opacity: 0.7,
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
const text = new Konva.Text({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
align: 'center',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
text: t('controlLayers.noLayersAdded', 'No Layers Added'),
|
||||||
|
fontFamily: '"Inter Variable", sans-serif',
|
||||||
|
fontStyle: '600',
|
||||||
|
fill: 'white',
|
||||||
|
});
|
||||||
|
noLayersMessageLayer.add(text);
|
||||||
|
stage.add(noLayersMessageLayer);
|
||||||
|
return noLayersMessageLayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the "no layers" message when there are no layers to render
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerCount The current number of layers
|
||||||
|
* @param width The target width of the text
|
||||||
|
* @param height The target height of the text
|
||||||
|
*/
|
||||||
|
export const renderNoLayersMessage = (stage: Konva.Stage, layerCount: number, width: number, height: number): void => {
|
||||||
|
const noLayersMessageLayer =
|
||||||
|
stage.findOne<Konva.Layer>(`#${NO_LAYERS_MESSAGE_LAYER_ID}`) ?? createNoLayersMessageLayer(stage);
|
||||||
|
if (layerCount === 0) {
|
||||||
|
noLayersMessageLayer.findOne<Konva.Text>('Text')?.setAttrs({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fontSize: 32 / stage.scaleX(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
noLayersMessageLayer?.destroy();
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,77 @@
|
|||||||
|
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||||
|
import { RG_LAYER_LINE_NAME, RG_LAYER_RECT_NAME } from 'features/controlLayers/konva/naming';
|
||||||
|
import type { BrushLine, EraserLine, RectShape } from 'features/controlLayers/store/types';
|
||||||
|
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
|
||||||
|
import Konva from 'konva';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities to create various konva objects from layer state. These are used by both the raster and regional guidance
|
||||||
|
* layers types.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a konva line for a brush line.
|
||||||
|
* @param brushLine The brush line state
|
||||||
|
* @param layerObjectGroup The konva layer's object group to add the line to
|
||||||
|
*/
|
||||||
|
export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => {
|
||||||
|
const konvaLine = new Konva.Line({
|
||||||
|
id: brushLine.id,
|
||||||
|
key: brushLine.id,
|
||||||
|
name: RG_LAYER_LINE_NAME,
|
||||||
|
strokeWidth: brushLine.strokeWidth,
|
||||||
|
tension: 0,
|
||||||
|
lineCap: 'round',
|
||||||
|
lineJoin: 'round',
|
||||||
|
shadowForStrokeEnabled: false,
|
||||||
|
globalCompositeOperation: 'source-over',
|
||||||
|
listening: false,
|
||||||
|
stroke: rgbaColorToString(brushLine.color),
|
||||||
|
});
|
||||||
|
layerObjectGroup.add(konvaLine);
|
||||||
|
return konvaLine;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a konva line for a eraser line.
|
||||||
|
* @param eraserLine The eraser line state
|
||||||
|
* @param layerObjectGroup The konva layer's object group to add the line to
|
||||||
|
*/
|
||||||
|
export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => {
|
||||||
|
const konvaLine = new Konva.Line({
|
||||||
|
id: eraserLine.id,
|
||||||
|
key: eraserLine.id,
|
||||||
|
name: RG_LAYER_LINE_NAME,
|
||||||
|
strokeWidth: eraserLine.strokeWidth,
|
||||||
|
tension: 0,
|
||||||
|
lineCap: 'round',
|
||||||
|
lineJoin: 'round',
|
||||||
|
shadowForStrokeEnabled: false,
|
||||||
|
globalCompositeOperation: 'destination-out',
|
||||||
|
listening: false,
|
||||||
|
stroke: rgbaColorToString(DEFAULT_RGBA_COLOR),
|
||||||
|
});
|
||||||
|
layerObjectGroup.add(konvaLine);
|
||||||
|
return konvaLine;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a konva rect for a rect shape.
|
||||||
|
* @param rectShape The rect shape state
|
||||||
|
* @param layerObjectGroup The konva layer's object group to add the line to
|
||||||
|
*/
|
||||||
|
export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => {
|
||||||
|
const konvaRect = new Konva.Rect({
|
||||||
|
id: rectShape.id,
|
||||||
|
key: rectShape.id,
|
||||||
|
name: RG_LAYER_RECT_NAME,
|
||||||
|
x: rectShape.x,
|
||||||
|
y: rectShape.y,
|
||||||
|
width: rectShape.width,
|
||||||
|
height: rectShape.height,
|
||||||
|
listening: false,
|
||||||
|
fill: rgbaColorToString(rectShape.color),
|
||||||
|
});
|
||||||
|
layerObjectGroup.add(konvaRect);
|
||||||
|
return konvaRect;
|
||||||
|
};
|
@ -0,0 +1,135 @@
|
|||||||
|
import {
|
||||||
|
getObjectGroupId,
|
||||||
|
RASTER_LAYER_NAME,
|
||||||
|
RASTER_LAYER_OBJECT_GROUP_NAME,
|
||||||
|
} from 'features/controlLayers/konva/naming';
|
||||||
|
import { createBrushLine, createEraserLine, createRectShape } from 'features/controlLayers/konva/renderers/objects';
|
||||||
|
import { getScaledFlooredCursorPosition, mapId } from 'features/controlLayers/konva/util';
|
||||||
|
import type { RasterLayer, Tool } from 'features/controlLayers/store/types';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logic for creating and rendering raster layers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a raster layer.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerState The raster layer state
|
||||||
|
* @param onLayerPosChanged Callback for when the layer's position changes
|
||||||
|
*/
|
||||||
|
const createRasterLayer = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
layerState: RasterLayer,
|
||||||
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
|
): Konva.Layer => {
|
||||||
|
// This layer hasn't been added to the konva state yet
|
||||||
|
const konvaLayer = new Konva.Layer({
|
||||||
|
id: layerState.id,
|
||||||
|
name: RASTER_LAYER_NAME,
|
||||||
|
draggable: true,
|
||||||
|
dragDistance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
|
||||||
|
// the position - we do not need to call this on the `dragmove` event.
|
||||||
|
if (onLayerPosChanged) {
|
||||||
|
konvaLayer.on('dragend', function (e) {
|
||||||
|
onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The dragBoundFunc limits how far the layer can be dragged
|
||||||
|
konvaLayer.dragBoundFunc(function (pos) {
|
||||||
|
const cursorPos = getScaledFlooredCursorPosition(stage);
|
||||||
|
if (!cursorPos) {
|
||||||
|
return this.getAbsolutePosition();
|
||||||
|
}
|
||||||
|
// Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds
|
||||||
|
if (
|
||||||
|
cursorPos.x < 0 ||
|
||||||
|
cursorPos.x > stage.width() / stage.scaleX() ||
|
||||||
|
cursorPos.y < 0 ||
|
||||||
|
cursorPos.y > stage.height() / stage.scaleY()
|
||||||
|
) {
|
||||||
|
return this.getAbsolutePosition();
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The object group holds all of the layer's objects (e.g. lines and rects)
|
||||||
|
const konvaObjectGroup = new Konva.Group({
|
||||||
|
id: getObjectGroupId(layerState.id, uuidv4()),
|
||||||
|
name: RASTER_LAYER_OBJECT_GROUP_NAME,
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
konvaLayer.add(konvaObjectGroup);
|
||||||
|
|
||||||
|
stage.add(konvaLayer);
|
||||||
|
|
||||||
|
return konvaLayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a regional guidance layer.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerState The regional guidance layer state
|
||||||
|
* @param tool The current tool
|
||||||
|
* @param onLayerPosChanged Callback for when the layer's position changes
|
||||||
|
*/
|
||||||
|
export const renderRasterLayer = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
layerState: RasterLayer,
|
||||||
|
tool: Tool,
|
||||||
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
|
): void => {
|
||||||
|
const konvaLayer =
|
||||||
|
stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onLayerPosChanged);
|
||||||
|
|
||||||
|
// Update the layer's position and listening state
|
||||||
|
konvaLayer.setAttrs({
|
||||||
|
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
|
||||||
|
x: Math.floor(layerState.x),
|
||||||
|
y: Math.floor(layerState.y),
|
||||||
|
});
|
||||||
|
|
||||||
|
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${RASTER_LAYER_OBJECT_GROUP_NAME}`);
|
||||||
|
assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`);
|
||||||
|
|
||||||
|
const objectIds = layerState.objects.map(mapId);
|
||||||
|
// Destroy any objects that are no longer in the redux state
|
||||||
|
for (const objectNode of konvaObjectGroup.getChildren()) {
|
||||||
|
if (!objectIds.includes(objectNode.id())) {
|
||||||
|
objectNode.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const obj of layerState.objects) {
|
||||||
|
if (obj.type === 'brush_line') {
|
||||||
|
const konvaBrushLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup);
|
||||||
|
// Only update the points if they have changed.
|
||||||
|
if (konvaBrushLine.points().length !== obj.points.length) {
|
||||||
|
konvaBrushLine.points(obj.points);
|
||||||
|
}
|
||||||
|
} else if (obj.type === 'eraser_line') {
|
||||||
|
const konvaEraserLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup);
|
||||||
|
// Only update the points if they have changed.
|
||||||
|
if (konvaEraserLine.points().length !== obj.points.length) {
|
||||||
|
konvaEraserLine.points(obj.points);
|
||||||
|
}
|
||||||
|
} else if (obj.type === 'rect_shape') {
|
||||||
|
if (!stage.findOne<Konva.Rect>(`#${obj.id}`)) {
|
||||||
|
createRectShape(obj, konvaObjectGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update layer visibility if it has changed.
|
||||||
|
if (konvaLayer.visible() !== layerState.isEnabled) {
|
||||||
|
konvaLayer.visible(layerState.isEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
konvaObjectGroup.opacity(layerState.opacity);
|
||||||
|
};
|
@ -0,0 +1,229 @@
|
|||||||
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
|
import {
|
||||||
|
COMPOSITING_RECT_NAME,
|
||||||
|
getObjectGroupId,
|
||||||
|
RG_LAYER_NAME,
|
||||||
|
RG_LAYER_OBJECT_GROUP_NAME,
|
||||||
|
} from 'features/controlLayers/konva/naming';
|
||||||
|
import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox';
|
||||||
|
import { createBrushLine, createEraserLine, createRectShape } from 'features/controlLayers/konva/renderers/objects';
|
||||||
|
import { getScaledFlooredCursorPosition, mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util';
|
||||||
|
import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logic for creating and rendering regional guidance layers.
|
||||||
|
*
|
||||||
|
* Some special handling is needed to render layer opacity correctly using a "compositing rect". See the comments
|
||||||
|
* in `renderRGLayer`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the "compositing rect" for a regional guidance layer.
|
||||||
|
* @param konvaLayer The konva layer
|
||||||
|
*/
|
||||||
|
const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
|
||||||
|
const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false });
|
||||||
|
konvaLayer.add(compositingRect);
|
||||||
|
return compositingRect;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a regional guidance layer.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerState The regional guidance layer state
|
||||||
|
* @param onLayerPosChanged Callback for when the layer's position changes
|
||||||
|
*/
|
||||||
|
const createRGLayer = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
layerState: RegionalGuidanceLayer,
|
||||||
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
|
): Konva.Layer => {
|
||||||
|
// This layer hasn't been added to the konva state yet
|
||||||
|
const konvaLayer = new Konva.Layer({
|
||||||
|
id: layerState.id,
|
||||||
|
name: RG_LAYER_NAME,
|
||||||
|
draggable: true,
|
||||||
|
dragDistance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
|
||||||
|
// the position - we do not need to call this on the `dragmove` event.
|
||||||
|
if (onLayerPosChanged) {
|
||||||
|
konvaLayer.on('dragend', function (e) {
|
||||||
|
onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The dragBoundFunc limits how far the layer can be dragged
|
||||||
|
konvaLayer.dragBoundFunc(function (pos) {
|
||||||
|
const cursorPos = getScaledFlooredCursorPosition(stage);
|
||||||
|
if (!cursorPos) {
|
||||||
|
return this.getAbsolutePosition();
|
||||||
|
}
|
||||||
|
// Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds
|
||||||
|
if (
|
||||||
|
cursorPos.x < 0 ||
|
||||||
|
cursorPos.x > stage.width() / stage.scaleX() ||
|
||||||
|
cursorPos.y < 0 ||
|
||||||
|
cursorPos.y > stage.height() / stage.scaleY()
|
||||||
|
) {
|
||||||
|
return this.getAbsolutePosition();
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The object group holds all of the layer's objects (e.g. lines and rects)
|
||||||
|
const konvaObjectGroup = new Konva.Group({
|
||||||
|
id: getObjectGroupId(layerState.id, uuidv4()),
|
||||||
|
name: RG_LAYER_OBJECT_GROUP_NAME,
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
konvaLayer.add(konvaObjectGroup);
|
||||||
|
|
||||||
|
stage.add(konvaLayer);
|
||||||
|
|
||||||
|
return konvaLayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a raster layer.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param layerState The regional guidance layer state
|
||||||
|
* @param globalMaskLayerOpacity The global mask layer opacity
|
||||||
|
* @param tool The current tool
|
||||||
|
* @param onLayerPosChanged Callback for when the layer's position changes
|
||||||
|
*/
|
||||||
|
export const renderRGLayer = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
layerState: RegionalGuidanceLayer,
|
||||||
|
globalMaskLayerOpacity: number,
|
||||||
|
tool: Tool,
|
||||||
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
|
): void => {
|
||||||
|
const konvaLayer =
|
||||||
|
stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createRGLayer(stage, layerState, onLayerPosChanged);
|
||||||
|
|
||||||
|
// Update the layer's position and listening state
|
||||||
|
konvaLayer.setAttrs({
|
||||||
|
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
|
||||||
|
x: Math.floor(layerState.x),
|
||||||
|
y: Math.floor(layerState.y),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
|
||||||
|
const rgbColor = rgbColorToString(layerState.previewColor);
|
||||||
|
|
||||||
|
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${RG_LAYER_OBJECT_GROUP_NAME}`);
|
||||||
|
assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`);
|
||||||
|
|
||||||
|
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
|
||||||
|
let groupNeedsCache = false;
|
||||||
|
|
||||||
|
const objectIds = layerState.objects.map(mapId);
|
||||||
|
// Destroy any objects that are no longer in the redux state
|
||||||
|
for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) {
|
||||||
|
if (!objectIds.includes(objectNode.id())) {
|
||||||
|
objectNode.destroy();
|
||||||
|
groupNeedsCache = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const obj of layerState.objects) {
|
||||||
|
if (obj.type === 'brush_line') {
|
||||||
|
const konvaBrushLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup);
|
||||||
|
|
||||||
|
// Only update the points if they have changed. The point values are never mutated, they are only added to the
|
||||||
|
// array, so checking the length is sufficient to determine if we need to re-cache.
|
||||||
|
if (konvaBrushLine.points().length !== obj.points.length) {
|
||||||
|
konvaBrushLine.points(obj.points);
|
||||||
|
groupNeedsCache = true;
|
||||||
|
}
|
||||||
|
// Only update the color if it has changed.
|
||||||
|
if (konvaBrushLine.stroke() !== rgbColor) {
|
||||||
|
konvaBrushLine.stroke(rgbColor);
|
||||||
|
groupNeedsCache = true;
|
||||||
|
}
|
||||||
|
} else if (obj.type === 'eraser_line') {
|
||||||
|
const konvaEraserLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup);
|
||||||
|
|
||||||
|
// Only update the points if they have changed. The point values are never mutated, they are only added to the
|
||||||
|
// array, so checking the length is sufficient to determine if we need to re-cache.
|
||||||
|
if (konvaEraserLine.points().length !== obj.points.length) {
|
||||||
|
konvaEraserLine.points(obj.points);
|
||||||
|
groupNeedsCache = true;
|
||||||
|
}
|
||||||
|
// Only update the color if it has changed.
|
||||||
|
if (konvaEraserLine.stroke() !== rgbColor) {
|
||||||
|
konvaEraserLine.stroke(rgbColor);
|
||||||
|
groupNeedsCache = true;
|
||||||
|
}
|
||||||
|
} else if (obj.type === 'rect_shape') {
|
||||||
|
const konvaRectShape = stage.findOne<Konva.Rect>(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup);
|
||||||
|
|
||||||
|
// Only update the color if it has changed.
|
||||||
|
if (konvaRectShape.fill() !== rgbColor) {
|
||||||
|
konvaRectShape.fill(rgbColor);
|
||||||
|
groupNeedsCache = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update layer visibility if it has changed.
|
||||||
|
if (konvaLayer.visible() !== layerState.isEnabled) {
|
||||||
|
konvaLayer.visible(layerState.isEnabled);
|
||||||
|
groupNeedsCache = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (konvaObjectGroup.getChildren().length === 0) {
|
||||||
|
// No objects - clear the cache to reset the previous pixel data
|
||||||
|
konvaObjectGroup.clearCache();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compositingRect =
|
||||||
|
konvaLayer.findOne<Konva.Rect>(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows
|
||||||
|
* shapes to render as a "raster" layer with all pixels drawn at the same color and opacity.
|
||||||
|
*
|
||||||
|
* Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The
|
||||||
|
* effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity.
|
||||||
|
* Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes.
|
||||||
|
*
|
||||||
|
* Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to
|
||||||
|
* a single raster image, and _then_ applied the 50% opacity.
|
||||||
|
*/
|
||||||
|
if (layerState.isSelected && tool !== 'move') {
|
||||||
|
// We must clear the cache first so Konva will re-draw the group with the new compositing rect
|
||||||
|
if (konvaObjectGroup.isCached()) {
|
||||||
|
konvaObjectGroup.clearCache();
|
||||||
|
}
|
||||||
|
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
|
||||||
|
konvaObjectGroup.opacity(1);
|
||||||
|
|
||||||
|
compositingRect.setAttrs({
|
||||||
|
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
|
||||||
|
...(!layerState.bboxNeedsUpdate && layerState.bbox ? layerState.bbox : getLayerBboxFast(konvaLayer)),
|
||||||
|
fill: rgbColor,
|
||||||
|
opacity: globalMaskLayerOpacity,
|
||||||
|
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
||||||
|
globalCompositeOperation: 'source-in',
|
||||||
|
visible: true,
|
||||||
|
// This rect must always be on top of all other shapes
|
||||||
|
zIndex: konvaObjectGroup.getChildren().length,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// The compositing rect should only be shown when the layer is selected.
|
||||||
|
compositingRect.visible(false);
|
||||||
|
// Cache only if needed - or if we are on this code path and _don't_ have a cache
|
||||||
|
if (groupNeedsCache || !konvaObjectGroup.isCached()) {
|
||||||
|
konvaObjectGroup.cache();
|
||||||
|
}
|
||||||
|
// Updating group opacity does not require re-caching
|
||||||
|
konvaObjectGroup.opacity(globalMaskLayerOpacity);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,161 @@
|
|||||||
|
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||||
|
import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants';
|
||||||
|
import {
|
||||||
|
TOOL_PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||||
|
TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||||
|
TOOL_PREVIEW_BRUSH_FILL_ID,
|
||||||
|
TOOL_PREVIEW_BRUSH_GROUP_ID,
|
||||||
|
TOOL_PREVIEW_LAYER_ID,
|
||||||
|
TOOL_PREVIEW_RECT_ID,
|
||||||
|
} from 'features/controlLayers/konva/naming';
|
||||||
|
import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/konva/util';
|
||||||
|
import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import type { Vector2d } from 'konva/lib/types';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logic to create and render the singleton tool preview layer.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the singleton tool preview layer and all its objects.
|
||||||
|
* @param stage The konva stage
|
||||||
|
*/
|
||||||
|
const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => {
|
||||||
|
// Initialize the brush preview layer & add to the stage
|
||||||
|
const toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, visible: false, listening: false });
|
||||||
|
stage.add(toolPreviewLayer);
|
||||||
|
|
||||||
|
// Create the brush preview group & circles
|
||||||
|
const brushPreviewGroup = new Konva.Group({ id: TOOL_PREVIEW_BRUSH_GROUP_ID });
|
||||||
|
const brushPreviewFill = new Konva.Circle({
|
||||||
|
id: TOOL_PREVIEW_BRUSH_FILL_ID,
|
||||||
|
listening: false,
|
||||||
|
strokeEnabled: false,
|
||||||
|
});
|
||||||
|
brushPreviewGroup.add(brushPreviewFill);
|
||||||
|
const brushPreviewBorderInner = new Konva.Circle({
|
||||||
|
id: TOOL_PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||||
|
listening: false,
|
||||||
|
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeEnabled: true,
|
||||||
|
});
|
||||||
|
brushPreviewGroup.add(brushPreviewBorderInner);
|
||||||
|
const brushPreviewBorderOuter = new Konva.Circle({
|
||||||
|
id: TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||||
|
listening: false,
|
||||||
|
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeEnabled: true,
|
||||||
|
});
|
||||||
|
brushPreviewGroup.add(brushPreviewBorderOuter);
|
||||||
|
toolPreviewLayer.add(brushPreviewGroup);
|
||||||
|
|
||||||
|
// Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position
|
||||||
|
const rectPreview = new Konva.Rect({ id: TOOL_PREVIEW_RECT_ID, listening: false, stroke: 'white', strokeWidth: 1 });
|
||||||
|
toolPreviewLayer.add(rectPreview);
|
||||||
|
|
||||||
|
return toolPreviewLayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the brush preview for the selected tool.
|
||||||
|
* @param stage The konva stage
|
||||||
|
* @param tool The selected tool
|
||||||
|
* @param color The selected layer's color
|
||||||
|
* @param selectedLayerType The selected layer's type
|
||||||
|
* @param globalMaskLayerOpacity The global mask layer opacity
|
||||||
|
* @param cursorPos The cursor position
|
||||||
|
* @param lastMouseDownPos The position of the last mouse down event - used for the rect tool
|
||||||
|
* @param brushSize The brush size
|
||||||
|
*/
|
||||||
|
export const renderToolPreview = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
tool: Tool,
|
||||||
|
brushColor: RgbaColor,
|
||||||
|
selectedLayerType: Layer['type'] | null,
|
||||||
|
globalMaskLayerOpacity: number,
|
||||||
|
cursorPos: Vector2d | null,
|
||||||
|
lastMouseDownPos: Vector2d | null,
|
||||||
|
brushSize: number
|
||||||
|
): void => {
|
||||||
|
const layerCount = stage.find(selectRenderableLayers).length;
|
||||||
|
// Update the stage's pointer style
|
||||||
|
if (layerCount === 0) {
|
||||||
|
// We have no layers, so we should not render any tool
|
||||||
|
stage.container().style.cursor = 'default';
|
||||||
|
} else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') {
|
||||||
|
// Non-mask-guidance layers don't have tools
|
||||||
|
stage.container().style.cursor = 'not-allowed';
|
||||||
|
} else if (tool === 'move') {
|
||||||
|
// Move tool gets a pointer
|
||||||
|
stage.container().style.cursor = 'default';
|
||||||
|
} else if (tool === 'rect') {
|
||||||
|
// Move rect gets a crosshair
|
||||||
|
stage.container().style.cursor = 'crosshair';
|
||||||
|
} else {
|
||||||
|
// Else we hide the native cursor and use the konva-rendered brush preview
|
||||||
|
stage.container().style.cursor = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolPreviewLayer = stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`) ?? createToolPreviewLayer(stage);
|
||||||
|
|
||||||
|
if (!cursorPos || layerCount === 0) {
|
||||||
|
// We can bail early if the mouse isn't over the stage or there are no layers
|
||||||
|
toolPreviewLayer.visible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolPreviewLayer.visible(true);
|
||||||
|
|
||||||
|
const brushPreviewGroup = stage.findOne<Konva.Group>(`#${TOOL_PREVIEW_BRUSH_GROUP_ID}`);
|
||||||
|
assert(brushPreviewGroup, 'Brush preview group not found');
|
||||||
|
|
||||||
|
const rectPreview = stage.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`);
|
||||||
|
assert(rectPreview, 'Rect preview not found');
|
||||||
|
|
||||||
|
// No need to render the brush preview if the cursor position or color is missing
|
||||||
|
if (cursorPos && (tool === 'brush' || tool === 'eraser')) {
|
||||||
|
// Update the fill circle
|
||||||
|
const brushPreviewFill = brushPreviewGroup.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`);
|
||||||
|
brushPreviewFill?.setAttrs({
|
||||||
|
x: cursorPos.x,
|
||||||
|
y: cursorPos.y,
|
||||||
|
radius: brushSize / 2,
|
||||||
|
fill: rgbaColorToString(brushColor),
|
||||||
|
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the inner border of the brush preview
|
||||||
|
const brushPreviewInner = toolPreviewLayer.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_BORDER_INNER_ID}`);
|
||||||
|
brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 });
|
||||||
|
|
||||||
|
// Update the outer border of the brush preview
|
||||||
|
const brushPreviewOuter = toolPreviewLayer.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID}`);
|
||||||
|
brushPreviewOuter?.setAttrs({
|
||||||
|
x: cursorPos.x,
|
||||||
|
y: cursorPos.y,
|
||||||
|
radius: brushSize / 2 + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
brushPreviewGroup.visible(true);
|
||||||
|
} else {
|
||||||
|
brushPreviewGroup.visible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorPos && lastMouseDownPos && tool === 'rect') {
|
||||||
|
const snappedPos = snapPosToStage(cursorPos, stage);
|
||||||
|
const rectPreview = toolPreviewLayer.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`);
|
||||||
|
rectPreview?.setAttrs({
|
||||||
|
x: Math.min(snappedPos.x, lastMouseDownPos.x),
|
||||||
|
y: Math.min(snappedPos.y, lastMouseDownPos.y),
|
||||||
|
width: Math.abs(snappedPos.x - lastMouseDownPos.x),
|
||||||
|
height: Math.abs(snappedPos.y - lastMouseDownPos.y),
|
||||||
|
});
|
||||||
|
rectPreview?.visible(true);
|
||||||
|
} else {
|
||||||
|
rectPreview?.visible(false);
|
||||||
|
}
|
||||||
|
};
|
@ -1,3 +1,11 @@
|
|||||||
|
import {
|
||||||
|
CA_LAYER_NAME,
|
||||||
|
INITIAL_IMAGE_LAYER_NAME,
|
||||||
|
RASTER_LAYER_NAME,
|
||||||
|
RG_LAYER_LINE_NAME,
|
||||||
|
RG_LAYER_NAME,
|
||||||
|
RG_LAYER_RECT_NAME,
|
||||||
|
} from 'features/controlLayers/konva/naming';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import type { Vector2d } from 'konva/lib/types';
|
import type { Vector2d } from 'konva/lib/types';
|
||||||
@ -65,3 +73,33 @@ export const getIsMouseDown = (e: KonvaEventObject<MouseEvent>): boolean => e.ev
|
|||||||
*/
|
*/
|
||||||
export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().contains(document.activeElement);
|
export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().contains(document.activeElement);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region mapId
|
||||||
|
/**
|
||||||
|
* Simple util to map an object to its id property. Serves as a minor optimization to avoid recreating a map callback
|
||||||
|
* every time we need to map an object to its id, which happens very often.
|
||||||
|
* @param object The object with an `id` property
|
||||||
|
* @returns The object's id property
|
||||||
|
*/
|
||||||
|
export const mapId = (object: { id: string }): string => object.id;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region konva selector callbacks
|
||||||
|
/**
|
||||||
|
* Konva selection callback to select all renderable layers. This includes RG, CA II and Raster layers.
|
||||||
|
* This can be provided to the `find` or `findOne` konva node methods.
|
||||||
|
*/
|
||||||
|
export const selectRenderableLayers = (n: Konva.Node): boolean =>
|
||||||
|
n.name() === RG_LAYER_NAME ||
|
||||||
|
n.name() === CA_LAYER_NAME ||
|
||||||
|
n.name() === INITIAL_IMAGE_LAYER_NAME ||
|
||||||
|
n.name() === RASTER_LAYER_NAME;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konva selection callback to select RG mask objects. This includes lines and rects.
|
||||||
|
* This can be provided to the `find` or `findOne` konva node methods.
|
||||||
|
*/
|
||||||
|
export const selectVectorMaskObjects = (node: Konva.Node): boolean => {
|
||||||
|
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME;
|
||||||
|
};
|
||||||
|
//#endregion
|
||||||
|
@ -50,23 +50,28 @@ import type {
|
|||||||
AddEraserLineArg,
|
AddEraserLineArg,
|
||||||
AddPointToLineArg,
|
AddPointToLineArg,
|
||||||
AddRectShapeArg,
|
AddRectShapeArg,
|
||||||
BrushLine,
|
|
||||||
ControlAdapterLayer,
|
ControlAdapterLayer,
|
||||||
ControlLayersState,
|
ControlLayersState,
|
||||||
EllipseShape,
|
|
||||||
EraserLine,
|
|
||||||
ImageObject,
|
|
||||||
InitialImageLayer,
|
InitialImageLayer,
|
||||||
IPAdapterLayer,
|
IPAdapterLayer,
|
||||||
Layer,
|
Layer,
|
||||||
PolygonShape,
|
|
||||||
RasterLayer,
|
RasterLayer,
|
||||||
RectShape,
|
|
||||||
RegionalGuidanceLayer,
|
RegionalGuidanceLayer,
|
||||||
RgbaColor,
|
RgbaColor,
|
||||||
Tool,
|
Tool,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { DEFAULT_RGBA_COLOR } from './types';
|
import {
|
||||||
|
DEFAULT_RGBA_COLOR,
|
||||||
|
isCAOrIPALayer,
|
||||||
|
isControlAdapterLayer,
|
||||||
|
isInitialImageLayer,
|
||||||
|
isIPAdapterLayer,
|
||||||
|
isLine,
|
||||||
|
isRasterLayer,
|
||||||
|
isRegionalGuidanceLayer,
|
||||||
|
isRenderableLayer,
|
||||||
|
isRGOrRasterlayer,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
export const initialControlLayersState: ControlLayersState = {
|
export const initialControlLayersState: ControlLayersState = {
|
||||||
_version: 3,
|
_version: 3,
|
||||||
@ -87,76 +92,31 @@ export const initialControlLayersState: ControlLayersState = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLine = (
|
/**
|
||||||
obj: BrushLine | EraserLine | RectShape | EllipseShape | PolygonShape | ImageObject
|
* A selector that accepts a type guard and returns the first layer that matches the guard.
|
||||||
): obj is BrushLine => obj.type === 'brush_line' || obj.type === 'eraser_line';
|
* Throws if the layer is not found or does not match the guard.
|
||||||
export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer =>
|
*/
|
||||||
layer?.type === 'regional_guidance_layer';
|
export const selectLayerOrThrow = <T extends Layer>(
|
||||||
export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer =>
|
state: ControlLayersState,
|
||||||
layer?.type === 'control_adapter_layer';
|
layerId: string,
|
||||||
export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => layer?.type === 'ip_adapter_layer';
|
predicate: (layer: Layer) => layer is T
|
||||||
export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => layer?.type === 'initial_image_layer';
|
): T => {
|
||||||
export const isRasterLayer = (layer?: Layer): layer is RasterLayer => layer?.type === 'raster_layer';
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
export const isRenderableLayer = (
|
assert(layer && predicate(layer));
|
||||||
layer?: Layer
|
return layer;
|
||||||
): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer =>
|
};
|
||||||
layer?.type === 'regional_guidance_layer' ||
|
|
||||||
layer?.type === 'control_adapter_layer' ||
|
|
||||||
layer?.type === 'initial_image_layer' ||
|
|
||||||
layer?.type === 'raster_layer';
|
|
||||||
|
|
||||||
export const selectCALayerOrThrow = (state: ControlLayersState, layerId: string): ControlAdapterLayer => {
|
|
||||||
const layer = state.layers.find((l) => l.id === layerId);
|
|
||||||
assert(isControlAdapterLayer(layer));
|
|
||||||
return layer;
|
|
||||||
};
|
|
||||||
export const selectIPALayerOrThrow = (state: ControlLayersState, layerId: string): IPAdapterLayer => {
|
|
||||||
const layer = state.layers.find((l) => l.id === layerId);
|
|
||||||
assert(isIPAdapterLayer(layer));
|
|
||||||
return layer;
|
|
||||||
};
|
|
||||||
export const selectIILayerOrThrow = (state: ControlLayersState, layerId: string): InitialImageLayer => {
|
|
||||||
const layer = state.layers.find((l) => l.id === layerId);
|
|
||||||
assert(isInitialImageLayer(layer));
|
|
||||||
return layer;
|
|
||||||
};
|
|
||||||
export const selectRasterLayerOrThrow = (state: ControlLayersState, layerId: string): RasterLayer => {
|
|
||||||
const layer = state.layers.find((l) => l.id === layerId);
|
|
||||||
assert(isRasterLayer(layer));
|
|
||||||
return layer;
|
|
||||||
};
|
|
||||||
const selectCAOrIPALayerOrThrow = (
|
|
||||||
state: ControlLayersState,
|
|
||||||
layerId: string
|
|
||||||
): ControlAdapterLayer | IPAdapterLayer => {
|
|
||||||
const layer = state.layers.find((l) => l.id === layerId);
|
|
||||||
assert(isControlAdapterLayer(layer) || isIPAdapterLayer(layer));
|
|
||||||
return layer;
|
|
||||||
};
|
|
||||||
const selectRGLayerOrThrow = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => {
|
|
||||||
const layer = state.layers.find((l) => l.id === layerId);
|
|
||||||
assert(isRegionalGuidanceLayer(layer));
|
|
||||||
return layer;
|
|
||||||
};
|
|
||||||
const selectRGOrRasterLayerOrThrow = (
|
|
||||||
state: ControlLayersState,
|
|
||||||
layerId: string
|
|
||||||
): RegionalGuidanceLayer | RasterLayer => {
|
|
||||||
const layer = state.layers.find((l) => l.id === layerId);
|
|
||||||
assert(isRegionalGuidanceLayer(layer) || isRasterLayer(layer));
|
|
||||||
return layer;
|
|
||||||
};
|
|
||||||
export const selectRGLayerIPAdapterOrThrow = (
|
export const selectRGLayerIPAdapterOrThrow = (
|
||||||
state: ControlLayersState,
|
state: ControlLayersState,
|
||||||
layerId: string,
|
layerId: string,
|
||||||
ipAdapterId: string
|
ipAdapterId: string
|
||||||
): IPAdapterConfigV2 => {
|
): IPAdapterConfigV2 => {
|
||||||
const layer = state.layers.find((l) => l.id === layerId);
|
const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer);
|
||||||
assert(isRegionalGuidanceLayer(layer));
|
|
||||||
const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId);
|
const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId);
|
||||||
assert(ipAdapter);
|
assert(ipAdapter);
|
||||||
return ipAdapter;
|
return ipAdapter;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => {
|
const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => {
|
||||||
const rgLayers = state.layers.filter(isRegionalGuidanceLayer);
|
const rgLayers = state.layers.filter(isRegionalGuidanceLayer);
|
||||||
const lastColor = rgLayers[rgLayers.length - 1]?.previewColor;
|
const lastColor = rgLayers[rgLayers.length - 1]?.previewColor;
|
||||||
@ -222,6 +182,13 @@ export const controlLayersSlice = createSlice({
|
|||||||
state.layers = state.layers.filter((l) => l.id !== action.payload);
|
state.layers = state.layers.filter((l) => l.id !== action.payload);
|
||||||
state.selectedLayerId = state.layers[0]?.id ?? null;
|
state.selectedLayerId = state.layers[0]?.id ?? null;
|
||||||
},
|
},
|
||||||
|
layerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => {
|
||||||
|
const { layerId, opacity } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer)) {
|
||||||
|
layer.opacity = opacity;
|
||||||
|
}
|
||||||
|
},
|
||||||
layerMovedForward: (state, action: PayloadAction<string>) => {
|
layerMovedForward: (state, action: PayloadAction<string>) => {
|
||||||
const cb = (l: Layer) => l.id === action.payload;
|
const cb = (l: Layer) => l.id === action.payload;
|
||||||
const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer);
|
const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer);
|
||||||
@ -291,7 +258,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer);
|
||||||
layer.bbox = null;
|
layer.bbox = null;
|
||||||
layer.bboxNeedsUpdate = true;
|
layer.bboxNeedsUpdate = true;
|
||||||
layer.isEnabled = true;
|
layer.isEnabled = true;
|
||||||
@ -309,7 +276,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
caLayerProcessedImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
caLayerProcessedImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer);
|
||||||
layer.bbox = null;
|
layer.bbox = null;
|
||||||
layer.bboxNeedsUpdate = true;
|
layer.bboxNeedsUpdate = true;
|
||||||
layer.isEnabled = true;
|
layer.isEnabled = true;
|
||||||
@ -323,7 +290,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { layerId, modelConfig } = action.payload;
|
const { layerId, modelConfig } = action.payload;
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer);
|
||||||
if (!modelConfig) {
|
if (!modelConfig) {
|
||||||
layer.controlAdapter.model = null;
|
layer.controlAdapter.model = null;
|
||||||
return;
|
return;
|
||||||
@ -347,7 +314,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlModeV2 }>) => {
|
caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlModeV2 }>) => {
|
||||||
const { layerId, controlMode } = action.payload;
|
const { layerId, controlMode } = action.payload;
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer);
|
||||||
assert(layer.controlAdapter.type === 'controlnet');
|
assert(layer.controlAdapter.type === 'controlnet');
|
||||||
layer.controlAdapter.controlMode = controlMode;
|
layer.controlAdapter.controlMode = controlMode;
|
||||||
},
|
},
|
||||||
@ -356,7 +323,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
action: PayloadAction<{ layerId: string; processorConfig: ProcessorConfig | null }>
|
action: PayloadAction<{ layerId: string; processorConfig: ProcessorConfig | null }>
|
||||||
) => {
|
) => {
|
||||||
const { layerId, processorConfig } = action.payload;
|
const { layerId, processorConfig } = action.payload;
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer);
|
||||||
layer.controlAdapter.processorConfig = processorConfig;
|
layer.controlAdapter.processorConfig = processorConfig;
|
||||||
if (!processorConfig) {
|
if (!processorConfig) {
|
||||||
layer.controlAdapter.processedImage = null;
|
layer.controlAdapter.processedImage = null;
|
||||||
@ -364,20 +331,15 @@ export const controlLayersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
caLayerIsFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => {
|
caLayerIsFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => {
|
||||||
const { layerId, isFilterEnabled } = action.payload;
|
const { layerId, isFilterEnabled } = action.payload;
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer);
|
||||||
layer.isFilterEnabled = isFilterEnabled;
|
layer.isFilterEnabled = isFilterEnabled;
|
||||||
},
|
},
|
||||||
caLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => {
|
|
||||||
const { layerId, opacity } = action.payload;
|
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
|
||||||
layer.opacity = opacity;
|
|
||||||
},
|
|
||||||
caLayerProcessorPendingBatchIdChanged: (
|
caLayerProcessorPendingBatchIdChanged: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ layerId: string; batchId: string | null }>
|
action: PayloadAction<{ layerId: string; batchId: string | null }>
|
||||||
) => {
|
) => {
|
||||||
const { layerId, batchId } = action.payload;
|
const { layerId, batchId } = action.payload;
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer);
|
||||||
layer.controlAdapter.processorPendingBatchId = batchId;
|
layer.controlAdapter.processorPendingBatchId = batchId;
|
||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -403,12 +365,12 @@ export const controlLayersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
const layer = selectIPALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer);
|
||||||
layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
||||||
},
|
},
|
||||||
ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethodV2 }>) => {
|
ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethodV2 }>) => {
|
||||||
const { layerId, method } = action.payload;
|
const { layerId, method } = action.payload;
|
||||||
const layer = selectIPALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer);
|
||||||
layer.ipAdapter.method = method;
|
layer.ipAdapter.method = method;
|
||||||
},
|
},
|
||||||
ipaLayerModelChanged: (
|
ipaLayerModelChanged: (
|
||||||
@ -419,7 +381,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { layerId, modelConfig } = action.payload;
|
const { layerId, modelConfig } = action.payload;
|
||||||
const layer = selectIPALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer);
|
||||||
if (!modelConfig) {
|
if (!modelConfig) {
|
||||||
layer.ipAdapter.model = null;
|
layer.ipAdapter.model = null;
|
||||||
return;
|
return;
|
||||||
@ -431,7 +393,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModelV2 }>
|
action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModelV2 }>
|
||||||
) => {
|
) => {
|
||||||
const { layerId, clipVisionModel } = action.payload;
|
const { layerId, clipVisionModel } = action.payload;
|
||||||
const layer = selectIPALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer);
|
||||||
layer.ipAdapter.clipVisionModel = clipVisionModel;
|
layer.ipAdapter.clipVisionModel = clipVisionModel;
|
||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -439,7 +401,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
//#region CA or IPA Layers
|
//#region CA or IPA Layers
|
||||||
caOrIPALayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => {
|
caOrIPALayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => {
|
||||||
const { layerId, weight } = action.payload;
|
const { layerId, weight } = action.payload;
|
||||||
const layer = selectCAOrIPALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isCAOrIPALayer);
|
||||||
if (layer.type === 'control_adapter_layer') {
|
if (layer.type === 'control_adapter_layer') {
|
||||||
layer.controlAdapter.weight = weight;
|
layer.controlAdapter.weight = weight;
|
||||||
} else {
|
} else {
|
||||||
@ -451,7 +413,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }>
|
action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }>
|
||||||
) => {
|
) => {
|
||||||
const { layerId, beginEndStepPct } = action.payload;
|
const { layerId, beginEndStepPct } = action.payload;
|
||||||
const layer = selectCAOrIPALayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isCAOrIPALayer);
|
||||||
if (layer.type === 'control_adapter_layer') {
|
if (layer.type === 'control_adapter_layer') {
|
||||||
layer.controlAdapter.beginEndStepPct = beginEndStepPct;
|
layer.controlAdapter.beginEndStepPct = beginEndStepPct;
|
||||||
} else {
|
} else {
|
||||||
@ -492,119 +454,23 @@ export const controlLayersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
||||||
const { layerId, prompt } = action.payload;
|
const { layerId, prompt } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer);
|
||||||
layer.positivePrompt = prompt;
|
layer.positivePrompt = prompt;
|
||||||
},
|
},
|
||||||
rgLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
rgLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
||||||
const { layerId, prompt } = action.payload;
|
const { layerId, prompt } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer);
|
||||||
layer.negativePrompt = prompt;
|
layer.negativePrompt = prompt;
|
||||||
},
|
},
|
||||||
rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => {
|
rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => {
|
||||||
const { layerId, color } = action.payload;
|
const { layerId, color } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer);
|
||||||
layer.previewColor = color;
|
layer.previewColor = color;
|
||||||
},
|
},
|
||||||
brushLineAdded: {
|
|
||||||
reducer: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<
|
|
||||||
AddBrushLineArg & {
|
|
||||||
lineUuid: string;
|
|
||||||
}
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
const { layerId, points, lineUuid, color } = action.payload;
|
|
||||||
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
|
|
||||||
layer.objects.push({
|
|
||||||
id: getBrushLineId(layer.id, lineUuid),
|
|
||||||
type: 'brush_line',
|
|
||||||
// Points must be offset by the layer's x and y coordinates
|
|
||||||
// TODO: Handle this in the event listener?
|
|
||||||
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
|
||||||
strokeWidth: state.brushSize,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
layer.bboxNeedsUpdate = true;
|
|
||||||
if (layer.type === 'regional_guidance_layer') {
|
|
||||||
layer.uploadedMaskImage = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prepare: (payload: AddBrushLineArg) => ({
|
|
||||||
payload: { ...payload, lineUuid: uuidv4() },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
eraserLineAdded: {
|
|
||||||
reducer: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<
|
|
||||||
AddEraserLineArg & {
|
|
||||||
lineUuid: string;
|
|
||||||
}
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
const { layerId, points, lineUuid } = action.payload;
|
|
||||||
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
|
|
||||||
layer.objects.push({
|
|
||||||
id: getEraserLineId(layer.id, lineUuid),
|
|
||||||
type: 'eraser_line',
|
|
||||||
// Points must be offset by the layer's x and y coordinates
|
|
||||||
// TODO: Handle this in the event listener?
|
|
||||||
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
|
||||||
strokeWidth: state.brushSize,
|
|
||||||
});
|
|
||||||
layer.bboxNeedsUpdate = true;
|
|
||||||
if (isRegionalGuidanceLayer(layer)) {
|
|
||||||
layer.uploadedMaskImage = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prepare: (payload: AddEraserLineArg) => ({
|
|
||||||
payload: { ...payload, lineUuid: uuidv4() },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
linePointsAdded: (state, action: PayloadAction<AddPointToLineArg>) => {
|
|
||||||
const { layerId, point } = action.payload;
|
|
||||||
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
|
|
||||||
const lastLine = layer.objects.findLast(isLine);
|
|
||||||
if (!lastLine || !isLine(lastLine)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Points must be offset by the layer's x and y coordinates
|
|
||||||
// TODO: Handle this in the event listener
|
|
||||||
lastLine.points.push(point[0] - layer.x, point[1] - layer.y);
|
|
||||||
layer.bboxNeedsUpdate = true;
|
|
||||||
if (isRegionalGuidanceLayer(layer)) {
|
|
||||||
layer.uploadedMaskImage = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rectAdded: {
|
|
||||||
reducer: (state, action: PayloadAction<AddRectShapeArg & { rectUuid: string }>) => {
|
|
||||||
const { layerId, rect, rectUuid, color } = action.payload;
|
|
||||||
if (rect.height === 0 || rect.width === 0) {
|
|
||||||
// Ignore zero-area rectangles
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
|
|
||||||
const id = getRectId(layer.id, rectUuid);
|
|
||||||
layer.objects.push({
|
|
||||||
type: 'rect_shape',
|
|
||||||
id,
|
|
||||||
x: rect.x - layer.x,
|
|
||||||
y: rect.y - layer.y,
|
|
||||||
width: rect.width,
|
|
||||||
height: rect.height,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
layer.bboxNeedsUpdate = true;
|
|
||||||
if (isRegionalGuidanceLayer(layer)) {
|
|
||||||
layer.uploadedMaskImage = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }),
|
|
||||||
},
|
|
||||||
rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => {
|
rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer);
|
||||||
layer.uploadedMaskImage = imageDTOToImageWithDims(imageDTO);
|
layer.uploadedMaskImage = imageDTOToImageWithDims(imageDTO);
|
||||||
},
|
},
|
||||||
rgLayerAutoNegativeChanged: (
|
rgLayerAutoNegativeChanged: (
|
||||||
@ -612,17 +478,17 @@ export const controlLayersSlice = createSlice({
|
|||||||
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
|
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
|
||||||
) => {
|
) => {
|
||||||
const { layerId, autoNegative } = action.payload;
|
const { layerId, autoNegative } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer);
|
||||||
layer.autoNegative = autoNegative;
|
layer.autoNegative = autoNegative;
|
||||||
},
|
},
|
||||||
rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => {
|
rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => {
|
||||||
const { layerId, ipAdapter } = action.payload;
|
const { layerId, ipAdapter } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer);
|
||||||
layer.ipAdapters.push(ipAdapter);
|
layer.ipAdapters.push(ipAdapter);
|
||||||
},
|
},
|
||||||
rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => {
|
rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => {
|
||||||
const { layerId, ipAdapterId } = action.payload;
|
const { layerId, ipAdapterId } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer);
|
||||||
layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId);
|
layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId);
|
||||||
},
|
},
|
||||||
rgLayerIPAdapterImageChanged: (
|
rgLayerIPAdapterImageChanged: (
|
||||||
@ -726,20 +592,15 @@ export const controlLayersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
const layer = selectIILayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isInitialImageLayer);
|
||||||
layer.bbox = null;
|
layer.bbox = null;
|
||||||
layer.bboxNeedsUpdate = true;
|
layer.bboxNeedsUpdate = true;
|
||||||
layer.isEnabled = true;
|
layer.isEnabled = true;
|
||||||
layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
||||||
},
|
},
|
||||||
iiLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => {
|
|
||||||
const { layerId, opacity } = action.payload;
|
|
||||||
const layer = selectIILayerOrThrow(state, layerId);
|
|
||||||
layer.opacity = opacity;
|
|
||||||
},
|
|
||||||
iiLayerDenoisingStrengthChanged: (state, action: PayloadAction<{ layerId: string; denoisingStrength: number }>) => {
|
iiLayerDenoisingStrengthChanged: (state, action: PayloadAction<{ layerId: string; denoisingStrength: number }>) => {
|
||||||
const { layerId, denoisingStrength } = action.payload;
|
const { layerId, denoisingStrength } = action.payload;
|
||||||
const layer = selectIILayerOrThrow(state, layerId);
|
const layer = selectLayerOrThrow(state, layerId, isInitialImageLayer);
|
||||||
layer.denoisingStrength = denoisingStrength;
|
layer.denoisingStrength = denoisingStrength;
|
||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -765,10 +626,105 @@ export const controlLayersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
prepare: () => ({ payload: { layerId: uuidv4() } }),
|
prepare: () => ({ payload: { layerId: uuidv4() } }),
|
||||||
},
|
},
|
||||||
rasterLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => {
|
//#endregion
|
||||||
const { layerId, opacity } = action.payload;
|
|
||||||
const layer = selectRasterLayerOrThrow(state, layerId);
|
//#region Objects
|
||||||
layer.opacity = opacity;
|
brushLineAdded: {
|
||||||
|
reducer: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<
|
||||||
|
AddBrushLineArg & {
|
||||||
|
lineUuid: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { layerId, points, lineUuid, color } = action.payload;
|
||||||
|
const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer);
|
||||||
|
layer.objects.push({
|
||||||
|
id: getBrushLineId(layer.id, lineUuid),
|
||||||
|
type: 'brush_line',
|
||||||
|
// Points must be offset by the layer's x and y coordinates
|
||||||
|
// TODO: Handle this in the event listener?
|
||||||
|
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
||||||
|
strokeWidth: state.brushSize,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
if (layer.type === 'regional_guidance_layer') {
|
||||||
|
layer.uploadedMaskImage = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload: AddBrushLineArg) => ({
|
||||||
|
payload: { ...payload, lineUuid: uuidv4() },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
eraserLineAdded: {
|
||||||
|
reducer: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<
|
||||||
|
AddEraserLineArg & {
|
||||||
|
lineUuid: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { layerId, points, lineUuid } = action.payload;
|
||||||
|
const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer);
|
||||||
|
layer.objects.push({
|
||||||
|
id: getEraserLineId(layer.id, lineUuid),
|
||||||
|
type: 'eraser_line',
|
||||||
|
// Points must be offset by the layer's x and y coordinates
|
||||||
|
// TODO: Handle this in the event listener?
|
||||||
|
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
||||||
|
strokeWidth: state.brushSize,
|
||||||
|
});
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
|
layer.uploadedMaskImage = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload: AddEraserLineArg) => ({
|
||||||
|
payload: { ...payload, lineUuid: uuidv4() },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
linePointsAdded: (state, action: PayloadAction<AddPointToLineArg>) => {
|
||||||
|
const { layerId, point } = action.payload;
|
||||||
|
const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer);
|
||||||
|
const lastLine = layer.objects.findLast(isLine);
|
||||||
|
if (!lastLine || !isLine(lastLine)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Points must be offset by the layer's x and y coordinates
|
||||||
|
// TODO: Handle this in the event listener
|
||||||
|
lastLine.points.push(point[0] - layer.x, point[1] - layer.y);
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
|
layer.uploadedMaskImage = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rectAdded: {
|
||||||
|
reducer: (state, action: PayloadAction<AddRectShapeArg & { rectUuid: string }>) => {
|
||||||
|
const { layerId, rect, rectUuid, color } = action.payload;
|
||||||
|
if (rect.height === 0 || rect.width === 0) {
|
||||||
|
// Ignore zero-area rectangles
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer);
|
||||||
|
const id = getRectId(layer.id, rectUuid);
|
||||||
|
layer.objects.push({
|
||||||
|
type: 'rect_shape',
|
||||||
|
id,
|
||||||
|
x: rect.x - layer.x,
|
||||||
|
y: rect.y - layer.y,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
|
layer.uploadedMaskImage = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }),
|
||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
@ -898,6 +854,7 @@ export const {
|
|||||||
layerBboxChanged,
|
layerBboxChanged,
|
||||||
layerReset,
|
layerReset,
|
||||||
layerDeleted,
|
layerDeleted,
|
||||||
|
layerOpacityChanged,
|
||||||
layerMovedForward,
|
layerMovedForward,
|
||||||
layerMovedToFront,
|
layerMovedToFront,
|
||||||
layerMovedBackward,
|
layerMovedBackward,
|
||||||
@ -913,7 +870,6 @@ export const {
|
|||||||
caLayerControlModeChanged,
|
caLayerControlModeChanged,
|
||||||
caLayerProcessorConfigChanged,
|
caLayerProcessorConfigChanged,
|
||||||
caLayerIsFilterEnabledChanged,
|
caLayerIsFilterEnabledChanged,
|
||||||
caLayerOpacityChanged,
|
|
||||||
caLayerProcessorPendingBatchIdChanged,
|
caLayerProcessorPendingBatchIdChanged,
|
||||||
// IPA Layers
|
// IPA Layers
|
||||||
ipaLayerAdded,
|
ipaLayerAdded,
|
||||||
@ -949,11 +905,9 @@ export const {
|
|||||||
iiLayerAdded,
|
iiLayerAdded,
|
||||||
iiLayerRecalled,
|
iiLayerRecalled,
|
||||||
iiLayerImageChanged,
|
iiLayerImageChanged,
|
||||||
iiLayerOpacityChanged,
|
|
||||||
iiLayerDenoisingStrengthChanged,
|
iiLayerDenoisingStrengthChanged,
|
||||||
// Raster layers
|
// Raster layers
|
||||||
rasterLayerAdded,
|
rasterLayerAdded,
|
||||||
rasterLayerOpacityChanged,
|
|
||||||
// Globals
|
// Globals
|
||||||
positivePromptChanged,
|
positivePromptChanged,
|
||||||
negativePromptChanged,
|
negativePromptChanged,
|
||||||
@ -1053,6 +1007,15 @@ export const controlLayersUndoableConfig: UndoableOptions<ControlLayersState, Un
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
filter: (action, _state, _history) => {
|
filter: (action, _state, _history) => {
|
||||||
return false;
|
// // Ignore all actions from other slices
|
||||||
|
// if (!action.type.startsWith(controlLayersSlice.name)) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// // This action is triggered on state changes, including when we undo. If we do not ignore this action, when we
|
||||||
|
// // undo, this action triggers and empties the future states array. Therefore, we must ignore this action.
|
||||||
|
// if (layerBboxChanged.match(action)) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,6 @@ import { z } from 'zod';
|
|||||||
const zTool = z.enum(['brush', 'eraser', 'move', 'rect']);
|
const zTool = z.enum(['brush', 'eraser', 'move', 'rect']);
|
||||||
export type Tool = z.infer<typeof zTool>;
|
export type Tool = z.infer<typeof zTool>;
|
||||||
const zDrawingTool = zTool.extract(['brush', 'eraser']);
|
const zDrawingTool = zTool.extract(['brush', 'eraser']);
|
||||||
export type DrawingTool = z.infer<typeof zDrawingTool>;
|
|
||||||
|
|
||||||
const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, {
|
const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, {
|
||||||
message: 'Must have an even number of points',
|
message: 'Must have an even number of points',
|
||||||
@ -118,6 +117,16 @@ const zImageObject = z.object({
|
|||||||
});
|
});
|
||||||
export type ImageObject = z.infer<typeof zImageObject>;
|
export type ImageObject = z.infer<typeof zImageObject>;
|
||||||
|
|
||||||
|
const zAnyLayerObject = z.discriminatedUnion('type', [
|
||||||
|
zImageObject,
|
||||||
|
zBrushLine,
|
||||||
|
zEraserline,
|
||||||
|
zRectShape,
|
||||||
|
zEllipseShape,
|
||||||
|
zPolygonShape,
|
||||||
|
]);
|
||||||
|
export type AnyLayerObject = z.infer<typeof zAnyLayerObject>;
|
||||||
|
|
||||||
const zLayerBase = z.object({
|
const zLayerBase = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
isEnabled: z.boolean().default(true),
|
isEnabled: z.boolean().default(true),
|
||||||
@ -140,9 +149,7 @@ const zRenderableLayerBase = zLayerBase.extend({
|
|||||||
const zRasterLayer = zRenderableLayerBase.extend({
|
const zRasterLayer = zRenderableLayerBase.extend({
|
||||||
type: z.literal('raster_layer'),
|
type: z.literal('raster_layer'),
|
||||||
opacity: zOpacity,
|
opacity: zOpacity,
|
||||||
objects: z.array(
|
objects: z.array(zAnyLayerObject),
|
||||||
z.discriminatedUnion('type', [zImageObject, zBrushLine, zEraserline, zRectShape, zEllipseShape, zPolygonShape])
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
export type RasterLayer = z.infer<typeof zRasterLayer>;
|
export type RasterLayer = z.infer<typeof zRasterLayer>;
|
||||||
|
|
||||||
@ -213,6 +220,7 @@ const zRegionalGuidanceLayer = zRenderableLayerBase.extend({
|
|||||||
autoNegative: zAutoNegative,
|
autoNegative: zAutoNegative,
|
||||||
uploadedMaskImage: zImageWithDims.nullable(),
|
uploadedMaskImage: zImageWithDims.nullable(),
|
||||||
});
|
});
|
||||||
|
// TODO(psyche): This doesn't migrate correctly!
|
||||||
const zRGLayer = z
|
const zRGLayer = z
|
||||||
.union([zOLD_RegionalGuidanceLayer, zRegionalGuidanceLayer])
|
.union([zOLD_RegionalGuidanceLayer, zRegionalGuidanceLayer])
|
||||||
.transform((val) => {
|
.transform((val) => {
|
||||||
@ -265,4 +273,46 @@ export type ControlLayersState = {
|
|||||||
export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] };
|
export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] };
|
||||||
export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor };
|
export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor };
|
||||||
export type AddPointToLineArg = { layerId: string; point: [number, number] };
|
export type AddPointToLineArg = { layerId: string; point: [number, number] };
|
||||||
export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor };
|
export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor }; //#region Type guards
|
||||||
|
|
||||||
|
//#region Type guards
|
||||||
|
export const isLine = (obj: AnyLayerObject): obj is BrushLine | EraserLine => {
|
||||||
|
return obj.type === 'brush_line' || obj.type === 'eraser_line';
|
||||||
|
};
|
||||||
|
export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer => {
|
||||||
|
return layer?.type === 'regional_guidance_layer';
|
||||||
|
};
|
||||||
|
export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => {
|
||||||
|
return layer?.type === 'control_adapter_layer';
|
||||||
|
};
|
||||||
|
export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => {
|
||||||
|
return layer?.type === 'ip_adapter_layer';
|
||||||
|
};
|
||||||
|
export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => {
|
||||||
|
return layer?.type === 'initial_image_layer';
|
||||||
|
};
|
||||||
|
export const isRasterLayer = (layer?: Layer): layer is RasterLayer => {
|
||||||
|
return layer?.type === 'raster_layer';
|
||||||
|
};
|
||||||
|
export const isRenderableLayer = (
|
||||||
|
layer?: Layer
|
||||||
|
): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer => {
|
||||||
|
return (
|
||||||
|
layer?.type === 'regional_guidance_layer' ||
|
||||||
|
layer?.type === 'control_adapter_layer' ||
|
||||||
|
layer?.type === 'initial_image_layer' ||
|
||||||
|
layer?.type === 'raster_layer'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const isLayerWithOpacity = (layer?: Layer): layer is ControlAdapterLayer | InitialImageLayer | RasterLayer => {
|
||||||
|
return (
|
||||||
|
layer?.type === 'control_adapter_layer' || layer?.type === 'initial_image_layer' || layer?.type === 'raster_layer'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const isCAOrIPALayer = (layer?: Layer): layer is ControlAdapterLayer | IPAdapterLayer => {
|
||||||
|
return isControlAdapterLayer(layer) || isIPAdapterLayer(layer);
|
||||||
|
};
|
||||||
|
export const isRGOrRasterlayer = (layer?: Layer): layer is RegionalGuidanceLayer | RasterLayer => {
|
||||||
|
return isRegionalGuidanceLayer(layer) || isRasterLayer(layer);
|
||||||
|
};
|
||||||
|
//#endregion
|
||||||
|
@ -7,14 +7,14 @@ import {
|
|||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import type { ControlAdaptersState } from 'features/controlAdapters/store/types';
|
import type { ControlAdaptersState } from 'features/controlAdapters/store/types';
|
||||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||||
|
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import type { ControlLayersState } from 'features/controlLayers/store/types';
|
||||||
import {
|
import {
|
||||||
isControlAdapterLayer,
|
isControlAdapterLayer,
|
||||||
isInitialImageLayer,
|
isInitialImageLayer,
|
||||||
isIPAdapterLayer,
|
isIPAdapterLayer,
|
||||||
isRegionalGuidanceLayer,
|
isRegionalGuidanceLayer,
|
||||||
selectControlLayersSlice,
|
} from 'features/controlLayers/store/types';
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import type { ControlLayersState } from 'features/controlLayers/store/types';
|
|
||||||
import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
import type { NodesState } from 'features/nodes/store/types';
|
import type { NodesState } from 'features/nodes/store/types';
|
||||||
|
@ -4,15 +4,15 @@ import { deepClone } from 'common/util/deepClone';
|
|||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||||
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||||
import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming';
|
import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming';
|
||||||
import { renderers } from 'features/controlLayers/konva/renderers';
|
import { renderers } from 'features/controlLayers/konva/renderers/layers';
|
||||||
|
import { rgLayerMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
import {
|
import {
|
||||||
isControlAdapterLayer,
|
isControlAdapterLayer,
|
||||||
isInitialImageLayer,
|
isInitialImageLayer,
|
||||||
isIPAdapterLayer,
|
isIPAdapterLayer,
|
||||||
isRegionalGuidanceLayer,
|
isRegionalGuidanceLayer,
|
||||||
rgLayerMaskImageUploaded,
|
} from 'features/controlLayers/store/types';
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
|
||||||
import type {
|
import type {
|
||||||
ControlNetConfigV2,
|
ControlNetConfigV2,
|
||||||
ImageWithDims,
|
ImageWithDims,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice';
|
import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
||||||
import {
|
import {
|
||||||
CLIP_SKIP,
|
CLIP_SKIP,
|
||||||
|
Loading…
Reference in New Issue
Block a user