From 39ff2bab069c4ccda3a0c1863ef36b74b8cf2f6f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:53:28 +1000 Subject: [PATCH 1/9] feat(ui): rp ui layout --- .../public/assets/images/transparent_bg.png | Bin 0 -> 1736 bytes invokeai/frontend/web/public/locales/en.json | 3 +- .../components/AddLayerButton.tsx | 7 +- .../components/DeleteAllLayersButton.tsx | 7 +- .../components/RegionalPromptsEditor.tsx | 53 +++--------- .../RegionalPromptsPanelContent.tsx | 38 +++++++++ .../components/RegionalPromptsToolbar.tsx | 20 +++++ .../components/StageComponent.tsx | 78 ++++++++++++------ .../hooks/useRegionalControlTitle.ts | 30 +++++++ .../store/regionalPromptsSlice.ts | 2 + .../regionalPrompts/util/renderers.ts | 50 +++++++++++ .../ui/components/ParametersPanel.tsx | 35 ++++++-- .../ui/components/tabs/TextToImageTab.tsx | 27 +----- 13 files changed, 253 insertions(+), 97 deletions(-) create mode 100644 invokeai/frontend/web/public/assets/images/transparent_bg.png create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPanelContent.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsToolbar.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/hooks/useRegionalControlTitle.ts 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 0000000000000000000000000000000000000000..e1a3c339ce18867a426a347102cebcf10e6f81a7 GIT binary patch literal 1736 zcmah~eM}Q)96rD=NQ@?KZZ3qK$Cf!vd)E&bTzWLn7ARWAM-63Jj7RUbMPQX8l@W-`kq=uqfrgW{$yy9akAqDD<-rTB zSu??4L?Av%;&BuWg+khpLCXnVRF{^PhGKeDuh#&EMhvqODQj3UQIUvfSZUEMFucTY zEUajfrCd-V5Cq`xj5sYbbLy-(Q$8>PRS;CC#nAb5iSaB$SCB)IwebEvz@@z{2Uww(le-%bCBI)$rm^e;|G{n zn4JmRZI%>WT1tvWuZ=7L+J=Kz#uK)Z5@6{uy-|Z1HF|>+OU3mVZhUR44#TmCSt=Wx z28xu(|Klrr8a9ClhXc=HM2Qo^>QrQ-7twnnqsY+Ds5=~n)js2-Td|2ic!8sWZkkfd z0&)>l>_tSg;Pw%~HB-q1T@V!Y4g=bke8{x-AJoToo%0NyZXUaZc7-Y`hBe=u5>9N{^Ye|Bl~Lyhy9%wj zogv3hXLepK+r4d~<)a7th9+A^@3xG-O*9rbFOS3|H{ZBi6gAm*yRrLNZB-u5kJKjB zq$I?x{ET^V-H#{b(XQRs(G?^<7P*G11?oqTuW`&X*3RzC_} zX#Ml6wt>Mn&!z6TgC2O8BS+s4^!?#{cA&#$zt?f*V*H__uWsp?{Cwx_?bPV#OB1yr z_j*q~#CncU(dMf62Ja7iyFTXZ@vKYZuT(a*{F0*sPWWFohgKwxALojVS2v$y z`}%{n17p3vZj=b7fS5|WUU%~YyG&Fy+4x(T*Mtk+o<}X|nUv+y@2e)(wZ1-dzH?v9 z##V2yZ`(vS_+G^FIVB1lJqU4wV%`@uT}-z8cM&}wo`_2KhvS;_~ { const { t } = useTranslation(); @@ -11,7 +12,11 @@ export const AddLayerButton = memo(() => { dispatch(layerAdded('vector_mask_layer')); }, [dispatch]); - return ; + return ( + + ); }); 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 20300a4d67e..4306e3f3f37 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 ( + + ); }); 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 fbf1bfec498..dd2e797235b 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 00000000000..1fe4d536235 --- /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 00000000000..4a3b611efd3 --- /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 8d9f12710b4..fe53951cde0 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 00000000000..4f23804c2ae --- /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 0cc3103fe44..4ca70b488bc 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 a802f4ee83b..4e4b2750736 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 a74d132bd60..a026a951963 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 8021a3a9e50..733c7f7b2e5 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} From 2961fb538066c43b6f61c5cb09bd537aef62bdd5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:37:28 +1000 Subject: [PATCH 2/9] feat(ui): aspect ratio preview is regional prompts canvas --- .../ImageSize/AspectRatioPreview.tsx | 71 +------- .../components/StageComponent.tsx | 157 ++++++++++-------- .../regionalPrompts/util/getLayerBlobs.ts | 4 +- .../regionalPrompts/util/renderers.ts | 31 +++- 4 files changed, 114 insertions(+), 149 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx index e662acae7d4..dcef435c390 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx @@ -1,73 +1,10 @@ -import { useSize } from '@chakra-ui/react-use-size'; -import { Flex, Icon } from '@invoke-ai/ui-library'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; -import { AnimatePresence, motion } from 'framer-motion'; -import { useMemo, useRef } from 'react'; -import { PiFrameCorners } from 'react-icons/pi'; - -import { - BOX_SIZE_CSS_CALC, - ICON_CONTAINER_STYLES, - ICON_HIGH_CUTOFF, - ICON_LOW_CUTOFF, - MOTION_ICON_ANIMATE, - MOTION_ICON_EXIT, - MOTION_ICON_INITIAL, -} from './constants'; +import { Flex } from '@invoke-ai/ui-library'; +import { StageComponent } from 'features/regionalPrompts/components/StageComponent'; export const AspectRatioPreview = () => { - const ctx = useImageSizeContext(); - const containerRef = useRef(null); - const containerSize = useSize(containerRef); - - const shouldShowIcon = useMemo( - () => ctx.aspectRatioState.value < ICON_HIGH_CUTOFF && ctx.aspectRatioState.value > ICON_LOW_CUTOFF, - [ctx.aspectRatioState.value] - ); - - const { width, height } = useMemo(() => { - if (!containerSize) { - return { width: 0, height: 0 }; - } - - let width = ctx.width; - let height = ctx.height; - - if (ctx.width > ctx.height) { - width = containerSize.width; - height = width / ctx.aspectRatioState.value; - } else { - height = containerSize.height; - width = height * ctx.aspectRatioState.value; - } - - return { width, height }; - }, [containerSize, ctx.width, ctx.height, ctx.aspectRatioState.value]); - return ( - - - - {shouldShowIcon && ( - - - - )} - - + + ); }; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx index fe53951cde0..1f69b37bbef 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx @@ -1,3 +1,4 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { logger } from 'app/logging/logger'; @@ -14,18 +15,18 @@ import { layerTranslated, selectRegionalPromptsSlice, } from 'features/regionalPrompts/store/regionalPromptsSlice'; -import { renderBackground, renderBbox, renderLayers, renderToolPreview } from 'features/regionalPrompts/util/renderers'; +import { renderers } from 'features/regionalPrompts/util/renderers'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; -import { atom } from 'nanostores'; -import { memo, useCallback, useLayoutEffect } from 'react'; +import type { MutableRefObject } from 'react'; +import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { assert } from 'tsafe'; // This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead? Konva.showWarnings = false; const log = logger('regionalPrompts'); -const $stage = atom(null); + const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => { const layer = regionalPrompts.present.layers.find((l) => l.id === regionalPrompts.present.selectedLayerId); if (!layer) { @@ -35,43 +36,52 @@ const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSli return layer.previewColor; }); -const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElement | null, asPreview: boolean) => { +const useStageRenderer = ( + stageRef: MutableRefObject, + 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); const state = useAppSelector((s) => s.regionalPrompts.present); - const stage = useStore($stage); const tool = useStore($tool); const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave } = useMouseEvents(); const cursorPosition = useStore($cursorPosition); const lastMouseDownPos = useStore($lastMouseDownPos); const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor); + const renderLayers = useMemo(() => (asPreview ? renderers.layersDebounced : renderers.layers), [asPreview]); + const renderToolPreview = useMemo( + () => (asPreview ? renderers.toolPreviewDebounced : renderers.toolPreview), + [asPreview] + ); + const renderBbox = useMemo(() => (asPreview ? renderers.bboxDebounced : renderers.bbox), [asPreview]); + const renderBackground = useMemo( + () => (asPreview ? renderers.backgroundDebounced : renderers.background), + [asPreview] + ); + const onLayerPosChanged = useCallback( (layerId: string, x: number, y: number) => { - if (asPreview) { - dispatch(layerTranslated({ layerId, x, y })); - } + dispatch(layerTranslated({ layerId, x, y })); }, - [dispatch, asPreview] + [dispatch] ); const onBboxChanged = useCallback( (layerId: string, bbox: IRect | null) => { - if (asPreview) { - dispatch(layerBboxChanged({ layerId, bbox })); - } + dispatch(layerBboxChanged({ layerId, bbox })); }, - [dispatch, asPreview] + [dispatch] ); const onBboxMouseDown = useCallback( (layerId: string) => { - if (asPreview) { - dispatch(layerSelected(layerId)); - } + dispatch(layerSelected(layerId)); }, - [dispatch, asPreview] + [dispatch] ); useLayoutEffect(() => { @@ -79,27 +89,24 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem if (!container) { return; } - $stage.set( - new Konva.Stage({ - container, - }) - ); + const stage = stageRef.current.container(container); return () => { log.trace('Cleaning up stage'); - $stage.get()?.destroy(); + stage.destroy(); }; - }, [container]); + }, [container, stageRef]); useLayoutEffect(() => { log.trace('Adding stage listeners'); - if (!stage || asPreview) { + if (asPreview) { return; } - stage.on('mousedown', onMouseDown); - stage.on('mouseup', onMouseUp); - stage.on('mousemove', onMouseMove); - stage.on('mouseenter', onMouseEnter); - stage.on('mouseleave', onMouseLeave); + stageRef.current.on('mousedown', onMouseDown); + stageRef.current.on('mouseup', onMouseUp); + stageRef.current.on('mousemove', onMouseMove); + stageRef.current.on('mouseenter', onMouseEnter); + stageRef.current.on('mouseleave', onMouseLeave); + const stage = stageRef.current; return () => { log.trace('Cleaning up stage listeners'); @@ -109,14 +116,16 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem stage.off('mouseenter', onMouseEnter); stage.off('mouseleave', onMouseLeave); }; - }, [stage, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave]); + }, [stageRef, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave]); useLayoutEffect(() => { log.trace('Updating stage dimensions'); - if (!stage || !wrapper) { + if (!wrapper) { return; } + const stage = stageRef.current; + const fitStageToContainer = () => { const newXScale = wrapper.offsetWidth / width; const newYScale = wrapper.offsetHeight / height; @@ -134,15 +143,15 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem return () => { resizeObserver.disconnect(); }; - }, [stage, width, height, wrapper]); + }, [stageRef, width, height, wrapper]); useLayoutEffect(() => { log.trace('Rendering brush preview'); - if (!stage || asPreview) { + if (asPreview) { return; } renderToolPreview( - stage, + stageRef.current, tool, selectedLayerIdColor, state.globalMaskLayerOpacity, @@ -152,47 +161,36 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem ); }, [ asPreview, - stage, + stageRef, tool, selectedLayerIdColor, state.globalMaskLayerOpacity, cursorPosition, lastMouseDownPos, state.brushSize, + renderToolPreview, ]); useLayoutEffect(() => { log.trace('Rendering layers'); - if (!stage) { - return; - } - renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged); - }, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged]); + renderLayers(stageRef.current, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged); + }, [stageRef, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderLayers]); useLayoutEffect(() => { log.trace('Rendering bbox'); - if (!stage || asPreview) { + if (asPreview) { return; } - renderBbox(stage, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown); - }, [stage, asPreview, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown]); + renderBbox(stageRef.current, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown); + }, [stageRef, asPreview, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown, renderBbox]); useLayoutEffect(() => { log.trace('Rendering background'); - if (!stage || asPreview) { + if (asPreview) { return; } - renderBackground(stage, width, height); - }, [stage, asPreview, width, height]); -}; - -const $container = atom(null); -const containerRef = (el: HTMLDivElement | null) => { - $container.set(el); -}; -const $wrapper = atom(null); -const wrapperRef = (el: HTMLDivElement | null) => { - $wrapper.set(el); + renderBackground(stageRef.current, width, height); + }, [stageRef, asPreview, width, height, renderBackground]); }; type Props = { @@ -200,24 +198,39 @@ type Props = { }; export const StageComponent = memo(({ asPreview = false }: Props) => { - const container = useStore($container); - const wrapper = useStore($wrapper); - useStageRenderer(container, wrapper, asPreview); + const stageRef = useRef( + new Konva.Stage({ + container: document.createElement('div'), // We will overwrite this shortly... + }) + ); + const [container, setContainer] = useState(null); + const [wrapper, setWrapper] = useState(null); + + const containerRef = useCallback((el: HTMLDivElement | null) => { + setContainer(el); + }, []); + + const wrapperRef = useCallback((el: HTMLDivElement | null) => { + setWrapper(el); + }, []); + + useStageRenderer(stageRef, container, wrapper, asPreview); + + const sx = useMemo( + () => ({ + bg: 'base.850', + p: asPreview ? 0 : 2, + borderRadius: asPreview ? 0 : 'base', + borderWidth: asPreview ? 0 : 1, + w: 'min-content', + h: 'min-content', + }), + [asPreview] + ); return ( - + ); diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts index aa61a9a4066..183042bb43b 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts @@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; import { VECTOR_MASK_LAYER_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice'; -import { renderLayers } from 'features/regionalPrompts/util/renderers'; +import { renderers } from 'features/regionalPrompts/util/renderers'; import Konva from 'konva'; import { assert } from 'tsafe'; @@ -20,7 +20,7 @@ export const getRegionalPromptLayerBlobs = async ( const reduxLayers = state.regionalPrompts.present.layers; const container = document.createElement('div'); const stage = new Konva.Stage({ container, width: state.generation.width, height: state.generation.height }); - renderLayers(stage, reduxLayers, 1, 'brush'); + renderers.layers(stage, reduxLayers, 1, 'brush'); const konvaLayers = stage.find(`.${VECTOR_MASK_LAYER_NAME}`); const blobs: Record = {}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts index 4e4b2750736..ae1c2a9d38f 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts @@ -25,6 +25,7 @@ import { import { getLayerBboxFast, getLayerBboxPixels } from 'features/regionalPrompts/util/bbox'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; +import { debounce } from 'lodash-es'; import type { RgbColor } from 'react-colorful'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -34,6 +35,8 @@ const BBOX_NOT_SELECTED_STROKE = 'rgba(255, 255, 255, 0.353)'; const BBOX_NOT_SELECTED_MOUSEOVER_STROKE = 'rgba(255, 255, 255, 0.661)'; const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)'; const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)'; +const STAGE_BG_DATAURL = + '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 mapId = (object: { id: string }) => object.id; @@ -57,7 +60,7 @@ const selectVectorMaskObjects = (node: Konva.Node) => { * @param lastMouseDownPos The position of the last mouse down event - used for the rect tool. * @param brushSize The brush size. */ -export const renderToolPreview = ( +const toolPreview = ( stage: Konva.Stage, tool: Tool, color: RgbColor | null, @@ -197,7 +200,7 @@ export const renderToolPreview = ( } }; -const renderVectorMaskLayer = ( +const vectorMaskLayer = ( stage: Konva.Stage, vmLayer: VectorMaskLayer, vmLayerIndex: number, @@ -369,7 +372,7 @@ const renderVectorMaskLayer = ( * @param onLayerPosChanged Callback for when the layer's position changes. This is optional to allow for offscreen rendering. * @returns */ -export const renderLayers = ( +const layers = ( stage: Konva.Stage, reduxLayers: Layer[], globalMaskLayerOpacity: number, @@ -389,7 +392,7 @@ export const renderLayers = ( const reduxLayer = reduxLayers[layerIndex]; assert(reduxLayer, `Layer at index ${layerIndex} is undefined`); if (isVectorMaskLayer(reduxLayer)) { - renderVectorMaskLayer(stage, reduxLayer, layerIndex, globalMaskLayerOpacity, tool, onLayerPosChanged); + vectorMaskLayer(stage, reduxLayer, layerIndex, globalMaskLayerOpacity, tool, onLayerPosChanged); } } }; @@ -402,7 +405,7 @@ export const renderLayers = ( * @param onBboxChanged A callback to be called when the bounding box changes. * @returns */ -export const renderBbox = ( +const bbox = ( stage: Konva.Stage, reduxLayers: Layer[], selectedLayerId: string | null, @@ -478,7 +481,7 @@ export const renderBbox = ( } }; -export const renderBackground = (stage: Konva.Stage, width: number, height: number) => { +const background = (stage: Konva.Stage, width: number, height: number) => { let layer = stage.findOne(`#${BACKGROUND_LAYER_ID}`); if (!layer) { @@ -501,8 +504,7 @@ export const renderBackground = (stage: Konva.Stage, width: number, height: numb 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='; + image.src = STAGE_BG_DATAURL; } const background = layer.findOne(`#${BACKGROUND_RECT_ID}`); @@ -525,3 +527,16 @@ export const renderBackground = (stage: Konva.Stage, width: number, height: numb // Apply that movement to the fill pattern background.fillPatternOffset(stagePos); }; + +const DEBOUNCE_MS = 100; + +export const renderers = { + toolPreview, + toolPreviewDebounced: debounce(toolPreview, DEBOUNCE_MS), + layers, + layersDebounced: debounce(layers, DEBOUNCE_MS), + bbox, + bboxDebounced: debounce(bbox, DEBOUNCE_MS), + background, + backgroundDebounced: debounce(background, DEBOUNCE_MS), +}; From 5b0f4b298ed8a5459bb01c0a63821ada92a85fe2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:41:59 +1000 Subject: [PATCH 3/9] perf(ui): add brush spacing Only add point to line if the next point is 10 or more px from the last point --- .../features/regionalPrompts/hooks/mouseEventHooks.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts index 7ce11ccf289..33bcd575237 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts @@ -12,7 +12,7 @@ import { } from 'features/regionalPrompts/store/regionalPromptsSlice'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; -import { useCallback } from 'react'; +import { useCallback, useRef } from 'react'; const getIsFocused = (stage: Konva.Stage) => { return stage.container().contains(document.activeElement); @@ -48,6 +48,7 @@ export const useMouseEvents = () => { const dispatch = useAppDispatch(); const selectedLayerId = useAppSelector((s) => s.regionalPrompts.present.selectedLayerId); const tool = useStore($tool); + const lastCursorPosRef = useRef<[number, number] | null>(null); const onMouseDown = useCallback( (e: KonvaEventObject) => { @@ -117,7 +118,13 @@ export const useMouseEvents = () => { return; } if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) { - dispatch(maskLayerPointsAdded({ layerId: selectedLayerId, point: [Math.floor(pos.x), Math.floor(pos.y)] })); + if (lastCursorPosRef.current) { + if (Math.hypot(lastCursorPosRef.current[0] - pos.x, lastCursorPosRef.current[1] - pos.y) < 10) { + return; + } + } + lastCursorPosRef.current = [Math.floor(pos.x), Math.floor(pos.y)]; + dispatch(maskLayerPointsAdded({ layerId: selectedLayerId, point: lastCursorPosRef.current })); } }, [dispatch, selectedLayerId, tool] From 5e53c81f89bd2a71573e7231e2cef988d928323d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:42:58 +1000 Subject: [PATCH 4/9] perf(ui): debounce render wait = 300ms --- .../frontend/web/src/features/regionalPrompts/util/renderers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts index ae1c2a9d38f..80bb1ddad5f 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts @@ -528,7 +528,7 @@ const background = (stage: Konva.Stage, width: number, height: number) => { background.fillPatternOffset(stagePos); }; -const DEBOUNCE_MS = 100; +const DEBOUNCE_MS = 300; export const renderers = { toolPreview, From 3591525728d2f165d549e5225fc012dc80c07e71 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:50:48 +1000 Subject: [PATCH 5/9] tidy(ui): memo aspectratiopreview --- .../components/ImageSize/AspectRatioPreview.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx index dcef435c390..4825d73bb5e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx @@ -1,10 +1,13 @@ import { Flex } from '@invoke-ai/ui-library'; import { StageComponent } from 'features/regionalPrompts/components/StageComponent'; +import { memo } from 'react'; -export const AspectRatioPreview = () => { +export const AspectRatioPreview = memo(() => { return ( - + ); -}; +}); + +AspectRatioPreview.displayName = 'AspectRatioPreview'; From 3d9cb8737733d70fe482f64e7fa3116f9dc60ead Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:50:57 +1000 Subject: [PATCH 6/9] chore(ui): lint --- invokeai/frontend/web/package.json | 1 - invokeai/frontend/web/pnpm-lock.yaml | 3 --- .../components/ImageSize/constants.ts | 24 ------------------- 3 files changed, 28 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index a591e654a7b..b7e954035f9 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -51,7 +51,6 @@ } }, "dependencies": { - "@chakra-ui/react-use-size": "^2.1.0", "@dagrejs/dagre": "^1.1.1", "@dagrejs/graphlib": "^2.2.1", "@dnd-kit/core": "^6.1.0", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index c0cbc59ad26..fb2cad51a9a 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -8,9 +8,6 @@ dependencies: '@chakra-ui/react': specifier: ^2.8.2 version: 2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-use-size': - specifier: ^2.1.0 - version: 2.1.0(react@18.2.0) '@dagrejs/dagre': specifier: ^1.1.1 version: 1.1.1 diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/constants.ts b/invokeai/frontend/web/src/features/parameters/components/ImageSize/constants.ts index b8c46005e66..8ecdf2bc1bb 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/constants.ts +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/constants.ts @@ -2,30 +2,6 @@ import type { ComboboxOption } from '@invoke-ai/ui-library'; import type { AspectRatioID, AspectRatioState } from './types'; -// When the aspect ratio is between these two values, we show the icon (experimentally determined) -export const ICON_LOW_CUTOFF = 0.23; -export const ICON_HIGH_CUTOFF = 1 / ICON_LOW_CUTOFF; -const ICON_SIZE_PX = 64; -const ICON_PADDING_PX = 16; -export const BOX_SIZE_CSS_CALC = `min(${ICON_SIZE_PX}px, calc(100% - ${ICON_PADDING_PX}px))`; -export const MOTION_ICON_INITIAL = { - opacity: 0, -}; -export const MOTION_ICON_ANIMATE = { - opacity: 1, - transition: { duration: 0.1 }, -}; -export const MOTION_ICON_EXIT = { - opacity: 0, - transition: { duration: 0.1 }, -}; -export const ICON_CONTAINER_STYLES = { - width: '100%', - height: '100%', - alignItems: 'center', - justifyContent: 'center', -}; - export const ASPECT_RATIO_OPTIONS: ComboboxOption[] = [ { label: 'Free' as const, value: 'Free' }, { label: '16:9' as const, value: '16:9' }, From 1bef74a97fe1d731c76d6ecbea74dd20bd63e15d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:11:40 +1000 Subject: [PATCH 7/9] feat(ui): rp hotkeys - Shift+C: Reset selected layer mask (same as canvas) - Shift+D: Delete selected layer (cannot be Del, that deletes an image in gallery) - Shift+A: Add layer (cannot be Ctrl+Shift+N, that opens a new window) - Ctrl/Cmd+Wheel: Brush size (same as canvas) --- .../features/canvas/hooks/useCanvasZoom.ts | 22 ++++++++------- .../components/StageComponent.tsx | 6 +++-- .../components/ToolChooser.tsx | 25 +++++++++++++++-- .../regionalPrompts/hooks/mouseEventHooks.ts | 27 ++++++++++++++++++- .../store/regionalPromptsSlice.ts | 25 +++++++++++++---- 5 files changed, 86 insertions(+), 19 deletions(-) diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts index ef6a74ae9cb..1434bc9afc5 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts @@ -10,6 +10,18 @@ import { clamp } from 'lodash-es'; import type { MutableRefObject } from 'react'; import { useCallback } from 'react'; +export const calculateNewBrushSize = (brushSize: number, delta: number) => { + // This equation was derived by fitting a curve to the desired brush sizes and deltas + // see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565 + const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize); + // This needs to be clamped to prevent the delta from getting too large + const finalDelta = clamp(targetDelta, -20, 20); + // The new brush size is also clamped to prevent it from getting too large or small + const newBrushSize = clamp(brushSize + finalDelta, 1, 500); + + return newBrushSize; +}; + const useCanvasWheel = (stageRef: MutableRefObject) => { const dispatch = useAppDispatch(); const stageScale = useAppSelector((s) => s.canvas.stageScale); @@ -36,15 +48,7 @@ const useCanvasWheel = (stageRef: MutableRefObject) => { } if ($ctrl.get() || $meta.get()) { - // This equation was derived by fitting a curve to the desired brush sizes and deltas - // see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565 - const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize); - // This needs to be clamped to prevent the delta from getting too large - const finalDelta = clamp(targetDelta, -20, 20); - // The new brush size is also clamped to prevent it from getting too large or small - const newBrushSize = clamp(brushSize + finalDelta, 1, 500); - - dispatch(setBrushSize(newBrushSize)); + dispatch(setBrushSize(calculateNewBrushSize(brushSize, delta))); } else { const cursorPos = stageRef.current.getPointerPosition(); let delta = e.evt.deltaY; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx index 1f69b37bbef..dbdfa7c5634 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx @@ -47,7 +47,7 @@ const useStageRenderer = ( const height = useAppSelector((s) => s.generation.height); const state = useAppSelector((s) => s.regionalPrompts.present); const tool = useStore($tool); - const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave } = useMouseEvents(); + const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel } = useMouseEvents(); const cursorPosition = useStore($cursorPosition); const lastMouseDownPos = useStore($lastMouseDownPos); const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor); @@ -106,6 +106,7 @@ const useStageRenderer = ( stageRef.current.on('mousemove', onMouseMove); stageRef.current.on('mouseenter', onMouseEnter); stageRef.current.on('mouseleave', onMouseLeave); + stageRef.current.on('wheel', onMouseWheel); const stage = stageRef.current; return () => { @@ -115,8 +116,9 @@ const useStageRenderer = ( stage.off('mousemove', onMouseMove); stage.off('mouseenter', onMouseEnter); stage.off('mouseleave', onMouseLeave); + stage.off('wheel', onMouseWheel); }; - }, [stageRef, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave]); + }, [stageRef, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel]); useLayoutEffect(() => { log.trace('Updating stage dimensions'); diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx index 816f10f34d5..a79c443a2fe 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx @@ -1,7 +1,12 @@ import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { useAppSelector } from 'app/store/storeHooks'; -import { $tool } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + $tool, + layerAdded, + selectedLayerDeleted, + selectedLayerReset, +} from 'features/regionalPrompts/store/regionalPromptsSlice'; import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -9,6 +14,7 @@ import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBol export const ToolChooser: React.FC = () => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); const isDisabled = useAppSelector((s) => s.regionalPrompts.present.layers.length === 0); const tool = useStore($tool); @@ -29,6 +35,21 @@ export const ToolChooser: React.FC = () => { }, []); useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]); + const resetSelectedLayer = useCallback(() => { + dispatch(selectedLayerReset()); + }, [dispatch]); + useHotkeys('shift+c', resetSelectedLayer); + + const addLayer = useCallback(() => { + dispatch(layerAdded('vector_mask_layer')); + }, [dispatch]); + useHotkeys('shift+a', addLayer); + + const deleteSelectedLayer = useCallback(() => { + dispatch(selectedLayerDeleted()); + }, [dispatch]); + useHotkeys('shift+d', deleteSelectedLayer); + return ( { const selectedLayerId = useAppSelector((s) => s.regionalPrompts.present.selectedLayerId); const tool = useStore($tool); const lastCursorPosRef = useRef<[number, number] | null>(null); + const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); + const brushSize = useAppSelector((s) => s.regionalPrompts.present.brushSize); const onMouseDown = useCallback( (e: KonvaEventObject) => { @@ -175,5 +180,25 @@ export const useMouseEvents = () => { [dispatch, selectedLayerId, tool] ); - return { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave }; + const onMouseWheel = useCallback( + (e: KonvaEventObject) => { + e.evt.preventDefault(); + + // checking for ctrl key is pressed or not, + // so that brush size can be controlled using ctrl + scroll up/down + + // Invert the delta if the property is set to true + let delta = e.evt.deltaY; + if (shouldInvertBrushSizeScrollDirection) { + delta = -delta; + } + + if ($ctrl.get() || $meta.get()) { + dispatch(brushSizeChanged(calculateNewBrushSize(brushSize, delta))); + } + }, + [shouldInvertBrushSizeScrollDirection, brushSize, dispatch] + ); + + return { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel }; }; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts index 4ca70b488bc..30130484707 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts @@ -78,6 +78,13 @@ export const initialRegionalPromptsState: RegionalPromptsState = { const isLine = (obj: VectorMaskLine | VectorMaskRect): obj is VectorMaskLine => obj.type === 'vector_mask_line'; export const isVectorMaskLayer = (layer?: Layer): layer is VectorMaskLayer => layer?.type === 'vector_mask_layer'; +const resetLayer = (layer: VectorMaskLayer) => { + layer.objects = []; + layer.bbox = null; + layer.isVisible = true; + layer.needsPixelBbox = false; + layer.bboxNeedsUpdate = false; +}; export const regionalPromptsSlice = createSlice({ name: 'regionalPrompts', @@ -144,11 +151,7 @@ export const regionalPromptsSlice = createSlice({ layerReset: (state, action: PayloadAction) => { const layer = state.layers.find((l) => l.id === action.payload); if (layer) { - layer.objects = []; - layer.bbox = null; - layer.isVisible = true; - layer.needsPixelBbox = false; - layer.bboxNeedsUpdate = false; + resetLayer(layer); } }, layerDeleted: (state, action: PayloadAction) => { @@ -177,6 +180,16 @@ export const regionalPromptsSlice = createSlice({ state.layers = []; state.selectedLayerId = null; }, + selectedLayerReset: (state) => { + const layer = state.layers.find((l) => l.id === state.selectedLayerId); + if (layer) { + resetLayer(layer); + } + }, + selectedLayerDeleted: (state) => { + state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId); + state.selectedLayerId = state.layers[0]?.id ?? null; + }, //#endregion //#region Mask Layers @@ -370,6 +383,8 @@ export const { layerBboxChanged, layerVisibilityToggled, allLayersDeleted, + selectedLayerReset, + selectedLayerDeleted, // Mask layer actions maskLayerLineAdded, maskLayerPointsAdded, From 90be7a915d8ed91e9bd597b06e2dae42efe07fdc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:26:12 +1000 Subject: [PATCH 8/9] fix(ui): minor canvas overflow --- .../regionalPrompts/components/StageComponent.tsx | 14 +------------- .../features/ui/components/tabs/TextToImageTab.tsx | 2 +- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx index dbdfa7c5634..ee32a12bbbe 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx @@ -1,4 +1,3 @@ -import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { logger } from 'app/logging/logger'; @@ -218,21 +217,10 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { useStageRenderer(stageRef, container, wrapper, asPreview); - const sx = useMemo( - () => ({ - bg: 'base.850', - p: asPreview ? 0 : 2, - borderRadius: asPreview ? 0 : 'base', - borderWidth: asPreview ? 0 : 1, - w: 'min-content', - h: 'min-content', - }), - [asPreview] - ); return ( - + ); 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 733c7f7b2e5..2a79c9b9d18 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx @@ -17,7 +17,7 @@ const TextToImageTab = () => { {regionalControlTitle} - + From eb05d3397f948d60f6a0f2ac4839ec065ad4d023 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:47:01 +1000 Subject: [PATCH 9/9] tidy(ui): remove extraneous cursor sync --- .../regionalPrompts/hooks/mouseEventHooks.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts index ba8f5a1498e..c0fdfc7c727 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts @@ -38,15 +38,6 @@ export const getScaledFlooredCursorPosition = (stage: Konva.Stage) => { }; }; -const syncCursorPos = (stage: Konva.Stage) => { - const pos = getScaledFlooredCursorPosition(stage); - if (!pos) { - return null; - } - $cursorPosition.set(pos); - return pos; -}; - export const useMouseEvents = () => { const dispatch = useAppDispatch(); const selectedLayerId = useAppSelector((s) => s.regionalPrompts.present.selectedLayerId); @@ -61,7 +52,7 @@ export const useMouseEvents = () => { if (!stage) { return; } - const pos = syncCursorPos(stage); + const pos = $cursorPosition.get(); if (!pos) { return; } @@ -118,13 +109,14 @@ export const useMouseEvents = () => { if (!stage) { return; } - const pos = syncCursorPos(stage); + const pos = getScaledFlooredCursorPosition(stage); if (!pos || !selectedLayerId) { return; } + $cursorPosition.set(pos); if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) { if (lastCursorPosRef.current) { - if (Math.hypot(lastCursorPosRef.current[0] - pos.x, lastCursorPosRef.current[1] - pos.y) < 10) { + if (Math.hypot(lastCursorPosRef.current[0] - pos.x, lastCursorPosRef.current[1] - pos.y) < 20) { return; } } @@ -152,7 +144,7 @@ export const useMouseEvents = () => { return; } $isMouseOver.set(true); - const pos = syncCursorPos(stage); + const pos = $cursorPosition.get(); if (!pos) { return; }