feat(ui): use IAIDndImage for compare mode

This commit is contained in:
psychedelicious 2024-05-31 18:07:39 +10:00
parent 4ef8cbd9d0
commit 0da36c1238
4 changed files with 54 additions and 51 deletions

View File

@ -1,32 +1,31 @@
import type { UseMeasureRect } from '@reactuses/core';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import IAIDroppable from 'common/components/IAIDroppable';
import type { SelectForCompareDropData } from 'features/dnd/types';
import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide';
import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider';
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = {
containerSize: UseMeasureRect;
};
const selector = createMemoizedSelector(selectGallerySlice, (gallerySlice) => {
const firstImage = gallerySlice.selection.slice(-1)[0] ?? null;
const secondImage = gallerySlice.imageToCompare;
return { firstImage, secondImage };
});
export const ImageComparison = memo(({ containerSize }: Props) => {
export const ImageComparison = memo(() => {
const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
const { firstImage, secondImage } = useAppSelector((s) => {
const firstImage = s.gallery.selection.slice(-1)[0] ?? null;
const secondImage = s.gallery.imageToCompare;
return { firstImage, secondImage };
});
const { firstImage, secondImage } = useAppSelector(selector);
if (!firstImage || !secondImage) {
return <ImageComparisonWrapper>No images to compare</ImageComparisonWrapper>;
return <ImageComparisonWrapper>Select an image to compare</ImageComparisonWrapper>;
}
if (comparisonMode === 'slider') {
return (
<ImageComparisonWrapper>
<ImageComparisonSlider containerSize={containerSize} firstImage={firstImage} secondImage={secondImage} />
<ImageComparisonSlider firstImage={firstImage} secondImage={secondImage} />
</ImageComparisonWrapper>
);
}

View File

@ -1,7 +1,8 @@
import { Flex, Image } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import IAIDndImage from 'common/components/IAIDndImage';
import type { ImageDraggableData } from 'features/dnd/types';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { memo, useCallback, useMemo, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
import type { ImageDTO } from 'services/api/types';
@ -18,7 +19,6 @@ type Props = {
};
export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Props) => {
const { t } = useTranslation();
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const onDoubleClickHandle = useCallback(() => {
if (!panelGroupRef.current) {
@ -27,21 +27,31 @@ export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Prop
panelGroupRef.current.setLayout([50, 50]);
}, []);
const firstImageDraggableData = useMemo<ImageDraggableData>(
() => ({
id: 'image-compare-first-image',
payloadType: 'IMAGE_DTO',
payload: { imageDTO: firstImage },
}),
[firstImage]
);
const secondImageDraggableData = useMemo<ImageDraggableData>(
() => ({
id: 'image-compare-second-image',
payloadType: 'IMAGE_DTO',
payload: { imageDTO: secondImage },
}),
[secondImage]
);
return (
<Flex w="full" h="full" maxW="full" maxH="full" position="relative" alignItems="center" justifyContent="center">
<Flex w="full" h="full" maxW="full" maxH="full" position="absolute" alignItems="center" justifyContent="center">
<PanelGroup ref={panelGroupRef} direction="horizontal" id="image-comparison-side-by-side">
<Panel minSize={20}>
<Flex w="full" h="full" alignItems="center" justifyContent="center">
<Image
src={firstImage.image_url}
fallbackSrc={firstImage.thumbnail_url}
objectFit="contain"
w={firstImage.width}
h={firstImage.height}
maxW="full"
maxH="full"
/>
<IAIDndImage imageDTO={firstImage} isDropDisabled={true} draggableData={firstImageDraggableData} />
</Flex>
</Panel>
<ResizeHandle
@ -52,15 +62,7 @@ export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Prop
<Panel minSize={20}>
<Flex w="full" h="full" alignItems="center" justifyContent="center">
<Image
src={secondImage.image_url}
fallbackSrc={secondImage.thumbnail_url}
objectFit="contain"
w={secondImage.width}
h={secondImage.height}
maxW="full"
maxH="full"
/>
<IAIDndImage imageDTO={secondImage} isDropDisabled={true} draggableData={secondImageDraggableData} />
</Flex>
</Panel>
</PanelGroup>

View File

@ -1,5 +1,5 @@
import { Box, Flex, Icon, Image, Text } from '@invoke-ai/ui-library';
import type { UseMeasureRect } from '@reactuses/core';
import { useMeasure } from '@reactuses/core';
import type { Dimensions } from 'features/canvas/store/canvasTypes';
import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -25,13 +25,9 @@ type Props = {
* The second image to compare
*/
secondImage: ImageDTO;
/**
* The size of the container, required to fit the component correctly and manage aspect ratios.
*/
containerSize: UseMeasureRect;
};
export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerSize }: Props) => {
export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) => {
const { t } = useTranslation();
// How far the handle is from the left - this will be a CSS calculation that takes into account the handle width
const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX);
@ -40,6 +36,7 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerS
const handleRef = useRef<HTMLDivElement>(null);
// If the container size is not provided, use an internal ref and measure - can cause flicker on mount tho
const containerRef = useRef<HTMLDivElement>(null);
const [containerSize] = useMeasure(containerRef);
// To keep things smooth, we use RAF to update the handle position & gate it to 60fps
const rafRef = useRef<number | null>(null);
const lastMoveTimeRef = useRef<number>(0);
@ -94,13 +91,13 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerS
const targetAspectRatio = containerSize.width / containerSize.height;
const imageAspectRatio = firstImage.width / firstImage.height;
let width: number;
let height: number;
if (firstImage.width <= containerSize.width && firstImage.height <= containerSize.height) {
return { width: firstImage.width, height: firstImage.height };
}
let width: number;
let height: number;
if (imageAspectRatio > targetAspectRatio) {
// Image is wider than container's aspect ratio
width = containerSize.width;
@ -123,7 +120,16 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerS
);
return (
<Flex w="full" h="full" maxW="full" maxH="full" position="relative" alignItems="center" justifyContent="center">
<Flex
ref={containerRef}
w="full"
h="full"
maxW="full"
maxH="full"
position="relative"
alignItems="center"
justifyContent="center"
>
<Flex
id="image-comparison-container"
w="full"
@ -135,7 +141,6 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerS
justifyContent="center"
>
<Box
ref={containerRef}
position="relative"
id="image-comparison-second-image-container"
w={fittedSize.width}

View File

@ -1,5 +1,4 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useMeasure } from '@reactuses/core';
import { useAppSelector } from 'app/store/storeHooks';
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
@ -9,7 +8,7 @@ import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/To
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useMemo, useRef } from 'react';
import { memo, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import CurrentImageButtons from './CurrentImageButtons';
@ -21,8 +20,6 @@ export const ImageViewer = memo(() => {
const { viewerMode, onToggle, openEditor } = useImageViewer();
const activeTabName = useAppSelector(activeTabNameSelector);
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
const containerRef = useRef<HTMLDivElement>(null);
const [containerSize] = useMeasure(containerRef);
const shouldShowViewer = useMemo(() => {
if (!isViewerEnabled) {
return false;
@ -70,9 +67,9 @@ export const ImageViewer = memo(() => {
</Flex>
</Flex>
</Flex>
<Box ref={containerRef} w="full" h="full">
<Box w="full" h="full">
{viewerMode === 'view' && <CurrentImagePreview />}
{viewerMode === 'compare' && <ImageComparison containerSize={containerSize} />}
{viewerMode === 'compare' && <ImageComparison />}
</Box>
</Flex>
);