diff --git a/invokeai/frontend/web/public/assets/images/transparent_bg.png b/invokeai/frontend/web/public/assets/images/transparent_bg.png
new file mode 100644
index 0000000000..e1a3c339ce
Binary files /dev/null and b/invokeai/frontend/web/public/assets/images/transparent_bg.png differ
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 759d24842f..c130b11ba6 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -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",
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx
index ec0b5fcffd..8acfc18c56 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx
@@ -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 ;
+ return (
+ } variant="ghost">
+ {t('regionalPrompts.addLayer')}
+
+ );
});
AddLayerButton.displayName = 'AddLayerButton';
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/DeleteAllLayersButton.tsx
index 20300a4d67..4306e3f3f3 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/DeleteAllLayersButton.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/DeleteAllLayersButton.tsx
@@ -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 ;
+ return (
+ } variant="ghost" colorScheme="error">
+ {t('regionalPrompts.deleteAll')}
+
+ );
});
DeleteAllLayersButton.displayName = 'DeleteAllLayersButton';
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx
index fbf1bfec49..dd2e797235 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx
@@ -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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {rpLayerIdsReversed.map((id) => (
-
- ))}
-
-
-
+
+
);
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPanelContent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPanelContent.tsx
new file mode 100644
index 0000000000..1fe4d53623
--- /dev/null
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPanelContent.tsx
@@ -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 (
+
+
+
+
+
+
+
+ {rpLayerIdsReversed.map((id) => (
+
+ ))}
+
+
+
+ );
+});
+
+RegionalPromptsPanelContent.displayName = 'RegionalPromptsPanelContent';
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsToolbar.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsToolbar.tsx
new file mode 100644
index 0000000000..4a3b611efd
--- /dev/null
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsToolbar.tsx
@@ -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 (
+
+
+
+
+
+
+ );
+});
+
+RegionalPromptsToolbar.displayName = 'RegionalPromptsToolbar';
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx
index 8d9f12710b..fe53951cde 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx
@@ -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) => {
- dispatch(layerTranslated({ layerId, x, y }));
+ if (asPreview) {
+ dispatch(layerTranslated({ layerId, x, y }));
+ }
},
- [dispatch]
+ [dispatch, asPreview]
);
const onBboxChanged = useCallback(
(layerId: string, bbox: IRect | null) => {
- dispatch(layerBboxChanged({ layerId, bbox }));
+ if (asPreview) {
+ dispatch(layerBboxChanged({ layerId, bbox }));
+ }
},
- [dispatch]
+ [dispatch, asPreview]
);
const onBboxMouseDown = useCallback(
(layerId: string) => {
- dispatch(layerSelected(layerId));
+ 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(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 (
-
-
-
-
-
+
+
+
+
+
);
-};
+});
+
+StageComponent.displayName = 'StageComponent';
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/hooks/useRegionalControlTitle.ts b/invokeai/frontend/web/src/features/regionalPrompts/hooks/useRegionalControlTitle.ts
new file mode 100644
index 0000000000..4f23804c2a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/regionalPrompts/hooks/useRegionalControlTitle.ts
@@ -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;
+};
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts
index 0cc3103fe4..4ca70b488b 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts
+++ b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts
@@ -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';
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts
index a802f4ee83..4e4b275073 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts
+++ b/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts
@@ -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(`#${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(`#${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);
+};
diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx
index a74d132bd6..a026a95196 100644
--- a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx
@@ -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,12 +37,28 @@ const ParametersPanel = () => {
{isSDXL ? : }
-
-
-
- {activeTabName === 'unifiedCanvas' && }
- {isSDXL && }
-
+
+
+ {t('parameters.globalSettings')}
+ {regionalControlTitle}
+
+
+
+
+
+
+
+
+ {activeTabName === 'unifiedCanvas' && }
+ {isSDXL && }
+
+
+
+
+
+
+
+
diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx
index 8021a3a9e5..733c7f7b2e 100644
--- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx
@@ -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 (
{t('common.viewer')}
-
- {t('regionalPrompts.regionalPrompts')}
- {validLayerCount > 0 ? ` (${validLayerCount})` : ''}
-
+ {regionalControlTitle}