Skip to content

Commit a228e66

Browse files
author
Attila Cseh
committed
slider for brush and eraser tool
1 parent 1db55b0 commit a228e66

File tree

7 files changed

+355
-352
lines changed

7 files changed

+355
-352
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2224,6 +2224,9 @@
22242224
"uploadOrDragAnImage": "Drag an image from the gallery or <UploadButton>upload an image</UploadButton>.",
22252225
"imageNoise": "Image Noise",
22262226
"denoiseLimit": "Denoise Limit",
2227+
"toolWidthSelector": "Tool Width Selector",
2228+
"toolWidthSelectorDropDown": "Drop-down",
2229+
"toolWidthSelectorSlider": "Slider",
22272230
"warnings": {
22282231
"problemsFound": "Problems found",
22292232
"unsupportedModel": "layer not supported for selected base model",

invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import { memo } from 'react';
3333
import { useTranslation } from 'react-i18next';
3434
import { PiCodeFill, PiEyeFill, PiGearSixFill, PiPencilFill, PiSquaresFourFill } from 'react-icons/pi';
3535

36+
import { CanvasSettingsToolWidthSelectorDropdown } from './CanvasSettingsToolWidthSelectorDropdown';
37+
3638
export const CanvasSettingsPopover = memo(() => {
3739
const { t } = useTranslation();
3840
return (
@@ -80,6 +82,7 @@ export const CanvasSettingsPopover = memo(() => {
8082
<CanvasSettingsIsolatedLayerPreviewSwitch />
8183
<CanvasSettingsBboxOverlaySwitch />
8284
<CanvasSettingsShowHUDSwitch />
85+
<CanvasSettingsToolWidthSelectorDropdown />
8386
</Flex>
8487

8588
<Divider />
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
2+
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
3+
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
4+
import type { ToolWidthSelector } from 'features/controlLayers/store/canvasSettingsSlice';
5+
import {
6+
selectToolWidthSelector,
7+
settingsToolWidthSelectorChanged,
8+
zToolWidthSelector,
9+
} from 'features/controlLayers/store/canvasSettingsSlice';
10+
import { memo, useCallback, useMemo } from 'react';
11+
import { useTranslation } from 'react-i18next';
12+
13+
const isToolWidthSelector = (v: unknown): v is ToolWidthSelector => zToolWidthSelector.safeParse(v).success;
14+
15+
export const CanvasSettingsToolWidthSelectorDropdown = memo(() => {
16+
const { t } = useTranslation();
17+
const dispatch = useAppDispatch();
18+
const toolWidthSelector = useAppSelector(selectToolWidthSelector);
19+
20+
const OPTIONS: ComboboxOption[] = useMemo(
21+
() => [
22+
{ value: 'dropDown', label: t('controlLayers.toolWidthSelectorDropDown') },
23+
{ value: 'slider', label: t('controlLayers.toolWidthSelectorSlider') },
24+
],
25+
[t]
26+
);
27+
28+
const value = useMemo(() => {
29+
return OPTIONS.find((o) => o.value === toolWidthSelector) || OPTIONS[0];
30+
}, [toolWidthSelector, OPTIONS]);
31+
32+
const onChange = useCallback<ComboboxOnChange>(
33+
(option) => {
34+
if (!isToolWidthSelector(option?.value) || option.value === toolWidthSelector) {
35+
return;
36+
}
37+
dispatch(settingsToolWidthSelectorChanged(option.value));
38+
},
39+
[toolWidthSelector, dispatch]
40+
);
41+
42+
return (
43+
<FormControl>
44+
<FormLabel m={0} flexGrow={1}>
45+
{t('controlLayers.toolWidthSelector')}
46+
</FormLabel>
47+
<Combobox isSearchable={false} value={value} options={OPTIONS} onChange={onChange} />
48+
</FormControl>
49+
);
50+
});
51+
52+
CanvasSettingsToolWidthSelectorDropdown.displayName = 'CanvasSettingsToolWidthSelectorDropdown';
Lines changed: 7 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -1,195 +1,26 @@
1-
import {
2-
CompositeSlider,
3-
FormControl,
4-
IconButton,
5-
NumberInput,
6-
NumberInputField,
7-
Popover,
8-
PopoverAnchor,
9-
PopoverArrow,
10-
PopoverBody,
11-
PopoverContent,
12-
PopoverTrigger,
13-
} from '@invoke-ai/ui-library';
141
import { createSelector } from '@reduxjs/toolkit';
152
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
16-
import { clamp } from 'es-toolkit/compat';
173
import { useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
184
import { selectCanvasSettingsSlice, settingsBrushWidthChanged } from 'features/controlLayers/store/canvasSettingsSlice';
19-
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
20-
import type { KeyboardEvent } from 'react';
21-
import { memo, useCallback, useEffect, useState } from 'react';
22-
import { PiCaretDownBold } from 'react-icons/pi';
5+
import { memo, useCallback } from 'react';
236

24-
const selectBrushWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.brushWidth);
25-
const formatPx = (v: number | string) => `${v} px`;
26-
27-
function mapSliderValueToRawValue(value: number) {
28-
if (value <= 40) {
29-
// 0 to 40 on the slider -> 1px to 50px
30-
return 1 + (49 * value) / 40;
31-
} else if (value <= 70) {
32-
// 40 to 70 on the slider -> 50px to 200px
33-
return 50 + (150 * (value - 40)) / 30;
34-
} else {
35-
// 70 to 100 on the slider -> 200px to 600px
36-
return 200 + (400 * (value - 70)) / 30;
37-
}
38-
}
39-
40-
function mapRawValueToSliderValue(value: number) {
41-
if (value <= 50) {
42-
// 1px to 50px -> 0 to 40 on the slider
43-
return ((value - 1) * 40) / 49;
44-
} else if (value <= 200) {
45-
// 50px to 200px -> 40 to 70 on the slider
46-
return 40 + ((value - 50) * 30) / 150;
47-
} else {
48-
// 200px to 600px -> 70 to 100 on the slider
49-
return 70 + ((value - 200) * 30) / 400;
50-
}
51-
}
52-
53-
function formatSliderValue(value: number) {
54-
return `${String(mapSliderValueToRawValue(value))} px`;
55-
}
56-
57-
const marks = [
58-
mapRawValueToSliderValue(1),
59-
mapRawValueToSliderValue(50),
60-
mapRawValueToSliderValue(200),
61-
mapRawValueToSliderValue(600),
62-
];
7+
import { ToolWidth } from './ToolWidth';
638

64-
const sliderDefaultValue = mapRawValueToSliderValue(50);
9+
const selectBrushWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.brushWidth);
6510

6611
export const ToolBrushWidth = memo(() => {
6712
const dispatch = useAppDispatch();
6813
const isSelected = useToolIsSelected('brush');
6914
const width = useAppSelector(selectBrushWidth);
70-
const [localValue, setLocalValue] = useState(width);
71-
const onChange = useCallback(
72-
(v: number) => {
73-
dispatch(settingsBrushWidthChanged(clamp(Math.round(v), 1, 600)));
74-
},
75-
[dispatch]
76-
);
77-
78-
const increment = useCallback(() => {
79-
let newWidth = Math.round(width * 1.15);
80-
if (newWidth === width) {
81-
newWidth += 1;
82-
}
83-
onChange(newWidth);
84-
}, [onChange, width]);
8515

86-
const decrement = useCallback(() => {
87-
let newWidth = Math.round(width * 0.85);
88-
if (newWidth === width) {
89-
newWidth -= 1;
90-
}
91-
onChange(newWidth);
92-
}, [onChange, width]);
93-
94-
const onChangeSlider = useCallback(
16+
const onValueChange = useCallback(
9517
(value: number) => {
96-
onChange(mapSliderValueToRawValue(value));
97-
},
98-
[onChange]
99-
);
100-
101-
const onBlur = useCallback(() => {
102-
if (isNaN(Number(localValue))) {
103-
onChange(50);
104-
setLocalValue(50);
105-
} else {
106-
onChange(localValue);
107-
}
108-
}, [localValue, onChange]);
109-
110-
const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => {
111-
setLocalValue(valueAsNumber);
112-
}, []);
113-
114-
const onKeyDown = useCallback(
115-
(e: KeyboardEvent<HTMLInputElement>) => {
116-
if (e.key === 'Enter') {
117-
onBlur();
118-
}
18+
dispatch(settingsBrushWidthChanged(value));
11919
},
120-
[onBlur]
20+
[dispatch]
12121
);
12222

123-
useEffect(() => {
124-
setLocalValue(width);
125-
}, [width]);
126-
127-
useRegisteredHotkeys({
128-
id: 'decrementToolWidth',
129-
category: 'canvas',
130-
callback: decrement,
131-
options: { enabled: isSelected },
132-
dependencies: [decrement, isSelected],
133-
});
134-
useRegisteredHotkeys({
135-
id: 'incrementToolWidth',
136-
category: 'canvas',
137-
callback: increment,
138-
options: { enabled: isSelected },
139-
dependencies: [increment, isSelected],
140-
});
141-
142-
return (
143-
<Popover>
144-
<FormControl w="min-content" gap={2}>
145-
<PopoverAnchor>
146-
<NumberInput
147-
variant="outline"
148-
display="flex"
149-
alignItems="center"
150-
min={1}
151-
max={600}
152-
value={localValue}
153-
onChange={onChangeNumberInput}
154-
onBlur={onBlur}
155-
w="76px"
156-
format={formatPx}
157-
defaultValue={50}
158-
onKeyDown={onKeyDown}
159-
clampValueOnBlur={false}
160-
>
161-
<NumberInputField _focusVisible={{ zIndex: 0 }} title="" paddingInlineEnd={7} />
162-
<PopoverTrigger>
163-
<IconButton
164-
aria-label="open-slider"
165-
icon={<PiCaretDownBold />}
166-
size="sm"
167-
variant="link"
168-
position="absolute"
169-
insetInlineEnd={0}
170-
h="full"
171-
/>
172-
</PopoverTrigger>
173-
</NumberInput>
174-
</PopoverAnchor>
175-
</FormControl>
176-
<PopoverContent w={200} pt={0} pb={2} px={4}>
177-
<PopoverArrow />
178-
<PopoverBody>
179-
<CompositeSlider
180-
min={0}
181-
max={100}
182-
value={mapRawValueToSliderValue(localValue)}
183-
onChange={onChangeSlider}
184-
defaultValue={sliderDefaultValue}
185-
marks={marks}
186-
formatValue={formatSliderValue}
187-
alwaysShowMarks
188-
/>
189-
</PopoverBody>
190-
</PopoverContent>
191-
</Popover>
192-
);
23+
return <ToolWidth isSelected={isSelected} width={width} onValueChange={onValueChange} />;
19324
});
19425

19526
ToolBrushWidth.displayName = 'ToolBrushWidth';

0 commit comments

Comments
 (0)