mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): use IAIDndImage for compare mode
This commit is contained in:
parent
4ef8cbd9d0
commit
0da36c1238
@ -1,32 +1,31 @@
|
|||||||
import type { UseMeasureRect } from '@reactuses/core';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIDroppable from 'common/components/IAIDroppable';
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
import type { SelectForCompareDropData } from 'features/dnd/types';
|
import type { SelectForCompareDropData } from 'features/dnd/types';
|
||||||
import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide';
|
import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide';
|
||||||
import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider';
|
import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider';
|
||||||
|
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
type Props = {
|
const selector = createMemoizedSelector(selectGallerySlice, (gallerySlice) => {
|
||||||
containerSize: UseMeasureRect;
|
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 comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
|
||||||
const { firstImage, secondImage } = useAppSelector((s) => {
|
const { firstImage, secondImage } = useAppSelector(selector);
|
||||||
const firstImage = s.gallery.selection.slice(-1)[0] ?? null;
|
|
||||||
const secondImage = s.gallery.imageToCompare;
|
|
||||||
return { firstImage, secondImage };
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!firstImage || !secondImage) {
|
if (!firstImage || !secondImage) {
|
||||||
return <ImageComparisonWrapper>No images to compare</ImageComparisonWrapper>;
|
return <ImageComparisonWrapper>Select an image to compare</ImageComparisonWrapper>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comparisonMode === 'slider') {
|
if (comparisonMode === 'slider') {
|
||||||
return (
|
return (
|
||||||
<ImageComparisonWrapper>
|
<ImageComparisonWrapper>
|
||||||
<ImageComparisonSlider containerSize={containerSize} firstImage={firstImage} secondImage={secondImage} />
|
<ImageComparisonSlider firstImage={firstImage} secondImage={secondImage} />
|
||||||
</ImageComparisonWrapper>
|
</ImageComparisonWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
@ -18,7 +19,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Props) => {
|
export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Props) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||||
const onDoubleClickHandle = useCallback(() => {
|
const onDoubleClickHandle = useCallback(() => {
|
||||||
if (!panelGroupRef.current) {
|
if (!panelGroupRef.current) {
|
||||||
@ -27,21 +27,31 @@ export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Prop
|
|||||||
panelGroupRef.current.setLayout([50, 50]);
|
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 (
|
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="relative" alignItems="center" justifyContent="center">
|
||||||
<Flex w="full" h="full" maxW="full" maxH="full" position="absolute" 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">
|
<PanelGroup ref={panelGroupRef} direction="horizontal" id="image-comparison-side-by-side">
|
||||||
<Panel minSize={20}>
|
<Panel minSize={20}>
|
||||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||||
<Image
|
<IAIDndImage imageDTO={firstImage} isDropDisabled={true} draggableData={firstImageDraggableData} />
|
||||||
src={firstImage.image_url}
|
|
||||||
fallbackSrc={firstImage.thumbnail_url}
|
|
||||||
objectFit="contain"
|
|
||||||
w={firstImage.width}
|
|
||||||
h={firstImage.height}
|
|
||||||
maxW="full"
|
|
||||||
maxH="full"
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Panel>
|
</Panel>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
@ -52,15 +62,7 @@ export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Prop
|
|||||||
|
|
||||||
<Panel minSize={20}>
|
<Panel minSize={20}>
|
||||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||||
<Image
|
<IAIDndImage imageDTO={secondImage} isDropDisabled={true} draggableData={secondImageDraggableData} />
|
||||||
src={secondImage.image_url}
|
|
||||||
fallbackSrc={secondImage.thumbnail_url}
|
|
||||||
objectFit="contain"
|
|
||||||
w={secondImage.width}
|
|
||||||
h={secondImage.height}
|
|
||||||
maxW="full"
|
|
||||||
maxH="full"
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Panel>
|
</Panel>
|
||||||
</PanelGroup>
|
</PanelGroup>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Box, Flex, Icon, Image, Text } from '@invoke-ai/ui-library';
|
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 type { Dimensions } from 'features/canvas/store/canvasTypes';
|
||||||
import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers';
|
import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
@ -25,13 +25,9 @@ type Props = {
|
|||||||
* The second image to compare
|
* The second image to compare
|
||||||
*/
|
*/
|
||||||
secondImage: ImageDTO;
|
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();
|
const { t } = useTranslation();
|
||||||
// How far the handle is from the left - this will be a CSS calculation that takes into account the handle width
|
// 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);
|
const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX);
|
||||||
@ -40,6 +36,7 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerS
|
|||||||
const handleRef = useRef<HTMLDivElement>(null);
|
const handleRef = useRef<HTMLDivElement>(null);
|
||||||
// If the container size is not provided, use an internal ref and measure - can cause flicker on mount tho
|
// 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 containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [containerSize] = useMeasure(containerRef);
|
||||||
// To keep things smooth, we use RAF to update the handle position & gate it to 60fps
|
// To keep things smooth, we use RAF to update the handle position & gate it to 60fps
|
||||||
const rafRef = useRef<number | null>(null);
|
const rafRef = useRef<number | null>(null);
|
||||||
const lastMoveTimeRef = useRef<number>(0);
|
const lastMoveTimeRef = useRef<number>(0);
|
||||||
@ -94,13 +91,13 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerS
|
|||||||
const targetAspectRatio = containerSize.width / containerSize.height;
|
const targetAspectRatio = containerSize.width / containerSize.height;
|
||||||
const imageAspectRatio = firstImage.width / firstImage.height;
|
const imageAspectRatio = firstImage.width / firstImage.height;
|
||||||
|
|
||||||
|
let width: number;
|
||||||
|
let height: number;
|
||||||
|
|
||||||
if (firstImage.width <= containerSize.width && firstImage.height <= containerSize.height) {
|
if (firstImage.width <= containerSize.width && firstImage.height <= containerSize.height) {
|
||||||
return { width: firstImage.width, height: firstImage.height };
|
return { width: firstImage.width, height: firstImage.height };
|
||||||
}
|
}
|
||||||
|
|
||||||
let width: number;
|
|
||||||
let height: number;
|
|
||||||
|
|
||||||
if (imageAspectRatio > targetAspectRatio) {
|
if (imageAspectRatio > targetAspectRatio) {
|
||||||
// Image is wider than container's aspect ratio
|
// Image is wider than container's aspect ratio
|
||||||
width = containerSize.width;
|
width = containerSize.width;
|
||||||
@ -123,7 +120,16 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerS
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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
|
<Flex
|
||||||
id="image-comparison-container"
|
id="image-comparison-container"
|
||||||
w="full"
|
w="full"
|
||||||
@ -135,7 +141,6 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerS
|
|||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
ref={containerRef}
|
|
||||||
position="relative"
|
position="relative"
|
||||||
id="image-comparison-second-image-container"
|
id="image-comparison-second-image-container"
|
||||||
w={fittedSize.width}
|
w={fittedSize.width}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||||
import { useMeasure } from '@reactuses/core';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
||||||
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
|
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 { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
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 { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
import CurrentImageButtons from './CurrentImageButtons';
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
@ -21,8 +20,6 @@ export const ImageViewer = memo(() => {
|
|||||||
const { viewerMode, onToggle, openEditor } = useImageViewer();
|
const { viewerMode, onToggle, openEditor } = useImageViewer();
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
|
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [containerSize] = useMeasure(containerRef);
|
|
||||||
const shouldShowViewer = useMemo(() => {
|
const shouldShowViewer = useMemo(() => {
|
||||||
if (!isViewerEnabled) {
|
if (!isViewerEnabled) {
|
||||||
return false;
|
return false;
|
||||||
@ -70,9 +67,9 @@ export const ImageViewer = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box ref={containerRef} w="full" h="full">
|
<Box w="full" h="full">
|
||||||
{viewerMode === 'view' && <CurrentImagePreview />}
|
{viewerMode === 'view' && <CurrentImagePreview />}
|
||||||
{viewerMode === 'compare' && <ImageComparison containerSize={containerSize} />}
|
{viewerMode === 'compare' && <ImageComparison />}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user