mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
Merge remote-tracking branch 'origin/main' into ryan/sd3-image-to-image
This commit is contained in:
@@ -1,38 +1,26 @@
|
||||
import { Grid, GridItem } from '@invoke-ai/ui-library';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import type {
|
||||
AddControlLayerFromImageDropData,
|
||||
AddGlobalReferenceImageFromImageDropData,
|
||||
AddRasterLayerFromImageDropData,
|
||||
AddRegionalReferenceImageFromImageDropData,
|
||||
} from 'features/dnd/types';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { newCanvasEntityFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const addRasterLayerFromImageDropData: AddRasterLayerFromImageDropData = {
|
||||
id: 'add-raster-layer-from-image-drop-data',
|
||||
actionType: 'ADD_RASTER_LAYER_FROM_IMAGE',
|
||||
};
|
||||
|
||||
const addControlLayerFromImageDropData: AddControlLayerFromImageDropData = {
|
||||
id: 'add-control-layer-from-image-drop-data',
|
||||
actionType: 'ADD_CONTROL_LAYER_FROM_IMAGE',
|
||||
};
|
||||
|
||||
const addRegionalReferenceImageFromImageDropData: AddRegionalReferenceImageFromImageDropData = {
|
||||
id: 'add-control-layer-from-image-drop-data',
|
||||
actionType: 'ADD_REGIONAL_REFERENCE_IMAGE_FROM_IMAGE',
|
||||
};
|
||||
|
||||
const addGlobalReferenceImageFromImageDropData: AddGlobalReferenceImageFromImageDropData = {
|
||||
id: 'add-control-layer-from-image-drop-data',
|
||||
actionType: 'ADD_GLOBAL_REFERENCE_IMAGE_FROM_IMAGE',
|
||||
};
|
||||
const addRasterLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({ type: 'raster_layer' });
|
||||
const addControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
|
||||
type: 'control_layer',
|
||||
});
|
||||
const addRegionalGuidanceReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
|
||||
type: 'regional_guidance_with_reference_image',
|
||||
});
|
||||
const addGlobalReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
|
||||
type: 'reference_image',
|
||||
});
|
||||
|
||||
export const CanvasDropArea = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageViewer = useImageViewer();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
if (imageViewer.isOpen) {
|
||||
return null;
|
||||
@@ -51,28 +39,36 @@ export const CanvasDropArea = memo(() => {
|
||||
pointerEvents="none"
|
||||
>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable
|
||||
dropLabel={t('controlLayers.canvasContextMenu.newRasterLayer')}
|
||||
data={addRasterLayerFromImageDropData}
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasEntityFromImageDndTarget}
|
||||
dndTargetData={addRasterLayerFromImageDndTargetData}
|
||||
label={t('controlLayers.canvasContextMenu.newRasterLayer')}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable
|
||||
dropLabel={t('controlLayers.canvasContextMenu.newControlLayer')}
|
||||
data={addControlLayerFromImageDropData}
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasEntityFromImageDndTarget}
|
||||
dndTargetData={addControlLayerFromImageDndTargetData}
|
||||
label={t('controlLayers.canvasContextMenu.newControlLayer')}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable
|
||||
dropLabel={t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
|
||||
data={addRegionalReferenceImageFromImageDropData}
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasEntityFromImageDndTarget}
|
||||
dndTargetData={addRegionalGuidanceReferenceImageFromImageDndTargetData}
|
||||
label={t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable
|
||||
dropLabel={t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
|
||||
data={addGlobalReferenceImageFromImageDropData}
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasEntityFromImageDndTarget}
|
||||
dndTargetData={addGlobalReferenceImageFromImageDndTargetData}
|
||||
label={t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useCanvasEntityListDnd } from 'features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected';
|
||||
import { entitySelected } from 'features/controlLayers/store/canvasSlice';
|
||||
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
|
||||
const sx = {
|
||||
position: 'relative',
|
||||
flexDir: 'column',
|
||||
w: 'full',
|
||||
bg: 'base.850',
|
||||
borderRadius: 'base',
|
||||
'&[data-selected=true]': {
|
||||
bg: 'base.800',
|
||||
},
|
||||
'&[data-is-dragging=true]': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
transitionProperty: 'common',
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
export const CanvasEntityContainer = memo((props: PropsWithChildren) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const isSelected = useEntityIsSelected(entityIdentifier);
|
||||
const onClick = useCallback(() => {
|
||||
if (isSelected) {
|
||||
return;
|
||||
}
|
||||
dispatch(entitySelected({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier, isSelected]);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [dndListState, isDragging] = useCanvasEntityListDnd(ref, entityIdentifier);
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<Flex
|
||||
// This is used to trigger the post-move flash animation
|
||||
data-entity-id={entityIdentifier.id}
|
||||
data-selected={isSelected}
|
||||
data-is-dragging={isDragging}
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
sx={sx}
|
||||
>
|
||||
{props.children}
|
||||
</Flex>
|
||||
<DndListDropIndicator dndState={dndListState} />
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityContainer.displayName = 'CanvasEntityContainer';
|
||||
@@ -0,0 +1,181 @@
|
||||
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import { reorderWithEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge';
|
||||
import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { useBoolean } from 'common/hooks/useBoolean';
|
||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||
import { fixTooltipCloseOnScrollStyles } from 'common/util/fixTooltipCloseOnScrollStyles';
|
||||
import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton';
|
||||
import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/components/common/CanvasEntityMergeVisibleButton';
|
||||
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
|
||||
import { useEntityTypeInformationalPopover } from 'features/controlLayers/hooks/useEntityTypeInformationalPopover';
|
||||
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
|
||||
import { entitiesReordered } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { isRenderableEntityType } from 'features/controlLayers/store/types';
|
||||
import { singleCanvasEntityDndSource } from 'features/dnd/dnd';
|
||||
import { triggerPostMoveFlash } from 'features/dnd/util';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { PiCaretDownBold } from 'react-icons/pi';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
isSelected: boolean;
|
||||
type: CanvasEntityIdentifier['type'];
|
||||
entityIdentifiers: CanvasEntityIdentifier[];
|
||||
}>;
|
||||
|
||||
export const CanvasEntityGroupList = memo(({ isSelected, type, children, entityIdentifiers }: Props) => {
|
||||
const title = useEntityTypeTitle(type);
|
||||
const informationalPopoverFeature = useEntityTypeInformationalPopover(type);
|
||||
const collapse = useBoolean(true);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
return monitorForElements({
|
||||
canMonitor({ source }) {
|
||||
if (!singleCanvasEntityDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
if (source.data.payload.entityIdentifier.type !== type) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onDrop({ location, source }) {
|
||||
const target = location.current.dropTargets[0];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceData = source.data;
|
||||
const targetData = target.data;
|
||||
|
||||
if (!singleCanvasEntityDndSource.typeGuard(sourceData) || !singleCanvasEntityDndSource.typeGuard(targetData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexOfSource = entityIdentifiers.findIndex(
|
||||
(entityIdentifier) => entityIdentifier.id === sourceData.payload.entityIdentifier.id
|
||||
);
|
||||
const indexOfTarget = entityIdentifiers.findIndex(
|
||||
(entityIdentifier) => entityIdentifier.id === targetData.payload.entityIdentifier.id
|
||||
);
|
||||
|
||||
if (indexOfTarget < 0 || indexOfSource < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't move if the source and target are the same index, meaning same position in the list
|
||||
if (indexOfSource === indexOfTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closestEdgeOfTarget = extractClosestEdge(targetData);
|
||||
|
||||
// It's possible that the indices are different, but refer to the same position. For example, if the source is
|
||||
// at 2 and the target is at 3, but the target edge is 'top', then the entity is already in the correct position.
|
||||
// We should bail if this is the case.
|
||||
let edgeIndexDelta = 0;
|
||||
|
||||
if (closestEdgeOfTarget === 'bottom') {
|
||||
edgeIndexDelta = 1;
|
||||
} else if (closestEdgeOfTarget === 'top') {
|
||||
edgeIndexDelta = -1;
|
||||
}
|
||||
|
||||
// If the source is already in the correct position, we don't need to move it.
|
||||
if (indexOfSource === indexOfTarget + edgeIndexDelta) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Using `flushSync` so we can query the DOM straight after this line
|
||||
flushSync(() => {
|
||||
dispatch(
|
||||
entitiesReordered({
|
||||
type,
|
||||
entityIdentifiers: reorderWithEdge({
|
||||
list: entityIdentifiers,
|
||||
startIndex: indexOfSource,
|
||||
indexOfTarget,
|
||||
closestEdgeOfTarget,
|
||||
axis: 'vertical',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Flash the element that was moved
|
||||
const element = document.querySelector(`[data-entity-id="${sourceData.payload.entityIdentifier.id}"]`);
|
||||
if (element instanceof HTMLElement) {
|
||||
triggerPostMoveFlash(element, colorTokenToCssVar('base.700'));
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [dispatch, entityIdentifiers, type]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full">
|
||||
<Flex w="full">
|
||||
<Flex
|
||||
flexGrow={1}
|
||||
as={Button}
|
||||
onClick={collapse.toggle}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap={3}
|
||||
variant="unstyled"
|
||||
p={0}
|
||||
h={8}
|
||||
>
|
||||
<Icon
|
||||
boxSize={4}
|
||||
as={PiCaretDownBold}
|
||||
transform={collapse.isTrue ? undefined : 'rotate(-90deg)'}
|
||||
fill={isSelected ? 'base.200' : 'base.500'}
|
||||
transitionProperty="common"
|
||||
transitionDuration="fast"
|
||||
/>
|
||||
{informationalPopoverFeature ? (
|
||||
<InformationalPopover feature={informationalPopoverFeature}>
|
||||
<Text
|
||||
fontWeight="semibold"
|
||||
color={isSelected ? 'base.200' : 'base.500'}
|
||||
userSelect="none"
|
||||
transitionProperty="common"
|
||||
transitionDuration="fast"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</InformationalPopover>
|
||||
) : (
|
||||
<Text
|
||||
fontWeight="semibold"
|
||||
color={isSelected ? 'base.200' : 'base.500'}
|
||||
userSelect="none"
|
||||
transitionProperty="common"
|
||||
transitionDuration="fast"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Spacer />
|
||||
</Flex>
|
||||
{isRenderableEntityType(type) && <CanvasEntityMergeVisibleButton type={type} />}
|
||||
{isRenderableEntityType(type) && <CanvasEntityTypeIsHiddenToggle type={type} />}
|
||||
<CanvasEntityAddOfTypeButton type={type} />
|
||||
</Flex>
|
||||
<Collapse in={collapse.isTrue} style={fixTooltipCloseOnScrollStyles}>
|
||||
<Flex flexDir="column" gap={2} pt={2}>
|
||||
{children}
|
||||
</Flex>
|
||||
</Collapse>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityGroupList.displayName = 'CanvasEntityGroupList';
|
||||
@@ -0,0 +1,83 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { singleCanvasEntityDndSource } from 'features/dnd/dnd';
|
||||
import { type DndListTargetState, idle } from 'features/dnd/types';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useCanvasEntityListDnd = (ref: RefObject<HTMLElement>, entityIdentifier: CanvasEntityIdentifier) => {
|
||||
const [dndListState, setDndListState] = useState<DndListTargetState>(idle);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
getInitialData() {
|
||||
return singleCanvasEntityDndSource.getData({ entityIdentifier });
|
||||
},
|
||||
onDragStart() {
|
||||
setDndListState({ type: 'is-dragging' });
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop() {
|
||||
setDndListState(idle);
|
||||
setIsDragging(false);
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop({ source }) {
|
||||
if (!singleCanvasEntityDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
if (source.data.payload.entityIdentifier.type !== entityIdentifier.type) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getData({ input }) {
|
||||
const data = singleCanvasEntityDndSource.getData({ entityIdentifier });
|
||||
return attachClosestEdge(data, {
|
||||
element,
|
||||
input,
|
||||
allowedEdges: ['top', 'bottom'],
|
||||
});
|
||||
},
|
||||
getIsSticky() {
|
||||
return true;
|
||||
},
|
||||
onDragEnter({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
setDndListState({ type: 'is-dragging-over', closestEdge });
|
||||
},
|
||||
onDrag({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
|
||||
// Only need to update react state if nothing has changed.
|
||||
// Prevents re-rendering.
|
||||
setDndListState((current) => {
|
||||
if (current.type === 'is-dragging-over' && current.closestEdge === closestEdge) {
|
||||
return current;
|
||||
}
|
||||
return { type: 'is-dragging-over', closestEdge };
|
||||
});
|
||||
},
|
||||
onDragLeave() {
|
||||
setDndListState(idle);
|
||||
},
|
||||
onDrop() {
|
||||
setDndListState(idle);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [entityIdentifier, ref]);
|
||||
|
||||
return [dndListState, isDragging] as const;
|
||||
};
|
||||
@@ -109,7 +109,9 @@ export const CanvasMainPanelContent = memo(() => {
|
||||
<SelectObject />
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
<CanvasDropArea />
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasDropArea />
|
||||
</CanvasManagerProviderGate>
|
||||
<GatedImageViewer />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { useDndContext } from '@dnd-kit/core';
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||
import { Box, Button, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDropOverlay from 'common/components/IAIDropOverlay';
|
||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectEntityCountActive } from 'features/controlLayers/store/selectors';
|
||||
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasRightPanel = memo(() => {
|
||||
@@ -79,37 +83,13 @@ CanvasRightPanel.displayName = 'CanvasRightPanel';
|
||||
|
||||
const PanelTabs = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const activeTab = useAppSelector(selectActiveTabCanvasRightPanel);
|
||||
const store = useAppStore();
|
||||
const activeEntityCount = useAppSelector(selectEntityCountActive);
|
||||
const tabTimeout = useRef<number | null>(null);
|
||||
const dndCtx = useDndContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const [mouseOverTab, setMouseOverTab] = useState<'layers' | 'gallery' | null>(null);
|
||||
|
||||
const onOnMouseOverLayersTab = useCallback(() => {
|
||||
setMouseOverTab('layers');
|
||||
tabTimeout.current = window.setTimeout(() => {
|
||||
if (dndCtx.active) {
|
||||
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
}
|
||||
}, 300);
|
||||
}, [dndCtx.active, dispatch]);
|
||||
|
||||
const onOnMouseOverGalleryTab = useCallback(() => {
|
||||
setMouseOverTab('gallery');
|
||||
tabTimeout.current = window.setTimeout(() => {
|
||||
if (dndCtx.active) {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}
|
||||
}, 300);
|
||||
}, [dndCtx.active, dispatch]);
|
||||
|
||||
const onMouseOut = useCallback(() => {
|
||||
setMouseOverTab(null);
|
||||
if (tabTimeout.current) {
|
||||
clearTimeout(tabTimeout.current);
|
||||
}
|
||||
}, []);
|
||||
const [layersTabDndState, setLayersTabDndState] = useState<DndTargetState>('idle');
|
||||
const [galleryTabDndState, setGalleryTabDndState] = useState<DndTargetState>('idle');
|
||||
const layersTabRef = useRef<HTMLDivElement>(null);
|
||||
const galleryTabRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
const layersTabLabel = useMemo(() => {
|
||||
if (activeEntityCount === 0) {
|
||||
@@ -118,23 +98,172 @@ const PanelTabs = memo(() => {
|
||||
return `${t('controlLayers.layer_other')} (${activeEntityCount})`;
|
||||
}, [activeEntityCount, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!layersTabRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getIsOnLayersTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'layers';
|
||||
|
||||
const onDragEnter = () => {
|
||||
// If we are already on the layers tab, do nothing
|
||||
if (getIsOnLayersTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Else set the state to active and switch to the layers tab after a timeout
|
||||
setLayersTabDndState('over');
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
// When we switch tabs, the other tab should be pending
|
||||
setLayersTabDndState('idle');
|
||||
setGalleryTabDndState('potential');
|
||||
}, 300);
|
||||
};
|
||||
const onDragLeave = () => {
|
||||
// Set the state to idle or pending depending on the current tab
|
||||
if (getIsOnLayersTab()) {
|
||||
setLayersTabDndState('idle');
|
||||
} else {
|
||||
setLayersTabDndState('potential');
|
||||
}
|
||||
// Abort the tab switch if it hasn't happened yet
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
const onDragStart = () => {
|
||||
// Set the state to pending when a drag starts
|
||||
setLayersTabDndState('potential');
|
||||
};
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element: layersTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: ({ source }) => {
|
||||
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
// Only monitor if we are not already on the gallery tab
|
||||
return !getIsOnLayersTab();
|
||||
},
|
||||
onDragStart,
|
||||
}),
|
||||
dropTargetForExternal({
|
||||
element: layersTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForExternal({
|
||||
canMonitor: () => !getIsOnLayersTab(),
|
||||
onDragStart,
|
||||
})
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!galleryTabRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getIsOnGalleryTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'gallery';
|
||||
|
||||
const onDragEnter = () => {
|
||||
// If we are already on the gallery tab, do nothing
|
||||
if (getIsOnGalleryTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Else set the state to active and switch to the gallery tab after a timeout
|
||||
setGalleryTabDndState('over');
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
// When we switch tabs, the other tab should be pending
|
||||
setGalleryTabDndState('idle');
|
||||
setLayersTabDndState('potential');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
// Set the state to idle or pending depending on the current tab
|
||||
if (getIsOnGalleryTab()) {
|
||||
setGalleryTabDndState('idle');
|
||||
} else {
|
||||
setGalleryTabDndState('potential');
|
||||
}
|
||||
// Abort the tab switch if it hasn't happened yet
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragStart = () => {
|
||||
// Set the state to pending when a drag starts
|
||||
setGalleryTabDndState('potential');
|
||||
};
|
||||
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element: galleryTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: ({ source }) => {
|
||||
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
// Only monitor if we are not already on the gallery tab
|
||||
return !getIsOnGalleryTab();
|
||||
},
|
||||
onDragStart,
|
||||
}),
|
||||
dropTargetForExternal({
|
||||
element: galleryTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForExternal({
|
||||
canMonitor: () => !getIsOnGalleryTab(),
|
||||
onDragStart,
|
||||
})
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
const onDrop = () => {
|
||||
// Reset the dnd state when a drop happens
|
||||
setGalleryTabDndState('idle');
|
||||
setLayersTabDndState('idle');
|
||||
};
|
||||
const cleanup = combine(monitorForElements({ onDrop }), monitorForExternal({ onDrop }));
|
||||
|
||||
return () => {
|
||||
cleanup();
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tab position="relative" onMouseOver={onOnMouseOverLayersTab} onMouseOut={onMouseOut} w={32}>
|
||||
<Tab ref={layersTabRef} position="relative" w={32}>
|
||||
<Box as="span" w="full">
|
||||
{layersTabLabel}
|
||||
</Box>
|
||||
{dndCtx.active && activeTab !== 'layers' && (
|
||||
<IAIDropOverlay isOver={mouseOverTab === 'layers'} withBackdrop={false} />
|
||||
)}
|
||||
<DndDropOverlay dndState={layersTabDndState} withBackdrop={false} />
|
||||
</Tab>
|
||||
<Tab position="relative" onMouseOver={onOnMouseOverGalleryTab} onMouseOut={onMouseOut} w={32}>
|
||||
<Tab ref={galleryTabRef} position="relative" w={32}>
|
||||
<Box as="span" w="full">
|
||||
{t('gallery.gallery')}
|
||||
</Box>
|
||||
{dndCtx.active && activeTab !== 'gallery' && (
|
||||
<IAIDropOverlay isOver={mouseOverTab === 'gallery'} withBackdrop={false} />
|
||||
)}
|
||||
<DndDropOverlay dndState={galleryTabDndState} withBackdrop={false} />
|
||||
</Tab>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
@@ -10,8 +9,11 @@ import { ControlLayerBadges } from 'features/controlLayers/components/ControlLay
|
||||
import { ControlLayerSettings } from 'features/controlLayers/components/ControlLayer/ControlLayerSettings';
|
||||
import { ControlLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import type { ReplaceLayerImageDropData } from 'features/dnd/types';
|
||||
import type { ReplaceCanvasEntityObjectsWithImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { replaceCanvasEntityObjectsWithImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -21,14 +23,16 @@ type Props = {
|
||||
|
||||
export const ControlLayer = memo(({ id }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const entityIdentifier = useMemo<CanvasEntityIdentifier<'control_layer'>>(
|
||||
() => ({ id, type: 'control_layer' }),
|
||||
[id]
|
||||
);
|
||||
const dropData = useMemo<ReplaceLayerImageDropData>(
|
||||
() => ({ id, actionType: 'REPLACE_LAYER_WITH_IMAGE', context: { entityIdentifier } }),
|
||||
[id, entityIdentifier]
|
||||
const dndTargetData = useMemo<ReplaceCanvasEntityObjectsWithImageDndTargetData>(
|
||||
() => replaceCanvasEntityObjectsWithImageDndTarget.getData({ entityIdentifier }, entityIdentifier.id),
|
||||
[entityIdentifier]
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||
<ControlLayerAdapterGate>
|
||||
@@ -43,7 +47,12 @@ export const ControlLayer = memo(({ id }: Props) => {
|
||||
<CanvasEntitySettingsWrapper>
|
||||
<ControlLayerSettings />
|
||||
</CanvasEntitySettingsWrapper>
|
||||
<IAIDroppable data={dropData} dropLabel={t('controlLayers.replaceLayer')} />
|
||||
<DndDropTarget
|
||||
dndTarget={replaceCanvasEntityObjectsWithImageDndTarget}
|
||||
dndTargetData={dndTargetData}
|
||||
label={t('controlLayers.replaceLayer')}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</CanvasEntityContainer>
|
||||
</ControlLayerAdapterGate>
|
||||
</EntityIdentifierContext.Provider>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Flex, IconButton } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
|
||||
import { Weight } from 'features/controlLayers/components/common/Weight';
|
||||
@@ -21,10 +22,11 @@ import { getFilterForModel } from 'features/controlLayers/store/filters';
|
||||
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasEntityIdentifier, ControlModeV2 } from 'features/controlLayers/store/types';
|
||||
import { replaceCanvasEntityObjectsWithImage } from 'features/imageActions/actions';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold, PiShootingStarFill, PiUploadBold } from 'react-icons/pi';
|
||||
import type { ControlNetModelConfig, PostUploadAction, T2IAdapterModelConfig } from 'services/api/types';
|
||||
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
|
||||
|
||||
const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => {
|
||||
const selectControlAdapter = useMemo(
|
||||
@@ -41,7 +43,7 @@ const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier<
|
||||
|
||||
export const ControlLayerControlAdapter = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { dispatch, getState } = useAppStore();
|
||||
const entityIdentifier = useEntityIdentifierContext('control_layer');
|
||||
const controlAdapter = useControlLayerControlAdapter(entityIdentifier);
|
||||
const filter = useEntityFilter(entityIdentifier);
|
||||
@@ -113,11 +115,17 @@ export const ControlLayerControlAdapter = memo(() => {
|
||||
|
||||
const pullBboxIntoLayer = usePullBboxIntoLayer(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const postUploadAction = useMemo<PostUploadAction>(
|
||||
() => ({ type: 'REPLACE_LAYER_WITH_IMAGE', entityIdentifier }),
|
||||
[entityIdentifier]
|
||||
const uploadOptions = useMemo(
|
||||
() =>
|
||||
({
|
||||
onUpload: (imageDTO: ImageDTO) => {
|
||||
replaceCanvasEntityObjectsWithImage({ entityIdentifier, imageDTO, dispatch, getState });
|
||||
},
|
||||
allowMultiple: false,
|
||||
}) as const,
|
||||
[dispatch, entityIdentifier, getState]
|
||||
);
|
||||
const uploadApi = useImageUploadButton({ postUploadAction });
|
||||
const uploadApi = useImageUploadButton(uploadOptions);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={3} position="relative" w="full">
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
||||
import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.controlLayers.entities.map(mapId).reverse();
|
||||
const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.controlLayers.entities.map(getEntityIdentifier).toReversed();
|
||||
});
|
||||
|
||||
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
||||
@@ -17,17 +17,17 @@ const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selecte
|
||||
|
||||
export const ControlLayerEntityList = memo(() => {
|
||||
const isSelected = useAppSelector(selectIsSelected);
|
||||
const layerIds = useAppSelector(selectEntityIds);
|
||||
const entityIdentifiers = useAppSelector(selectEntityIdentifiers);
|
||||
|
||||
if (layerIds.length === 0) {
|
||||
if (entityIdentifiers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (layerIds.length > 0) {
|
||||
if (entityIdentifiers.length > 0) {
|
||||
return (
|
||||
<CanvasEntityGroupList type="control_layer" isSelected={isSelected}>
|
||||
{layerIds.map((id) => (
|
||||
<ControlLayer key={id} id={id} />
|
||||
<CanvasEntityGroupList type="control_layer" isSelected={isSelected} entityIdentifiers={entityIdentifiers}>
|
||||
{entityIdentifiers.map((entityIdentifier) => (
|
||||
<ControlLayer key={entityIdentifier.id} id={entityIdentifier.id} />
|
||||
))}
|
||||
</CanvasEntityGroupList>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { Button, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { replaceCanvasEntityObjectsWithImage } from 'features/imageActions/actions';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import type { PostUploadAction } from 'services/api/types';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const ControlLayerSettingsEmptyState = memo(() => {
|
||||
const entityIdentifier = useEntityIdentifierContext('control_layer');
|
||||
const dispatch = useAppDispatch();
|
||||
const { dispatch, getState } = useAppStore();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const postUploadAction = useMemo<PostUploadAction>(
|
||||
() => ({ type: 'REPLACE_LAYER_WITH_IMAGE', entityIdentifier }),
|
||||
[entityIdentifier]
|
||||
const onUpload = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
replaceCanvasEntityObjectsWithImage({ imageDTO, entityIdentifier, dispatch, getState });
|
||||
},
|
||||
[dispatch, entityIdentifier, getState]
|
||||
);
|
||||
const uploadApi = useImageUploadButton({ postUploadAction });
|
||||
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
|
||||
const onClickGalleryButton = useCallback(() => {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
|
||||
@@ -1,82 +1,80 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
|
||||
import { useNanoid } from 'common/hooks/useNanoid';
|
||||
import { UploadImageButton } from 'common/hooks/useImageUploadButton';
|
||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import type { setGlobalReferenceImageDndTarget, setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO, PostUploadAction } from 'services/api/types';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { $isConnected } from 'services/events/stores';
|
||||
|
||||
type Props = {
|
||||
type Props<T extends typeof setGlobalReferenceImageDndTarget | typeof setRegionalGuidanceReferenceImageDndTarget> = {
|
||||
image: ImageWithDims | null;
|
||||
onChangeImage: (imageDTO: ImageDTO | null) => void;
|
||||
droppableData: TypesafeDroppableData;
|
||||
postUploadAction: PostUploadAction;
|
||||
dndTarget: T;
|
||||
dndTargetData: ReturnType<T['getData']>;
|
||||
};
|
||||
|
||||
export const IPAdapterImagePreview = memo(({ image, onChangeImage, droppableData, postUploadAction }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const isConnected = useStore($isConnected);
|
||||
const dndId = useNanoid('ip_adapter_image_preview');
|
||||
export const IPAdapterImagePreview = memo(
|
||||
<T extends typeof setGlobalReferenceImageDndTarget | typeof setRegionalGuidanceReferenceImageDndTarget>({
|
||||
image,
|
||||
onChangeImage,
|
||||
dndTarget,
|
||||
dndTargetData,
|
||||
}: Props<T>) => {
|
||||
const { t } = useTranslation();
|
||||
const isConnected = useStore($isConnected);
|
||||
const { currentData: imageDTO, isError } = useGetImageDTOQuery(image?.image_name ?? skipToken);
|
||||
const handleResetControlImage = useCallback(() => {
|
||||
onChangeImage(null);
|
||||
}, [onChangeImage]);
|
||||
|
||||
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
|
||||
image?.image_name ?? skipToken
|
||||
);
|
||||
const handleResetControlImage = useCallback(() => {
|
||||
onChangeImage(null);
|
||||
}, [onChangeImage]);
|
||||
useEffect(() => {
|
||||
if (isConnected && isError) {
|
||||
handleResetControlImage();
|
||||
}
|
||||
}, [handleResetControlImage, isError, isConnected]);
|
||||
|
||||
const draggableData = useMemo<ImageDraggableData | undefined>(() => {
|
||||
if (controlImage) {
|
||||
return {
|
||||
id: dndId,
|
||||
payloadType: 'IMAGE_DTO',
|
||||
payload: { imageDTO: controlImage },
|
||||
};
|
||||
}
|
||||
}, [controlImage, dndId]);
|
||||
const onUpload = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
onChangeImage(imageDTO);
|
||||
},
|
||||
[onChangeImage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && isErrorControlImage) {
|
||||
handleResetControlImage();
|
||||
}
|
||||
}, [handleResetControlImage, isConnected, isErrorControlImage]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position="relative"
|
||||
w="full"
|
||||
h="full"
|
||||
alignItems="center"
|
||||
borderColor="error.500"
|
||||
borderStyle="solid"
|
||||
borderWidth={controlImage ? 0 : 1}
|
||||
borderRadius="base"
|
||||
>
|
||||
<IAIDndImage
|
||||
draggableData={draggableData}
|
||||
droppableData={droppableData}
|
||||
imageDTO={controlImage}
|
||||
postUploadAction={postUploadAction}
|
||||
/>
|
||||
|
||||
{controlImage && (
|
||||
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
|
||||
<IAIDndImageIcon
|
||||
onClick={handleResetControlImage}
|
||||
icon={<PiArrowCounterClockwiseBold size={16} />}
|
||||
tooltip={t('common.reset')}
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" alignItems="center" data-error={!imageDTO && !image?.image_name}>
|
||||
{!imageDTO && (
|
||||
<UploadImageButton
|
||||
w="full"
|
||||
h="full"
|
||||
isError={!imageDTO && !image?.image_name}
|
||||
onUpload={onUpload}
|
||||
fontSize={36}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
)}
|
||||
{imageDTO && (
|
||||
<>
|
||||
<DndImage imageDTO={imageDTO} />
|
||||
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
|
||||
<DndImageIcon
|
||||
onClick={handleResetControlImage}
|
||||
icon={<PiArrowCounterClockwiseBold size={16} />}
|
||||
tooltip={t('common.reset')}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
<DndDropTarget dndTarget={dndTarget} dndTargetData={dndTargetData} label={t('gallery.drop')} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IPAdapterImagePreview.displayName = 'IPAdapterImagePreview';
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
||||
import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.referenceImages.entities.map(mapId).reverse();
|
||||
const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.referenceImages.entities.map(getEntityIdentifier).toReversed();
|
||||
});
|
||||
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
||||
return selectedEntityIdentifier?.type === 'reference_image';
|
||||
@@ -17,17 +17,17 @@ const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selecte
|
||||
|
||||
export const IPAdapterList = memo(() => {
|
||||
const isSelected = useAppSelector(selectIsSelected);
|
||||
const ipaIds = useAppSelector(selectEntityIds);
|
||||
const entityIdentifiers = useAppSelector(selectEntityIdentifiers);
|
||||
|
||||
if (ipaIds.length === 0) {
|
||||
if (entityIdentifiers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ipaIds.length > 0) {
|
||||
if (entityIdentifiers.length > 0) {
|
||||
return (
|
||||
<CanvasEntityGroupList type="reference_image" isSelected={isSelected}>
|
||||
{ipaIds.map((id) => (
|
||||
<IPAdapter key={id} id={id} />
|
||||
<CanvasEntityGroupList type="reference_image" isSelected={isSelected} entityIdentifiers={entityIdentifiers}>
|
||||
{entityIdentifiers.map((entityIdentifiers) => (
|
||||
<IPAdapter key={entityIdentifiers.id} id={entityIdentifiers.id} />
|
||||
))}
|
||||
</CanvasEntityGroupList>
|
||||
);
|
||||
|
||||
@@ -19,11 +19,12 @@ import {
|
||||
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
|
||||
import type { IPAImageDropData } from 'features/dnd/types';
|
||||
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold } from 'react-icons/pi';
|
||||
import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types';
|
||||
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
|
||||
|
||||
import { IPAdapterImagePreview } from './IPAdapterImagePreview';
|
||||
import { IPAdapterModel } from './IPAdapterModel';
|
||||
@@ -80,13 +81,9 @@ export const IPAdapterSettings = memo(() => {
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const droppableData = useMemo<IPAImageDropData>(
|
||||
() => ({ actionType: 'SET_IPA_IMAGE', context: { id: entityIdentifier.id }, id: entityIdentifier.id }),
|
||||
[entityIdentifier.id]
|
||||
);
|
||||
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(
|
||||
() => ({ type: 'SET_IPA_IMAGE', id: entityIdentifier.id }),
|
||||
[entityIdentifier.id]
|
||||
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
||||
() => setGlobalReferenceImageDndTarget.getData({ entityIdentifier }, ipAdapter.image?.image_name),
|
||||
[entityIdentifier, ipAdapter.image?.image_name]
|
||||
);
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
@@ -122,10 +119,10 @@ export const IPAdapterSettings = memo(() => {
|
||||
</Flex>
|
||||
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1">
|
||||
<IPAdapterImagePreview
|
||||
image={ipAdapter.image ?? null}
|
||||
image={ipAdapter.image}
|
||||
onChangeImage={onChangeImage}
|
||||
droppableData={droppableData}
|
||||
postUploadAction={postUploadAction}
|
||||
dndTarget={setGlobalReferenceImageDndTarget}
|
||||
dndTargetData={dndTargetData}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
||||
import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.inpaintMasks.entities.map(mapId).reverse();
|
||||
const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.inpaintMasks.entities.map(getEntityIdentifier).toReversed();
|
||||
});
|
||||
|
||||
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
||||
@@ -17,17 +17,17 @@ const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selecte
|
||||
|
||||
export const InpaintMaskList = memo(() => {
|
||||
const isSelected = useAppSelector(selectIsSelected);
|
||||
const entityIds = useAppSelector(selectEntityIds);
|
||||
const entityIdentifiers = useAppSelector(selectEntityIdentifiers);
|
||||
|
||||
if (entityIds.length === 0) {
|
||||
if (entityIdentifiers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entityIds.length > 0) {
|
||||
if (entityIdentifiers.length > 0) {
|
||||
return (
|
||||
<CanvasEntityGroupList type="inpaint_mask" isSelected={isSelected}>
|
||||
{entityIds.map((id) => (
|
||||
<InpaintMask key={id} id={id} />
|
||||
<CanvasEntityGroupList type="inpaint_mask" isSelected={isSelected} entityIdentifiers={entityIdentifiers}>
|
||||
{entityIdentifiers.map((entityIdentifier) => (
|
||||
<InpaintMask key={entityIdentifier.id} id={entityIdentifier.id} />
|
||||
))}
|
||||
</CanvasEntityGroupList>
|
||||
);
|
||||
|
||||
@@ -17,12 +17,15 @@ import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectIsEnabled = createSelector(selectActiveRasterLayerEntities, (entities) => entities.length > 0);
|
||||
const selectHasRasterLayersWithContent = createSelector(
|
||||
selectActiveRasterLayerEntities,
|
||||
(entities) => entities.length > 0
|
||||
);
|
||||
|
||||
export const ParamDenoisingStrength = memo(() => {
|
||||
const img2imgStrength = useAppSelector(selectImg2imgStrength);
|
||||
const dispatch = useAppDispatch();
|
||||
const isEnabled = useAppSelector(selectIsEnabled);
|
||||
const hasRasterLayersWithContent = useAppSelector(selectHasRasterLayersWithContent);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
@@ -37,16 +40,16 @@ export const ParamDenoisingStrength = memo(() => {
|
||||
const [invokeBlue300] = useToken('colors', ['invokeBlue.300']);
|
||||
|
||||
return (
|
||||
<FormControl isDisabled={!isEnabled} p={1} justifyContent="space-between" h={8}>
|
||||
<FormControl isDisabled={!hasRasterLayersWithContent} p={1} justifyContent="space-between" h={8}>
|
||||
<Flex gap={3} alignItems="center">
|
||||
<InformationalPopover feature="paramDenoisingStrength">
|
||||
<FormLabel mr={0}>{`${t('parameters.denoisingStrength')}`}</FormLabel>
|
||||
</InformationalPopover>
|
||||
{isEnabled && (
|
||||
{hasRasterLayersWithContent && (
|
||||
<WavyLine amplitude={img2imgStrength * 10} stroke={invokeBlue300} strokeWidth={1} width={40} height={14} />
|
||||
)}
|
||||
</Flex>
|
||||
{isEnabled ? (
|
||||
{hasRasterLayersWithContent ? (
|
||||
<>
|
||||
<CompositeSlider
|
||||
step={config.coarseStep}
|
||||
@@ -70,9 +73,7 @@ export const ParamDenoisingStrength = memo(() => {
|
||||
</>
|
||||
) : (
|
||||
<Flex alignItems="center">
|
||||
<Badge opacity="0.6">
|
||||
{t('common.disabled')} - {t('parameters.noRasterLayers')}
|
||||
</Badge>
|
||||
<Badge opacity="0.6">{t('parameters.disabledNoRasterContent')}</Badge>
|
||||
</Flex>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
import { RasterLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import type { ReplaceLayerImageDropData } from 'features/dnd/types';
|
||||
import type { ReplaceCanvasEntityObjectsWithImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { replaceCanvasEntityObjectsWithImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -18,10 +20,11 @@ type Props = {
|
||||
|
||||
export const RasterLayer = memo(({ id }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const entityIdentifier = useMemo<CanvasEntityIdentifier<'raster_layer'>>(() => ({ id, type: 'raster_layer' }), [id]);
|
||||
const dropData = useMemo<ReplaceLayerImageDropData>(
|
||||
() => ({ id, actionType: 'REPLACE_LAYER_WITH_IMAGE', context: { entityIdentifier } }),
|
||||
[id, entityIdentifier]
|
||||
const dndTargetData = useMemo<ReplaceCanvasEntityObjectsWithImageDndTargetData>(
|
||||
() => replaceCanvasEntityObjectsWithImageDndTarget.getData({ entityIdentifier }, entityIdentifier.id),
|
||||
[entityIdentifier]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -34,7 +37,12 @@ export const RasterLayer = memo(({ id }: Props) => {
|
||||
<Spacer />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
</CanvasEntityHeader>
|
||||
<IAIDroppable data={dropData} dropLabel={t('controlLayers.replaceLayer')} />
|
||||
<DndDropTarget
|
||||
dndTarget={replaceCanvasEntityObjectsWithImageDndTarget}
|
||||
dndTargetData={dndTargetData}
|
||||
label={t('controlLayers.replaceLayer')}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</CanvasEntityContainer>
|
||||
</RasterLayerAdapterGate>
|
||||
</EntityIdentifierContext.Provider>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
||||
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.rasterLayers.entities.map(mapId).reverse();
|
||||
const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.rasterLayers.entities.map(getEntityIdentifier).toReversed();
|
||||
});
|
||||
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
||||
return selectedEntityIdentifier?.type === 'raster_layer';
|
||||
@@ -16,17 +16,17 @@ const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selecte
|
||||
|
||||
export const RasterLayerEntityList = memo(() => {
|
||||
const isSelected = useAppSelector(selectIsSelected);
|
||||
const layerIds = useAppSelector(selectEntityIds);
|
||||
const entityIdentifiers = useAppSelector(selectEntityIdentifiers);
|
||||
|
||||
if (layerIds.length === 0) {
|
||||
if (entityIdentifiers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (layerIds.length > 0) {
|
||||
if (entityIdentifiers.length > 0) {
|
||||
return (
|
||||
<CanvasEntityGroupList type="raster_layer" isSelected={isSelected}>
|
||||
{layerIds.map((id) => (
|
||||
<RasterLayer key={id} id={id} />
|
||||
<CanvasEntityGroupList type="raster_layer" isSelected={isSelected} entityIdentifiers={entityIdentifiers}>
|
||||
{entityIdentifiers.map((entityIdentifier) => (
|
||||
<RasterLayer key={entityIdentifier.id} id={entityIdentifier.id} />
|
||||
))}
|
||||
</CanvasEntityGroupList>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
||||
import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidance';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.regionalGuidance.entities.map(mapId).reverse();
|
||||
const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.regionalGuidance.entities.map(getEntityIdentifier).toReversed();
|
||||
});
|
||||
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
||||
return selectedEntityIdentifier?.type === 'regional_guidance';
|
||||
@@ -16,17 +16,17 @@ const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selecte
|
||||
|
||||
export const RegionalGuidanceEntityList = memo(() => {
|
||||
const isSelected = useAppSelector(selectIsSelected);
|
||||
const rgIds = useAppSelector(selectEntityIds);
|
||||
const entityIdentifiers = useAppSelector(selectEntityIdentifiers);
|
||||
|
||||
if (rgIds.length === 0) {
|
||||
if (entityIdentifiers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rgIds.length > 0) {
|
||||
if (entityIdentifiers.length > 0) {
|
||||
return (
|
||||
<CanvasEntityGroupList type="regional_guidance" isSelected={isSelected}>
|
||||
{rgIds.map((id) => (
|
||||
<RegionalGuidance key={id} id={id} />
|
||||
<CanvasEntityGroupList type="regional_guidance" isSelected={isSelected} entityIdentifiers={entityIdentifiers}>
|
||||
{entityIdentifiers.map((entityIdentifier) => (
|
||||
<RegionalGuidance key={entityIdentifier.id} id={entityIdentifier.id} />
|
||||
))}
|
||||
</CanvasEntityGroupList>
|
||||
);
|
||||
|
||||
@@ -20,11 +20,12 @@ import {
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
|
||||
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
|
||||
import type { RGIPAdapterImageDropData } from 'features/dnd/types';
|
||||
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold, PiTrashSimpleFill } from 'react-icons/pi';
|
||||
import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types';
|
||||
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
type Props = {
|
||||
@@ -91,18 +92,15 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
|
||||
const droppableData = useMemo<RGIPAdapterImageDropData>(
|
||||
() => ({
|
||||
actionType: 'SET_RG_IP_ADAPTER_IMAGE',
|
||||
context: { id: entityIdentifier.id, referenceImageId: referenceImageId },
|
||||
id: entityIdentifier.id,
|
||||
}),
|
||||
[entityIdentifier.id, referenceImageId]
|
||||
);
|
||||
const postUploadAction = useMemo<RGIPAdapterImagePostUploadAction>(
|
||||
() => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id: entityIdentifier.id, referenceImageId: referenceImageId }),
|
||||
[entityIdentifier.id, referenceImageId]
|
||||
const dndTargetData = useMemo<SetRegionalGuidanceReferenceImageDndTargetData>(
|
||||
() =>
|
||||
setRegionalGuidanceReferenceImageDndTarget.getData(
|
||||
{ entityIdentifier, referenceImageId },
|
||||
ipAdapter.image?.image_name
|
||||
),
|
||||
[entityIdentifier, ipAdapter.image?.image_name, referenceImageId]
|
||||
);
|
||||
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
@@ -151,10 +149,10 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
|
||||
</Flex>
|
||||
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1">
|
||||
<IPAdapterImagePreview
|
||||
image={ipAdapter.image ?? null}
|
||||
image={ipAdapter.image}
|
||||
onChangeImage={onChangeImage}
|
||||
droppableData={droppableData}
|
||||
postUploadAction={postUploadAction}
|
||||
dndTarget={setRegionalGuidanceReferenceImageDndTarget}
|
||||
dndTargetData={dndTargetData}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected';
|
||||
import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor';
|
||||
import { entitySelected } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
export const CanvasEntityContainer = memo((props: PropsWithChildren) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const isSelected = useEntityIsSelected(entityIdentifier);
|
||||
const selectionColor = useEntitySelectionColor(entityIdentifier);
|
||||
const onClick = useCallback(() => {
|
||||
if (isSelected) {
|
||||
return;
|
||||
}
|
||||
dispatch(entitySelected({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier, isSelected]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position="relative"
|
||||
flexDir="column"
|
||||
w="full"
|
||||
bg={isSelected ? 'base.800' : 'base.850'}
|
||||
onClick={onClick}
|
||||
borderInlineStartWidth={5}
|
||||
borderColor={isSelected ? selectionColor : 'base.800'}
|
||||
borderRadius="base"
|
||||
>
|
||||
{props.children}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityContainer.displayName = 'CanvasEntityContainer';
|
||||
@@ -1,91 +0,0 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { useBoolean } from 'common/hooks/useBoolean';
|
||||
import { fixTooltipCloseOnScrollStyles } from 'common/util/fixTooltipCloseOnScrollStyles';
|
||||
import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton';
|
||||
import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/components/common/CanvasEntityMergeVisibleButton';
|
||||
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
|
||||
import { useEntityTypeInformationalPopover } from 'features/controlLayers/hooks/useEntityTypeInformationalPopover';
|
||||
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
|
||||
import { type CanvasEntityIdentifier, isRenderableEntityType } from 'features/controlLayers/store/types';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { PiCaretDownBold } from 'react-icons/pi';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
isSelected: boolean;
|
||||
type: CanvasEntityIdentifier['type'];
|
||||
}>;
|
||||
|
||||
const _hover: SystemStyleObject = {
|
||||
opacity: 1,
|
||||
};
|
||||
|
||||
export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props) => {
|
||||
const title = useEntityTypeTitle(type);
|
||||
const informationalPopoverFeature = useEntityTypeInformationalPopover(type);
|
||||
const collapse = useBoolean(true);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full">
|
||||
<Flex w="full">
|
||||
<Flex
|
||||
flexGrow={1}
|
||||
as={Button}
|
||||
onClick={collapse.toggle}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap={3}
|
||||
variant="unstyled"
|
||||
p={0}
|
||||
h={8}
|
||||
>
|
||||
<Icon
|
||||
boxSize={4}
|
||||
as={PiCaretDownBold}
|
||||
transform={collapse.isTrue ? undefined : 'rotate(-90deg)'}
|
||||
fill={isSelected ? 'base.200' : 'base.500'}
|
||||
transitionProperty="common"
|
||||
transitionDuration="fast"
|
||||
/>
|
||||
{informationalPopoverFeature ? (
|
||||
<InformationalPopover feature={informationalPopoverFeature}>
|
||||
<Text
|
||||
fontWeight="semibold"
|
||||
color={isSelected ? 'base.200' : 'base.500'}
|
||||
userSelect="none"
|
||||
transitionProperty="common"
|
||||
transitionDuration="fast"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</InformationalPopover>
|
||||
) : (
|
||||
<Text
|
||||
fontWeight="semibold"
|
||||
color={isSelected ? 'base.200' : 'base.500'}
|
||||
userSelect="none"
|
||||
transitionProperty="common"
|
||||
transitionDuration="fast"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Spacer />
|
||||
</Flex>
|
||||
{isRenderableEntityType(type) && <CanvasEntityMergeVisibleButton type={type} />}
|
||||
{isRenderableEntityType(type) && <CanvasEntityTypeIsHiddenToggle type={type} />}
|
||||
<CanvasEntityAddOfTypeButton type={type} />
|
||||
</Flex>
|
||||
<Collapse in={collapse.isTrue} style={fixTooltipCloseOnScrollStyles}>
|
||||
<Flex flexDir="column" gap={2} pt={2}>
|
||||
{children}
|
||||
</Flex>
|
||||
</Collapse>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityGroupList.displayName = 'CanvasEntityGroupList';
|
||||
Reference in New Issue
Block a user