mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): rp ui layout
This commit is contained in:
parent
dda1111f20
commit
bb37e25ed0
BIN
invokeai/frontend/web/public/assets/images/transparent_bg.png
Normal file
BIN
invokeai/frontend/web/public/assets/images/transparent_bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
@ -897,6 +897,7 @@
|
||||
"denoisingStrength": "Denoising Strength",
|
||||
"downloadImage": "Download Image",
|
||||
"general": "General",
|
||||
"globalSettings": "Global Settings",
|
||||
"height": "Height",
|
||||
"imageFit": "Fit Initial Image To Output Size",
|
||||
"images": "Images",
|
||||
@ -1518,7 +1519,7 @@
|
||||
"moveForward": "Move Forward",
|
||||
"moveBackward": "Move Backward",
|
||||
"brushSize": "Brush Size",
|
||||
"regionalPrompts": "Regional Prompts BETA",
|
||||
"regionalControl": "Regional Control (ALPHA)",
|
||||
"enableRegionalPrompts": "Enable $t(regionalPrompts.regionalPrompts)",
|
||||
"globalMaskOpacity": "Global Mask Opacity",
|
||||
"autoNegative": "Auto Negative",
|
||||
|
@ -3,6 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { layerAdded } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
export const AddLayerButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
@ -11,7 +12,11 @@ export const AddLayerButton = memo(() => {
|
||||
dispatch(layerAdded('vector_mask_layer'));
|
||||
}, [dispatch]);
|
||||
|
||||
return <Button onClick={onClick}>{t('regionalPrompts.addLayer')}</Button>;
|
||||
return (
|
||||
<Button onClick={onClick} leftIcon={<PiPlusBold />} variant="ghost">
|
||||
{t('regionalPrompts.addLayer')}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
AddLayerButton.displayName = 'AddLayerButton';
|
||||
|
@ -3,6 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { allLayersDeleted } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
export const DeleteAllLayersButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
@ -11,7 +12,11 @@ export const DeleteAllLayersButton = memo(() => {
|
||||
dispatch(allLayersDeleted());
|
||||
}, [dispatch]);
|
||||
|
||||
return <Button onClick={onClick}>{t('regionalPrompts.deleteAll')}</Button>;
|
||||
return (
|
||||
<Button onClick={onClick} leftIcon={<PiTrashSimpleBold />} variant="ghost" colorScheme="error">
|
||||
{t('regionalPrompts.deleteAll')}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
DeleteAllLayersButton.displayName = 'DeleteAllLayersButton';
|
||||
|
@ -1,50 +1,21 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton';
|
||||
import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
|
||||
import { DeleteAllLayersButton } from 'features/regionalPrompts/components/DeleteAllLayersButton';
|
||||
import { GlobalMaskLayerOpacity } from 'features/regionalPrompts/components/GlobalMaskLayerOpacity';
|
||||
import { RPLayerListItem } from 'features/regionalPrompts/components/RPLayerListItem';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { RegionalPromptsToolbar } from 'features/regionalPrompts/components/RegionalPromptsToolbar';
|
||||
import { StageComponent } from 'features/regionalPrompts/components/StageComponent';
|
||||
import { ToolChooser } from 'features/regionalPrompts/components/ToolChooser';
|
||||
import { UndoRedoButtonGroup } from 'features/regionalPrompts/components/UndoRedoButtonGroup';
|
||||
import { isVectorMaskLayer, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectRPLayerIdsReversed = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
|
||||
regionalPrompts.present.layers
|
||||
.filter(isVectorMaskLayer)
|
||||
.map((l) => l.id)
|
||||
.reverse()
|
||||
);
|
||||
|
||||
export const RegionalPromptsEditor = memo(() => {
|
||||
const rpLayerIdsReversed = useAppSelector(selectRPLayerIdsReversed);
|
||||
return (
|
||||
<Flex gap={4} w="full" h="full">
|
||||
<Flex flexDir="column" gap={4} minW={430}>
|
||||
<Flex gap={3} w="full" justifyContent="space-between">
|
||||
<AddLayerButton />
|
||||
<DeleteAllLayersButton />
|
||||
<Spacer />
|
||||
<UndoRedoButtonGroup />
|
||||
<ToolChooser />
|
||||
</Flex>
|
||||
<Flex justifyContent="space-between">
|
||||
<BrushSize />
|
||||
<GlobalMaskLayerOpacity />
|
||||
</Flex>
|
||||
<ScrollableContent>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
{rpLayerIdsReversed.map((id) => (
|
||||
<RPLayerListItem key={id} layerId={id} />
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Flex>
|
||||
<Flex
|
||||
position="relative"
|
||||
flexDirection="column"
|
||||
height="100%"
|
||||
width="100%"
|
||||
rowGap={4}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<RegionalPromptsToolbar />
|
||||
<StageComponent />
|
||||
</Flex>
|
||||
);
|
||||
|
@ -0,0 +1,38 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton';
|
||||
import { DeleteAllLayersButton } from 'features/regionalPrompts/components/DeleteAllLayersButton';
|
||||
import { RPLayerListItem } from 'features/regionalPrompts/components/RPLayerListItem';
|
||||
import { isVectorMaskLayer, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectRPLayerIdsReversed = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
|
||||
regionalPrompts.present.layers
|
||||
.filter(isVectorMaskLayer)
|
||||
.map((l) => l.id)
|
||||
.reverse()
|
||||
);
|
||||
|
||||
export const RegionalPromptsPanelContent = memo(() => {
|
||||
const rpLayerIdsReversed = useAppSelector(selectRPLayerIdsReversed);
|
||||
return (
|
||||
<Flex flexDir="column" gap={4} w="full" h="full">
|
||||
<Flex justifyContent="space-around">
|
||||
<AddLayerButton />
|
||||
<DeleteAllLayersButton />
|
||||
</Flex>
|
||||
<ScrollableContent>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
{rpLayerIdsReversed.map((id) => (
|
||||
<RPLayerListItem key={id} layerId={id} />
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
RegionalPromptsPanelContent.displayName = 'RegionalPromptsPanelContent';
|
@ -0,0 +1,20 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
|
||||
import { GlobalMaskLayerOpacity } from 'features/regionalPrompts/components/GlobalMaskLayerOpacity';
|
||||
import { ToolChooser } from 'features/regionalPrompts/components/ToolChooser';
|
||||
import { UndoRedoButtonGroup } from 'features/regionalPrompts/components/UndoRedoButtonGroup';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const RegionalPromptsToolbar = memo(() => {
|
||||
return (
|
||||
<Flex gap={4}>
|
||||
<BrushSize />
|
||||
<GlobalMaskLayerOpacity />
|
||||
<UndoRedoButtonGroup />
|
||||
<ToolChooser />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
RegionalPromptsToolbar.displayName = 'RegionalPromptsToolbar';
|
@ -1,4 +1,4 @@
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
@ -14,11 +14,11 @@ import {
|
||||
layerTranslated,
|
||||
selectRegionalPromptsSlice,
|
||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { renderBbox, renderLayers, renderToolPreview } from 'features/regionalPrompts/util/renderers';
|
||||
import { renderBackground, renderBbox, renderLayers, renderToolPreview } from 'features/regionalPrompts/util/renderers';
|
||||
import Konva from 'konva';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import { atom } from 'nanostores';
|
||||
import { useCallback, useLayoutEffect } from 'react';
|
||||
import { memo, useCallback, useLayoutEffect } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
|
||||
@ -35,7 +35,7 @@ const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSli
|
||||
return layer.previewColor;
|
||||
});
|
||||
|
||||
const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElement | null) => {
|
||||
const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElement | null, asPreview: boolean) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const width = useAppSelector((s) => s.generation.width);
|
||||
const height = useAppSelector((s) => s.generation.height);
|
||||
@ -49,23 +49,29 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
|
||||
const onLayerPosChanged = useCallback(
|
||||
(layerId: string, x: number, y: number) => {
|
||||
if (asPreview) {
|
||||
dispatch(layerTranslated({ layerId, x, y }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch, asPreview]
|
||||
);
|
||||
|
||||
const onBboxChanged = useCallback(
|
||||
(layerId: string, bbox: IRect | null) => {
|
||||
if (asPreview) {
|
||||
dispatch(layerBboxChanged({ layerId, bbox }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch, asPreview]
|
||||
);
|
||||
|
||||
const onBboxMouseDown = useCallback(
|
||||
(layerId: string) => {
|
||||
if (asPreview) {
|
||||
dispatch(layerSelected(layerId));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch, asPreview]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@ -86,7 +92,7 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.trace('Adding stage listeners');
|
||||
if (!stage) {
|
||||
if (!stage || asPreview) {
|
||||
return;
|
||||
}
|
||||
stage.on('mousedown', onMouseDown);
|
||||
@ -103,7 +109,7 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
stage.off('mouseenter', onMouseEnter);
|
||||
stage.off('mouseleave', onMouseLeave);
|
||||
};
|
||||
}, [stage, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave]);
|
||||
}, [stage, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.trace('Updating stage dimensions');
|
||||
@ -132,7 +138,7 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.trace('Rendering brush preview');
|
||||
if (!stage) {
|
||||
if (!stage || asPreview) {
|
||||
return;
|
||||
}
|
||||
renderToolPreview(
|
||||
@ -145,6 +151,7 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
state.brushSize
|
||||
);
|
||||
}, [
|
||||
asPreview,
|
||||
stage,
|
||||
tool,
|
||||
selectedLayerIdColor,
|
||||
@ -164,11 +171,19 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.trace('Rendering bbox');
|
||||
if (!stage) {
|
||||
if (!stage || asPreview) {
|
||||
return;
|
||||
}
|
||||
renderBbox(stage, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown);
|
||||
}, [dispatch, stage, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown]);
|
||||
}, [stage, asPreview, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.trace('Rendering background');
|
||||
if (!stage || asPreview) {
|
||||
return;
|
||||
}
|
||||
renderBackground(stage, width, height);
|
||||
}, [stage, asPreview, width, height]);
|
||||
};
|
||||
|
||||
const $container = atom<HTMLDivElement | null>(null);
|
||||
@ -180,15 +195,32 @@ const wrapperRef = (el: HTMLDivElement | null) => {
|
||||
$wrapper.set(el);
|
||||
};
|
||||
|
||||
export const StageComponent = () => {
|
||||
type Props = {
|
||||
asPreview?: boolean;
|
||||
};
|
||||
|
||||
export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||
const container = useStore($container);
|
||||
const wrapper = useStore($wrapper);
|
||||
useStageRenderer(container, wrapper);
|
||||
useStageRenderer(container, wrapper, asPreview);
|
||||
return (
|
||||
<Box overflow="hidden" w="full" h="full">
|
||||
<Box ref={wrapperRef} w="full" h="full">
|
||||
<Box ref={containerRef} tabIndex={-1} bg="base.850" w="min-content" h="min-content" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex overflow="hidden" w="full" h="full">
|
||||
<Flex ref={wrapperRef} w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Flex
|
||||
ref={containerRef}
|
||||
tabIndex={-1}
|
||||
bg="base.850"
|
||||
p={2}
|
||||
borderRadius="base"
|
||||
borderWidth={1}
|
||||
w="min-content"
|
||||
h="min-content"
|
||||
minW={64}
|
||||
minH={64}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
StageComponent.displayName = 'StageComponent';
|
||||
|
@ -0,0 +1,30 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectValidLayerCount = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||
if (!regionalPrompts.present.isEnabled) {
|
||||
return 0;
|
||||
}
|
||||
const validLayers = regionalPrompts.present.layers
|
||||
.filter((l) => l.isVisible)
|
||||
.filter((l) => {
|
||||
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
||||
const hasAtLeastOneImagePrompt = l.ipAdapterIds.length > 0;
|
||||
return hasTextPrompt || hasAtLeastOneImagePrompt;
|
||||
});
|
||||
|
||||
return validLayers.length;
|
||||
});
|
||||
|
||||
export const useRegionalControlTitle = () => {
|
||||
const { t } = useTranslation();
|
||||
const validLayerCount = useAppSelector(selectValidLayerCount);
|
||||
const title = useMemo(() => {
|
||||
const suffix = validLayerCount > 0 ? ` (${validLayerCount})` : '';
|
||||
return `${t('regionalPrompts.regionalControl')}${suffix}`;
|
||||
}, [t, validLayerCount]);
|
||||
return title;
|
||||
};
|
@ -406,6 +406,8 @@ export const TOOL_PREVIEW_BRUSH_FILL_ID = 'tool_preview_layer.brush_fill';
|
||||
export const TOOL_PREVIEW_BRUSH_BORDER_INNER_ID = 'tool_preview_layer.brush_border_inner';
|
||||
export const TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID = 'tool_preview_layer.brush_border_outer';
|
||||
export const TOOL_PREVIEW_RECT_ID = 'tool_preview_layer.rect';
|
||||
export const BACKGROUND_LAYER_ID = 'background_layer';
|
||||
export const BACKGROUND_RECT_ID = 'background_layer.rect';
|
||||
|
||||
// Names (aka classes) for Konva layers and objects
|
||||
export const VECTOR_MASK_LAYER_NAME = 'vector_mask_layer';
|
||||
|
@ -5,6 +5,8 @@ import type { Layer, Tool, VectorMaskLayer } from 'features/regionalPrompts/stor
|
||||
import {
|
||||
$isMouseOver,
|
||||
$tool,
|
||||
BACKGROUND_LAYER_ID,
|
||||
BACKGROUND_RECT_ID,
|
||||
getLayerBboxId,
|
||||
getVectorMaskLayerObjectGroupId,
|
||||
isVectorMaskLayer,
|
||||
@ -475,3 +477,51 @@ export const renderBbox = (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const renderBackground = (stage: Konva.Stage, width: number, height: number) => {
|
||||
let layer = stage.findOne<Konva.Layer>(`#${BACKGROUND_LAYER_ID}`);
|
||||
|
||||
if (!layer) {
|
||||
layer = new Konva.Layer({
|
||||
id: BACKGROUND_LAYER_ID,
|
||||
});
|
||||
const background = new Konva.Rect({
|
||||
id: BACKGROUND_RECT_ID,
|
||||
x: stage.x(),
|
||||
y: 0,
|
||||
width: stage.width() / stage.scaleX(),
|
||||
height: stage.height() / stage.scaleY(),
|
||||
listening: false,
|
||||
opacity: 0.2,
|
||||
});
|
||||
layer.add(background);
|
||||
stage.add(layer);
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
background.fillPatternImage(image);
|
||||
};
|
||||
// This is invokeai/frontend/web/public/assets/images/transparent_bg.png as a dataURL
|
||||
image.src =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAEsmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjIwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjAiCiAgIGV4aWY6Q29sb3JTcGFjZT0iMSIKICAgdGlmZjpJbWFnZVdpZHRoPSIyMCIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMjAiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjMwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIzMDAvMSIKICAgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIKICAgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InByb2R1Y2VkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBQaG90byAxLjEwLjgiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDQtMjNUMDg6MjA6NDcrMTA6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/Pn9pdVgAAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWR3yuDURjHP5uJmKghFy6WxpVpqMWNMgm1tGbKr5vt3S+1d3t73y3JrXKrKHHj1wV/AbfKtVJESq53TdywXs9rakv2nJ7zfM73nOfpnOeAPZJRVMPhAzWb18NTAffC4pK7oYiDTjpw4YgqhjYeCgWpaR8P2Kx457Vq1T73rzXHE4YCtkbhMUXT88LTwsG1vGbxrnC7ko7Ghc+F+3W5oPC9pcfKXLQ4VeYvi/VIeALsbcLuVBXHqlhJ66qwvByPmikov/exXuJMZOfnJPaId2MQZooAbmaYZAI/g4zK7MfLEAOyoka+7yd/lpzkKjJrrKOzSoo0efpFLUj1hMSk6AkZGdat/v/tq5EcHipXdwag/sU033qhYQdK26b5eWyapROoe4arbCU/dwQj76JvVzTPIbRuwsV1RYvtweUWdD1pUT36I9WJ25NJeD2DlkVw3ULTcrlnv/ucPkJkQ77qBvYPoE/Ot658AxagZ8FoS/a7AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAL0lEQVQ4jWM8ffo0A25gYmKCR5YJjxxBMKp5ZGhm/P//Px7pM2fO0MrmUc0jQzMAB2EIhZC3pUYAAAAASUVORK5CYII=';
|
||||
}
|
||||
|
||||
const background = layer.findOne<Konva.Rect>(`#${BACKGROUND_RECT_ID}`);
|
||||
assert(background, 'Background rect not found');
|
||||
// ensure background rect is in the top-left of the canvas
|
||||
background.absolutePosition({ x: 0, y: 0 });
|
||||
|
||||
// set the dimensions of the background rect to match the canvas - not the stage!!!
|
||||
background.size({
|
||||
width: width / stage.scaleX(),
|
||||
height: height / stage.scaleY(),
|
||||
});
|
||||
|
||||
// Calculate the amount the stage is moved - including the effect of scaling
|
||||
const stagePos = {
|
||||
x: -stage.x() / stage.scaleX(),
|
||||
y: -stage.y() / stage.scaleY(),
|
||||
};
|
||||
|
||||
// Apply that movement to the fill pattern
|
||||
background.fillPatternOffset(stagePos);
|
||||
};
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||
import QueueControls from 'features/queue/components/QueueControls';
|
||||
import { RegionalPromptsPanelContent } from 'features/regionalPrompts/components/RegionalPromptsPanelContent';
|
||||
import { useRegionalControlTitle } from 'features/regionalPrompts/hooks/useRegionalControlTitle';
|
||||
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
|
||||
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
||||
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
|
||||
@ -14,6 +16,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const overlayScrollbarsStyles: CSSProperties = {
|
||||
height: '100%',
|
||||
@ -21,7 +24,9 @@ const overlayScrollbarsStyles: CSSProperties = {
|
||||
};
|
||||
|
||||
const ParametersPanel = () => {
|
||||
const { t } = useTranslation();
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const regionalControlTitle = useRegionalControlTitle();
|
||||
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
|
||||
|
||||
return (
|
||||
@ -32,6 +37,15 @@ const ParametersPanel = () => {
|
||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||
{isSDXL ? <SDXLPrompts /> : <Prompts />}
|
||||
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
|
||||
<TabList>
|
||||
<Tab>{t('parameters.globalSettings')}</Tab>
|
||||
<Tab>{regionalControlTitle}</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels w="full" h="full">
|
||||
<TabPanel>
|
||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||
<ImageSettingsAccordion />
|
||||
<GenerationSettingsAccordion />
|
||||
<ControlSettingsAccordion />
|
||||
@ -39,6 +53,13 @@ const ParametersPanel = () => {
|
||||
{isSDXL && <RefinerSettingsAccordion />}
|
||||
<AdvancedSettingsAccordion />
|
||||
</Flex>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<RegionalPromptsPanelContent />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Flex>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
@ -1,39 +1,20 @@
|
||||
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
|
||||
import { RegionalPromptsEditor } from 'features/regionalPrompts/components/RegionalPromptsEditor';
|
||||
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { useRegionalControlTitle } from 'features/regionalPrompts/hooks/useRegionalControlTitle';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectValidLayerCount = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||
if (!regionalPrompts.present.isEnabled) {
|
||||
return 0;
|
||||
}
|
||||
const validLayers = regionalPrompts.present.layers
|
||||
.filter((l) => l.isVisible)
|
||||
.filter((l) => {
|
||||
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
||||
const hasAtLeastOneImagePrompt = l.ipAdapterIds.length > 0;
|
||||
return hasTextPrompt || hasAtLeastOneImagePrompt;
|
||||
});
|
||||
|
||||
return validLayers.length;
|
||||
});
|
||||
|
||||
const TextToImageTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const validLayerCount = useAppSelector(selectValidLayerCount);
|
||||
const regionalControlTitle = useRegionalControlTitle();
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
|
||||
<TabList>
|
||||
<Tab>{t('common.viewer')}</Tab>
|
||||
<Tab>
|
||||
{t('regionalPrompts.regionalPrompts')}
|
||||
{validLayerCount > 0 ? ` (${validLayerCount})` : ''}
|
||||
</Tab>
|
||||
<Tab>{regionalControlTitle}</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels w="full" h="full">
|
||||
|
Loading…
Reference in New Issue
Block a user