mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Inpainting Controls Code Spitting and Performance
Codesplit the entirety of the inpainting controls. Created new selectors for each and every component to ensure there are no unnecessary re-renders. App feels a lot smoother.
This commit is contained in:
parent
868e4b2db8
commit
db46e12f2b
File diff suppressed because one or more lines are too long
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||
<script type="module" crossorigin src="./assets/index.9ad2f384.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index.ae6d2a5e.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.14c578ee.css">
|
||||
</head>
|
||||
|
||||
|
@ -1,442 +1,53 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
FaEraser,
|
||||
FaMask,
|
||||
FaPaintBrush,
|
||||
FaPalette,
|
||||
FaPlus,
|
||||
FaRedo,
|
||||
FaTrash,
|
||||
FaUndo,
|
||||
} from 'react-icons/fa';
|
||||
import { BiHide, BiShow } from 'react-icons/bi';
|
||||
import { VscSplitHorizontal } from 'react-icons/vsc';
|
||||
import { useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||
import {
|
||||
clearMask,
|
||||
redo,
|
||||
setMaskColor,
|
||||
setBrushSize,
|
||||
setShouldShowBrushPreview,
|
||||
setTool,
|
||||
undo,
|
||||
setShouldShowMask,
|
||||
setShouldInvertMask,
|
||||
setNeedsCache,
|
||||
toggleShouldLockBoundingBox,
|
||||
clearImageToInpaint,
|
||||
} from './inpaintingSlice';
|
||||
|
||||
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
|
||||
import IAISlider from '../../../common/components/IAISlider';
|
||||
import IAINumberInput from '../../../common/components/IAINumberInput';
|
||||
import { inpaintingControlsSelector } from './inpaintingSliceSelectors';
|
||||
import IAIPopover from '../../../common/components/IAIPopover';
|
||||
import IAIColorPicker from '../../../common/components/IAIColorPicker';
|
||||
import { RgbaColor } from 'react-colorful';
|
||||
import { setShowDualDisplay } from '../../options/optionsSlice';
|
||||
import { useState } from 'react';
|
||||
import InpaintingBrushControl from './InpaintingControls/InpaintingBrushControl';
|
||||
import InpaintingEraserControl from './InpaintingControls/InpaintingEraserControl';
|
||||
import InpaintingMaskControl from './InpaintingControls/InpaintingMaskControl';
|
||||
import InpaintingUndoControl from './InpaintingControls/InpaintingUndoControl';
|
||||
import InpaintingRedoControl from './InpaintingControls/InpaintingRedoControl';
|
||||
import InpaintingClearImageControl from './InpaintingControls/InpaintingClearImageControl';
|
||||
import InpaintingSplitLayoutControl from './InpaintingControls/InpaintingSplitLayoutControl';
|
||||
|
||||
const InpaintingControls = () => {
|
||||
const {
|
||||
tool,
|
||||
brushSize,
|
||||
maskColor,
|
||||
shouldInvertMask,
|
||||
shouldShowMask,
|
||||
canUndo,
|
||||
canRedo,
|
||||
isMaskEmpty,
|
||||
activeTabName,
|
||||
showDualDisplay,
|
||||
} = useAppSelector(inpaintingControlsSelector);
|
||||
// const { shouldShowMask, activeTabName } = useAppSelector(
|
||||
// inpaintingControlsSelector
|
||||
// );
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
// Button State Controllers
|
||||
const [maskOptionsOpen, setMaskOptionsOpen] = useState<boolean>(false);
|
||||
// const dispatch = useAppDispatch();
|
||||
|
||||
/**
|
||||
* Hotkeys
|
||||
*/
|
||||
|
||||
// Decrease brush size
|
||||
useHotkeys(
|
||||
'[',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
if (brushSize - 5 > 0) {
|
||||
handleChangeBrushSize(brushSize - 5);
|
||||
} else {
|
||||
handleChangeBrushSize(1);
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, brushSize]
|
||||
);
|
||||
|
||||
// Increase brush size
|
||||
useHotkeys(
|
||||
']',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeBrushSize(brushSize + 5);
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, brushSize]
|
||||
);
|
||||
|
||||
// Decrease mask opacity
|
||||
useHotkeys(
|
||||
'shift+[',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeMaskColor({
|
||||
...maskColor,
|
||||
a: Math.max(maskColor.a - 0.05, 0),
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, maskColor.a]
|
||||
);
|
||||
|
||||
// Increase mask opacity
|
||||
useHotkeys(
|
||||
'shift+]',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeMaskColor({
|
||||
...maskColor,
|
||||
a: Math.min(maskColor.a + 0.05, 100),
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, maskColor.a]
|
||||
);
|
||||
|
||||
// Set tool to eraser
|
||||
useHotkeys(
|
||||
'e',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
if (activeTabName !== 'inpainting' || !shouldShowMask) return;
|
||||
handleSelectEraserTool();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
// Set tool to brush
|
||||
useHotkeys(
|
||||
'b',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleSelectBrushTool();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
// Toggle lock bounding box
|
||||
useHotkeys(
|
||||
'shift+q',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
dispatch(toggleShouldLockBoundingBox());
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
// Undo
|
||||
useHotkeys(
|
||||
'cmd+z, control+z',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleUndo();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && canUndo,
|
||||
},
|
||||
[activeTabName, shouldShowMask, canUndo]
|
||||
);
|
||||
|
||||
// Redo
|
||||
useHotkeys(
|
||||
'cmd+shift+z, control+shift+z, control+y, cmd+y',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleRedo();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && canRedo,
|
||||
},
|
||||
[activeTabName, shouldShowMask, canRedo]
|
||||
);
|
||||
|
||||
// Show/hide mask
|
||||
useHotkeys(
|
||||
'h',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleToggleShouldShowMask();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting',
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
// Invert mask
|
||||
useHotkeys(
|
||||
'shift+m',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleToggleShouldInvertMask();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldInvertMask, shouldShowMask]
|
||||
);
|
||||
|
||||
// Clear mask
|
||||
useHotkeys(
|
||||
'shift+c',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleClearMask();
|
||||
toast({
|
||||
title: 'Mask Cleared',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && !isMaskEmpty,
|
||||
},
|
||||
[activeTabName, isMaskEmpty, shouldShowMask]
|
||||
);
|
||||
|
||||
// Toggle split view
|
||||
useHotkeys(
|
||||
'shift+j',
|
||||
() => {
|
||||
handleDualDisplay();
|
||||
},
|
||||
[showDualDisplay]
|
||||
);
|
||||
|
||||
const handleClearMask = () => {
|
||||
dispatch(clearMask());
|
||||
};
|
||||
|
||||
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
||||
|
||||
const handleSelectBrushTool = () => dispatch(setTool('brush'));
|
||||
|
||||
const handleChangeBrushSize = (v: number) => {
|
||||
dispatch(setShouldShowBrushPreview(true));
|
||||
dispatch(setBrushSize(v));
|
||||
};
|
||||
|
||||
const handleToggleShouldShowMask = () =>
|
||||
dispatch(setShouldShowMask(!shouldShowMask));
|
||||
|
||||
const handleToggleShouldInvertMask = () =>
|
||||
dispatch(setShouldInvertMask(!shouldInvertMask));
|
||||
|
||||
const handleShowBrushPreview = () => {
|
||||
dispatch(setShouldShowBrushPreview(true));
|
||||
};
|
||||
|
||||
const handleHideBrushPreview = () => {
|
||||
dispatch(setShouldShowBrushPreview(false));
|
||||
};
|
||||
|
||||
const handleChangeMaskColor = (newColor: RgbaColor) => {
|
||||
dispatch(setMaskColor(newColor));
|
||||
};
|
||||
|
||||
const handleUndo = () => dispatch(undo());
|
||||
|
||||
const handleRedo = () => dispatch(redo());
|
||||
|
||||
const handleDualDisplay = () => {
|
||||
dispatch(setShowDualDisplay(!showDualDisplay));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const handleClearImage = () => {
|
||||
dispatch(clearImageToInpaint());
|
||||
};
|
||||
// useHotkeys(
|
||||
// 'shift+q',
|
||||
// (e: KeyboardEvent) => {
|
||||
// e.preventDefault();
|
||||
// dispatch(toggleShouldLockBoundingBox());
|
||||
// },
|
||||
// {
|
||||
// enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
// },
|
||||
// [activeTabName, shouldShowMask]
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className="inpainting-settings">
|
||||
<div className="inpainting-buttons-group">
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
onOpen={handleShowBrushPreview}
|
||||
onClose={handleHideBrushPreview}
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Brush (B)"
|
||||
tooltip="Brush (B)"
|
||||
icon={<FaPaintBrush />}
|
||||
onClick={handleSelectBrushTool}
|
||||
data-selected={tool === 'brush'}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="inpainting-slider-numberinput">
|
||||
<IAISlider
|
||||
label="Brush Size"
|
||||
value={brushSize}
|
||||
onChange={handleChangeBrushSize}
|
||||
min={1}
|
||||
max={200}
|
||||
width="100px"
|
||||
focusThumbOnChange={false}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
<IAINumberInput
|
||||
value={brushSize}
|
||||
onChange={handleChangeBrushSize}
|
||||
width={'80px'}
|
||||
min={1}
|
||||
max={999}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
</div>
|
||||
</IAIPopover>
|
||||
<IAIIconButton
|
||||
aria-label="Eraser (E)"
|
||||
tooltip="Eraser (E)"
|
||||
icon={<FaEraser />}
|
||||
onClick={handleSelectEraserTool}
|
||||
data-selected={tool === 'eraser'}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
<InpaintingBrushControl />
|
||||
<InpaintingEraserControl />
|
||||
</div>
|
||||
<div className="inpainting-buttons-group">
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
onOpen={() => setMaskOptionsOpen(true)}
|
||||
onClose={() => setMaskOptionsOpen(false)}
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Mask Options"
|
||||
tooltip="Mask Options"
|
||||
icon={<FaMask />}
|
||||
cursor={'pointer'}
|
||||
data-selected={maskOptionsOpen}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="inpainting-button-dropdown">
|
||||
<IAIIconButton
|
||||
aria-label="Hide/Show Mask (H)"
|
||||
tooltip="Hide/Show Mask (H)"
|
||||
data-selected={!shouldShowMask}
|
||||
icon={
|
||||
shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />
|
||||
}
|
||||
onClick={handleToggleShouldShowMask}
|
||||
/>
|
||||
<IAIIconButton
|
||||
tooltip="Invert Mask Display (Shift+M)"
|
||||
aria-label="Invert Mask Display (Shift+M)"
|
||||
data-selected={shouldInvertMask}
|
||||
icon={
|
||||
shouldInvertMask ? (
|
||||
<MdInvertColors size={22} />
|
||||
) : (
|
||||
<MdInvertColorsOff size={22} />
|
||||
)
|
||||
}
|
||||
onClick={handleToggleShouldInvertMask}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
styleClass="inpainting-color-picker"
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Mask Color"
|
||||
tooltip="Mask Color"
|
||||
icon={<FaPalette />}
|
||||
isDisabled={!shouldShowMask}
|
||||
cursor={'pointer'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IAIColorPicker
|
||||
color={maskColor}
|
||||
onChange={handleChangeMaskColor}
|
||||
/>
|
||||
</IAIPopover>
|
||||
</div>
|
||||
</IAIPopover>
|
||||
<IAIIconButton
|
||||
aria-label="Clear Mask (Shift+C)"
|
||||
tooltip="Clear Mask (Shift+C)"
|
||||
icon={<FaPlus size={18} style={{ transform: 'rotate(45deg)' }} />}
|
||||
onClick={handleClearMask}
|
||||
isDisabled={isMaskEmpty || !shouldShowMask}
|
||||
/>
|
||||
<InpaintingMaskControl />
|
||||
</div>
|
||||
<div className="inpainting-buttons-group">
|
||||
<IAIIconButton
|
||||
aria-label="Undo"
|
||||
tooltip="Undo"
|
||||
icon={<FaUndo />}
|
||||
onClick={handleUndo}
|
||||
isDisabled={!canUndo || !shouldShowMask}
|
||||
/>
|
||||
<IAIIconButton
|
||||
aria-label="Redo"
|
||||
tooltip="Redo"
|
||||
icon={<FaRedo />}
|
||||
onClick={handleRedo}
|
||||
isDisabled={!canRedo || !shouldShowMask}
|
||||
/>
|
||||
<InpaintingUndoControl />
|
||||
<InpaintingRedoControl />
|
||||
</div>
|
||||
|
||||
<div className="inpainting-buttons-group">
|
||||
<IAIIconButton
|
||||
aria-label="Clear Image"
|
||||
tooltip="Clear Image"
|
||||
icon={<FaTrash size={16} />}
|
||||
onClick={handleClearImage}
|
||||
/>
|
||||
<InpaintingClearImageControl />
|
||||
</div>
|
||||
<IAIIconButton
|
||||
aria-label="Split Layout (Shift+J)"
|
||||
tooltip="Split Layout (Shift+J)"
|
||||
icon={<VscSplitHorizontal />}
|
||||
data-selected={showDualDisplay}
|
||||
onClick={handleDualDisplay}
|
||||
/>
|
||||
<InpaintingSplitLayoutControl />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,148 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { FaPaintBrush } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import IAIPopover from '../../../../common/components/IAIPopover';
|
||||
import IAISlider from '../../../../common/components/IAISlider';
|
||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||
|
||||
import {
|
||||
InpaintingState,
|
||||
setBrushSize,
|
||||
setShouldShowBrushPreview,
|
||||
setTool,
|
||||
} from '../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
const inpaintingBrushSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { tool, brushSize, shouldShowMask } = inpainting;
|
||||
|
||||
return {
|
||||
tool,
|
||||
brushSize,
|
||||
shouldShowMask,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingBrushControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { tool, brushSize, shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingBrushSelector
|
||||
);
|
||||
|
||||
const handleSelectBrushTool = () => dispatch(setTool('brush'));
|
||||
|
||||
const handleShowBrushPreview = () => {
|
||||
dispatch(setShouldShowBrushPreview(true));
|
||||
};
|
||||
|
||||
const handleHideBrushPreview = () => {
|
||||
dispatch(setShouldShowBrushPreview(false));
|
||||
};
|
||||
|
||||
const handleChangeBrushSize = (v: number) => {
|
||||
dispatch(setShouldShowBrushPreview(true));
|
||||
dispatch(setBrushSize(v));
|
||||
};
|
||||
|
||||
// Hotkeys
|
||||
|
||||
// Decrease brush size
|
||||
useHotkeys(
|
||||
'[',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
if (brushSize - 5 > 0) {
|
||||
handleChangeBrushSize(brushSize - 5);
|
||||
} else {
|
||||
handleChangeBrushSize(1);
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, brushSize]
|
||||
);
|
||||
|
||||
// Increase brush size
|
||||
useHotkeys(
|
||||
']',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeBrushSize(brushSize + 5);
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, brushSize]
|
||||
);
|
||||
|
||||
// Set tool to brush
|
||||
useHotkeys(
|
||||
'b',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleSelectBrushTool();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
onOpen={handleShowBrushPreview}
|
||||
onClose={handleHideBrushPreview}
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Brush (B)"
|
||||
tooltip="Brush (B)"
|
||||
icon={<FaPaintBrush />}
|
||||
onClick={handleSelectBrushTool}
|
||||
data-selected={tool === 'brush'}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="inpainting-slider-numberinput">
|
||||
<IAISlider
|
||||
label="Brush Size"
|
||||
value={brushSize}
|
||||
onChange={handleChangeBrushSize}
|
||||
min={1}
|
||||
max={200}
|
||||
width="100px"
|
||||
focusThumbOnChange={false}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
<IAINumberInput
|
||||
value={brushSize}
|
||||
onChange={handleChangeBrushSize}
|
||||
width={'80px'}
|
||||
min={1}
|
||||
max={999}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
</div>
|
||||
</IAIPopover>
|
||||
);
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { useAppDispatch } from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { clearImageToInpaint } from '../inpaintingSlice';
|
||||
|
||||
export default function InpaintingClearImageControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClearImage = () => {
|
||||
dispatch(clearImageToInpaint());
|
||||
};
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Clear Image"
|
||||
tooltip="Clear Image"
|
||||
icon={<FaTrash size={16} />}
|
||||
onClick={handleClearImage}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { FaEraser } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { InpaintingState, setTool } from '../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||
|
||||
const inpaintingEraserSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { tool, shouldShowMask } = inpainting;
|
||||
|
||||
return {
|
||||
tool,
|
||||
shouldShowMask,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingEraserControl() {
|
||||
const { tool, shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingEraserSelector
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
||||
|
||||
// Hotkeys
|
||||
// Set tool to eraser
|
||||
useHotkeys(
|
||||
'e',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
if (activeTabName !== 'inpainting' || !shouldShowMask) return;
|
||||
handleSelectEraserTool();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Eraser (E)"
|
||||
tooltip="Eraser (E)"
|
||||
icon={<FaEraser />}
|
||||
onClick={handleSelectEraserTool}
|
||||
data-selected={tool === 'eraser'}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FaMask } from 'react-icons/fa';
|
||||
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import IAIPopover from '../../../../common/components/IAIPopover';
|
||||
|
||||
import InpaintingMaskVisibilityControl from './InpaintingMaskControls/InpaintingMaskVisibilityControl';
|
||||
import InpaintingMaskInvertControl from './InpaintingMaskControls/InpaintingMaskInvertControl';
|
||||
import InpaintingMaskColorPicker from './InpaintingMaskControls/InpaintingMaskColorPicker';
|
||||
import InpaintingMaskClear from './InpaintingMaskControls/InpaintingMaskClear';
|
||||
|
||||
export default function InpaintingMaskControl() {
|
||||
const [maskOptionsOpen, setMaskOptionsOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
onOpen={() => setMaskOptionsOpen(true)}
|
||||
onClose={() => setMaskOptionsOpen(false)}
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Mask Options"
|
||||
tooltip="Mask Options"
|
||||
icon={<FaMask />}
|
||||
cursor={'pointer'}
|
||||
data-selected={maskOptionsOpen}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="inpainting-button-dropdown">
|
||||
<InpaintingMaskVisibilityControl />
|
||||
<InpaintingMaskInvertControl />
|
||||
<InpaintingMaskColorPicker />
|
||||
</div>
|
||||
</IAIPopover>
|
||||
<InpaintingMaskClear />
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
||||
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
|
||||
import { clearMask, InpaintingState } from '../../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
|
||||
const inpaintingMaskClearSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { shouldShowMask, lines } = inpainting;
|
||||
|
||||
return { shouldShowMask, activeTabName, isMaskEmpty: lines.length === 0 };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingMaskClear() {
|
||||
const { shouldShowMask, activeTabName, isMaskEmpty } = useAppSelector(
|
||||
inpaintingMaskClearSelector
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
const handleClearMask = () => {
|
||||
dispatch(clearMask());
|
||||
};
|
||||
|
||||
// Clear mask
|
||||
useHotkeys(
|
||||
'shift+c',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleClearMask();
|
||||
toast({
|
||||
title: 'Mask Cleared',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && !isMaskEmpty,
|
||||
},
|
||||
[activeTabName, isMaskEmpty, shouldShowMask]
|
||||
);
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Clear Mask (Shift+C)"
|
||||
tooltip="Clear Mask (Shift+C)"
|
||||
icon={<FaPlus size={18} style={{ transform: 'rotate(45deg)' }} />}
|
||||
onClick={handleClearMask}
|
||||
isDisabled={isMaskEmpty || !shouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { RgbaColor } from 'react-colorful';
|
||||
import { FaPalette } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAIColorPicker from '../../../../../common/components/IAIColorPicker';
|
||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
||||
import IAIPopover from '../../../../../common/components/IAIPopover';
|
||||
import { InpaintingState, setMaskColor } from '../../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const inpaintingMaskColorPickerSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { shouldShowMask, maskColor } = inpainting;
|
||||
|
||||
return { shouldShowMask, maskColor, activeTabName };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingMaskColorPicker() {
|
||||
const { shouldShowMask, maskColor, activeTabName } = useAppSelector(
|
||||
inpaintingMaskColorPickerSelector
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
const handleChangeMaskColor = (newColor: RgbaColor) => {
|
||||
dispatch(setMaskColor(newColor));
|
||||
};
|
||||
|
||||
// Hotkeys
|
||||
// Decrease mask opacity
|
||||
useHotkeys(
|
||||
'shift+[',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeMaskColor({
|
||||
...maskColor,
|
||||
a: Math.max(maskColor.a - 0.05, 0),
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, maskColor.a]
|
||||
);
|
||||
|
||||
// Increase mask opacity
|
||||
useHotkeys(
|
||||
'shift+]',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeMaskColor({
|
||||
...maskColor,
|
||||
a: Math.min(maskColor.a + 0.05, 100),
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, maskColor.a]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
styleClass="inpainting-color-picker"
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Mask Color"
|
||||
tooltip="Mask Color"
|
||||
icon={<FaPalette />}
|
||||
isDisabled={!shouldShowMask}
|
||||
cursor={'pointer'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IAIColorPicker color={maskColor} onChange={handleChangeMaskColor} />
|
||||
</IAIPopover>
|
||||
);
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
||||
import { InpaintingState, setShouldInvertMask } from '../../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const inpaintingMaskInvertSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { shouldShowMask, shouldInvertMask } = inpainting;
|
||||
|
||||
return { shouldInvertMask, shouldShowMask, activeTabName };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingMaskInvertControl() {
|
||||
const { shouldInvertMask, shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingMaskInvertSelector
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleToggleShouldInvertMask = () =>
|
||||
dispatch(setShouldInvertMask(!shouldInvertMask));
|
||||
|
||||
// Invert mask
|
||||
useHotkeys(
|
||||
'shift+m',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleToggleShouldInvertMask();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldInvertMask, shouldShowMask]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
tooltip="Invert Mask Display (Shift+M)"
|
||||
aria-label="Invert Mask Display (Shift+M)"
|
||||
data-selected={shouldInvertMask}
|
||||
icon={
|
||||
shouldInvertMask ? (
|
||||
<MdInvertColors size={22} />
|
||||
) : (
|
||||
<MdInvertColorsOff size={22} />
|
||||
)
|
||||
}
|
||||
onClick={handleToggleShouldInvertMask}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { BiHide, BiShow } from 'react-icons/bi';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
||||
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
|
||||
import { InpaintingState, setShouldShowMask } from '../../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
const inpaintingMaskVisibilitySelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { shouldShowMask } = inpainting;
|
||||
|
||||
return { shouldShowMask, activeTabName };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingMaskVisibilityControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingMaskVisibilitySelector
|
||||
);
|
||||
|
||||
const handleToggleShouldShowMask = () =>
|
||||
dispatch(setShouldShowMask(!shouldShowMask));
|
||||
// Hotkeys
|
||||
// Show/hide mask
|
||||
useHotkeys(
|
||||
'h',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleToggleShouldShowMask();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting',
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Hide/Show Mask (H)"
|
||||
tooltip="Hide/Show Mask (H)"
|
||||
data-selected={!shouldShowMask}
|
||||
icon={shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />}
|
||||
onClick={handleToggleShouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { FaRedo } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||
import { InpaintingState, redo } from '../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
const inpaintingRedoSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { futureLines, shouldShowMask } = inpainting;
|
||||
|
||||
return {
|
||||
canRedo: futureLines.length > 0,
|
||||
shouldShowMask,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingRedoControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { canRedo, shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingRedoSelector
|
||||
);
|
||||
|
||||
const handleRedo = () => dispatch(redo());
|
||||
|
||||
// Hotkeys
|
||||
|
||||
// Redo
|
||||
useHotkeys(
|
||||
'cmd+shift+z, control+shift+z, control+y, cmd+y',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleRedo();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && canRedo,
|
||||
},
|
||||
[activeTabName, shouldShowMask, canRedo]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Redo"
|
||||
tooltip="Redo"
|
||||
icon={<FaRedo />}
|
||||
onClick={handleRedo}
|
||||
isDisabled={!canRedo || !shouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { VscSplitHorizontal } from 'react-icons/vsc';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { setShowDualDisplay } from '../../../options/optionsSlice';
|
||||
import { setNeedsCache } from '../inpaintingSlice';
|
||||
|
||||
export default function InpaintingSplitLayoutControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
const showDualDisplay = useAppSelector(
|
||||
(state: RootState) => state.options.showDualDisplay
|
||||
);
|
||||
|
||||
const handleDualDisplay = () => {
|
||||
dispatch(setShowDualDisplay(!showDualDisplay));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
// Hotkeys
|
||||
// Toggle split view
|
||||
useHotkeys(
|
||||
'shift+j',
|
||||
() => {
|
||||
handleDualDisplay();
|
||||
},
|
||||
[showDualDisplay]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Split Layout (Shift+J)"
|
||||
tooltip="Split Layout (Shift+J)"
|
||||
icon={<VscSplitHorizontal />}
|
||||
data-selected={showDualDisplay}
|
||||
onClick={handleDualDisplay}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { FaUndo } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { InpaintingState, undo } from '../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||
|
||||
const inpaintingUndoSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { pastLines, shouldShowMask } = inpainting;
|
||||
|
||||
return {
|
||||
canUndo: pastLines.length > 0,
|
||||
shouldShowMask,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingUndoControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { canUndo, shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingUndoSelector
|
||||
);
|
||||
|
||||
const handleUndo = () => dispatch(undo());
|
||||
|
||||
// Hotkeys
|
||||
// Undo
|
||||
useHotkeys(
|
||||
'cmd+z, control+z',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleUndo();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && canUndo,
|
||||
},
|
||||
[activeTabName, shouldShowMask, canUndo]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Undo"
|
||||
tooltip="Undo"
|
||||
icon={<FaUndo />}
|
||||
onClick={handleUndo}
|
||||
isDisabled={!canUndo || !shouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user