mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip regional prompting UI
- Arrange layers - Layer visibility - Layered brush preview - Cleanup
This commit is contained in:
parent
83d359b681
commit
822dfa77fc
@ -1505,5 +1505,13 @@
|
|||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"storeNotInitialized": "Store is not initialized"
|
"storeNotInitialized": "Store is not initialized"
|
||||||
|
},
|
||||||
|
"regionalPrompts": {
|
||||||
|
"addLayer": "Add Layer",
|
||||||
|
"moveToFront": "Move to Front",
|
||||||
|
"moveToBack": "Move to Back",
|
||||||
|
"moveForward": "Move Forward",
|
||||||
|
"moveBackward": "Move Backward",
|
||||||
|
"brushSize": "Brush Size"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
85
invokeai/frontend/web/src/common/util/arrayUtils.test.ts
Normal file
85
invokeai/frontend/web/src/common/util/arrayUtils.test.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('Array Manipulation Functions', () => {
|
||||||
|
const originalArray = ['a', 'b', 'c', 'd'];
|
||||||
|
describe('moveForwardOne', () => {
|
||||||
|
it('should move an item forward by one position', () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveForward(array, (item) => item === 'b');
|
||||||
|
expect(result).toEqual(['a', 'c', 'b', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if the item is at the end', () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveForward(array, (item) => item === 'd');
|
||||||
|
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveForward(array, (item) => item === 'z');
|
||||||
|
expect(result).toEqual(originalArray);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('moveToFront', () => {
|
||||||
|
it('should move an item to the front', () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveToFront(array, (item) => item === 'c');
|
||||||
|
expect(result).toEqual(['c', 'a', 'b', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if the item is already at the front', () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveToFront(array, (item) => item === 'a');
|
||||||
|
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveToFront(array, (item) => item === 'z');
|
||||||
|
expect(result).toEqual(originalArray);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('moveBackwardsOne', () => {
|
||||||
|
it('should move an item backward by one position', () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveBackward(array, (item) => item === 'c');
|
||||||
|
expect(result).toEqual(['a', 'c', 'b', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if the item is at the beginning', () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveBackward(array, (item) => item === 'a');
|
||||||
|
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveBackward(array, (item) => item === 'z');
|
||||||
|
expect(result).toEqual(originalArray);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('moveToBack', () => {
|
||||||
|
it('should move an item to the back', () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveToBack(array, (item) => item === 'b');
|
||||||
|
expect(result).toEqual(['a', 'c', 'd', 'b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if the item is already at the back', () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveToBack(array, (item) => item === 'd');
|
||||||
|
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||||
|
const array = [...originalArray];
|
||||||
|
const result = moveToBack(array, (item) => item === 'z');
|
||||||
|
expect(result).toEqual(originalArray);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
37
invokeai/frontend/web/src/common/util/arrayUtils.ts
Normal file
37
invokeai/frontend/web/src/common/util/arrayUtils.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export const moveForward = <T>(array: T[], callback: (item: T) => boolean): T[] => {
|
||||||
|
const index = array.findIndex(callback);
|
||||||
|
if (index >= 0 && index < array.length - 1) {
|
||||||
|
//@ts-expect-error - These indicies are safe per the previous check
|
||||||
|
[array[index], array[index + 1]] = [array[index + 1], array[index]];
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moveToFront = <T>(array: T[], callback: (item: T) => boolean): T[] => {
|
||||||
|
const index = array.findIndex(callback);
|
||||||
|
if (index > 0) {
|
||||||
|
const [item] = array.splice(index, 1);
|
||||||
|
//@ts-expect-error - These indicies are safe per the previous check
|
||||||
|
array.unshift(item);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moveBackward = <T>(array: T[], callback: (item: T) => boolean): T[] => {
|
||||||
|
const index = array.findIndex(callback);
|
||||||
|
if (index > 0) {
|
||||||
|
//@ts-expect-error - These indicies are safe per the previous check
|
||||||
|
[array[index], array[index - 1]] = [array[index - 1], array[index]];
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moveToBack = <T>(array: T[], callback: (item: T) => boolean): T[] => {
|
||||||
|
const index = array.findIndex(callback);
|
||||||
|
if (index >= 0 && index < array.length - 1) {
|
||||||
|
const [item] = array.splice(index, 1);
|
||||||
|
//@ts-expect-error - These indicies are safe per the previous check
|
||||||
|
array.push(item);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
};
|
@ -2,12 +2,14 @@ import { Button } from '@invoke-ai/ui-library';
|
|||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { layerAdded } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { layerAdded } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export const AddLayerButton = () => {
|
export const AddLayerButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(layerAdded('promptRegionLayer'));
|
dispatch(layerAdded('promptRegionLayer'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return <Button onClick={onClick}>Add Layer</Button>;
|
return <Button onClick={onClick}>{t('regionalPrompts.addLayer')}</Button>;
|
||||||
};
|
};
|
||||||
|
@ -2,9 +2,9 @@ import { useStore } from '@nanostores/react';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
import { $cursorPosition } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { $cursorPosition } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { Circle } from 'react-konva';
|
import { Circle, Group } from 'react-konva';
|
||||||
|
|
||||||
export const BrushPreview = () => {
|
export const BrushPreviewFill = () => {
|
||||||
const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize);
|
const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize);
|
||||||
const color = useAppSelector((s) => {
|
const color = useAppSelector((s) => {
|
||||||
const _color = s.regionalPrompts.layers.find((l) => l.id === s.regionalPrompts.selectedLayer)?.color;
|
const _color = s.regionalPrompts.layers.find((l) => l.id === s.regionalPrompts.selectedLayer)?.color;
|
||||||
@ -21,3 +21,42 @@ export const BrushPreview = () => {
|
|||||||
|
|
||||||
return <Circle x={pos.x} y={pos.y} radius={brushSize / 2} fill={color} />;
|
return <Circle x={pos.x} y={pos.y} radius={brushSize / 2} fill={color} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const BrushPreviewOutline = () => {
|
||||||
|
const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize);
|
||||||
|
const color = useAppSelector((s) => {
|
||||||
|
const _color = s.regionalPrompts.layers.find((l) => l.id === s.regionalPrompts.selectedLayer)?.color;
|
||||||
|
if (!_color) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return rgbColorToString(_color);
|
||||||
|
});
|
||||||
|
const pos = useStore($cursorPosition);
|
||||||
|
|
||||||
|
if (!brushSize || !color || !pos) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<Circle
|
||||||
|
x={pos.x}
|
||||||
|
y={pos.y}
|
||||||
|
radius={brushSize / 2 + 1}
|
||||||
|
stroke="rgba(255,255,255,0.8)"
|
||||||
|
strokeWidth={1}
|
||||||
|
strokeEnabled={true}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
<Circle
|
||||||
|
x={pos.x}
|
||||||
|
y={pos.y}
|
||||||
|
radius={brushSize / 2}
|
||||||
|
stroke="rgba(0,0,0,1)"
|
||||||
|
strokeWidth={1}
|
||||||
|
strokeEnabled={true}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -2,9 +2,11 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { brushSizeChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { brushSizeChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export const BrushSize = () => {
|
export const BrushSize = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize);
|
const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize);
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
@ -14,7 +16,7 @@ export const BrushSize = () => {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<FormControl orientation="vertical">
|
<FormControl orientation="vertical">
|
||||||
<FormLabel>Brush Size</FormLabel>
|
<FormLabel>{t('regionalPrompts.brushSize')}</FormLabel>
|
||||||
<CompositeSlider min={1} max={100} value={brushSize} onChange={onChange} />
|
<CompositeSlider min={1} max={100} value={brushSize} onChange={onChange} />
|
||||||
<CompositeNumberInput min={1} max={500} value={brushSize} onChange={onChange} />
|
<CompositeNumberInput min={1} max={500} value={brushSize} onChange={onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { Button } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import { layerDeleted } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DeleteLayerButton = ({ id }: Props) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const onClick = useCallback(() => {
|
|
||||||
dispatch(layerDeleted(id));
|
|
||||||
}, [dispatch, id]);
|
|
||||||
|
|
||||||
return <Button onClick={onClick} flexShrink={0}>Delete</Button>;
|
|
||||||
};
|
|
@ -2,7 +2,7 @@ import { Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai
|
|||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { ColorPreview } from 'common/components/ColorPreview';
|
import { ColorPreview } from 'common/components/ColorPreview';
|
||||||
import RgbColorPicker from 'common/components/RgbColorPicker';
|
import RgbColorPicker from 'common/components/RgbColorPicker';
|
||||||
import { useLayer } from 'features/regionalPrompts/hooks/useLayer';
|
import { useLayer } from 'features/regionalPrompts/hooks/layerStateHooks';
|
||||||
import { promptRegionLayerColorChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { promptRegionLayerColorChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import type { RgbColor } from 'react-colorful';
|
import type { RgbColor } from 'react-colorful';
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { DeleteLayerButton } from 'features/regionalPrompts/components/DeleteLayerButton';
|
|
||||||
import { LayerColorPicker } from 'features/regionalPrompts/components/LayerColorPicker';
|
import { LayerColorPicker } from 'features/regionalPrompts/components/LayerColorPicker';
|
||||||
|
import { LayerMenu } from 'features/regionalPrompts/components/LayerMenu';
|
||||||
|
import { LayerVisibilityToggle } from 'features/regionalPrompts/components/LayerVisibilityToggle';
|
||||||
import { RegionalPromptsPrompt } from 'features/regionalPrompts/components/RegionalPromptsPrompt';
|
import { RegionalPromptsPrompt } from 'features/regionalPrompts/components/RegionalPromptsPrompt';
|
||||||
import { ResetLayerButton } from 'features/regionalPrompts/components/ResetLayerButton';
|
|
||||||
import { layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
@ -14,19 +14,22 @@ type Props = {
|
|||||||
export const LayerListItem = ({ id }: Props) => {
|
export const LayerListItem = ({ id }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer);
|
const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer);
|
||||||
const border = useMemo(() => (selectedLayer === id ? '1px solid red' : 'none'), [selectedLayer, id]);
|
const bg = useMemo(() => (selectedLayer === id ? 'invokeBlue.500' : 'transparent'), [selectedLayer, id]);
|
||||||
const onClickCapture = useCallback(() => {
|
const onClickCapture = useCallback(() => {
|
||||||
// Must be capture so that the layer is selected before deleting/resetting/etc
|
// Must be capture so that the layer is selected before deleting/resetting/etc
|
||||||
dispatch(layerSelected(id));
|
dispatch(layerSelected(id));
|
||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" onClickCapture={onClickCapture} border={border}>
|
<Flex gap={2} onClickCapture={onClickCapture}>
|
||||||
<Flex gap={2}>
|
<Flex w={2} borderRadius="base" bg={bg} flexShrink={0} py={4} />
|
||||||
<ResetLayerButton id={id} />
|
<Flex flexDir="column" gap={2}>
|
||||||
<DeleteLayerButton id={id} />
|
<Flex gap={2}>
|
||||||
<LayerColorPicker id={id} />
|
<LayerColorPicker id={id} />
|
||||||
|
<LayerVisibilityToggle id={id} />
|
||||||
|
<LayerMenu id={id} />
|
||||||
|
</Flex>
|
||||||
|
<RegionalPromptsPrompt layerId={id} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<RegionalPromptsPrompt layerId={id} />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import {
|
||||||
|
layerDeleted,
|
||||||
|
layerMovedBackward,
|
||||||
|
layerMovedForward,
|
||||||
|
layerMovedToBack,
|
||||||
|
layerMovedToFront,
|
||||||
|
layerReset,
|
||||||
|
selectRegionalPromptsSlice,
|
||||||
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import type React from 'react';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
PiArrowCounterClockwiseBold,
|
||||||
|
PiArrowDownBold,
|
||||||
|
PiArrowLineDownBold,
|
||||||
|
PiArrowLineUpBold,
|
||||||
|
PiArrowUpBold,
|
||||||
|
PiDotsThreeVerticalBold,
|
||||||
|
PiTrashSimpleBold,
|
||||||
|
} from 'react-icons/pi';
|
||||||
|
|
||||||
|
type Props = { id: string };
|
||||||
|
|
||||||
|
export const LayerMenu: React.FC<Props> = ({ id }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const selectValidActions = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||||
|
const layerIndex = regionalPrompts.layers.findIndex((l) => l.id === id);
|
||||||
|
const layerCount = regionalPrompts.layers.length;
|
||||||
|
return {
|
||||||
|
canMoveForward: layerIndex < layerCount - 1,
|
||||||
|
canMoveBackward: layerIndex > 0,
|
||||||
|
canMoveToFront: layerIndex < layerCount - 1,
|
||||||
|
canMoveToBack: layerIndex > 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
const validActions = useAppSelector(selectValidActions);
|
||||||
|
const moveForward = useCallback(() => {
|
||||||
|
dispatch(layerMovedForward(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
const moveToFront = useCallback(() => {
|
||||||
|
dispatch(layerMovedToFront(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
const moveBackward = useCallback(() => {
|
||||||
|
dispatch(layerMovedBackward(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
const moveToBack = useCallback(() => {
|
||||||
|
dispatch(layerMovedToBack(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
const resetLayer = useCallback(() => {
|
||||||
|
dispatch(layerReset(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
const deleteLayer = useCallback(() => {
|
||||||
|
dispatch(layerDeleted(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={IconButton} aria-label="Layer menu" size="sm" icon={<PiDotsThreeVerticalBold />} />
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
|
||||||
|
{t('regionalPrompts.moveToFront')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={moveForward} isDisabled={!validActions.canMoveForward} icon={<PiArrowUpBold />}>
|
||||||
|
{t('regionalPrompts.moveForward')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={moveBackward} isDisabled={!validActions.canMoveBackward} icon={<PiArrowDownBold />}>
|
||||||
|
{t('regionalPrompts.moveBackward')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
|
||||||
|
{t('regionalPrompts.moveToBack')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem onClick={resetLayer} icon={<PiArrowCounterClockwiseBold />}>
|
||||||
|
{t('accessibility.reset')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={deleteLayer} icon={<PiTrashSimpleBold />} color="error.300">
|
||||||
|
{t('common.delete')}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { useLayerIsVisible } from 'features/regionalPrompts/hooks/layerStateHooks';
|
||||||
|
import { layerIsVisibleToggled } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { PiEyeBold, PiEyeClosedBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LayerVisibilityToggle = ({ id }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const isVisible = useLayerIsVisible(id);
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
dispatch(layerIsVisibleToggled(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
aria-label="Toggle layer visibility"
|
||||||
|
variant={isVisible ? 'outline' : 'ghost'}
|
||||||
|
icon={isVisible ? <PiEyeBold /> : <PiEyeClosedBold />}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -7,18 +7,18 @@ import { LayerListItem } from 'features/regionalPrompts/components/LayerListItem
|
|||||||
import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage';
|
import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage';
|
||||||
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
|
||||||
const selectLayerIds = createSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
|
const selectLayerIdsReversed = createSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
|
||||||
regionalPrompts.layers.map((l) => l.id)
|
regionalPrompts.layers.map((l) => l.id).reverse()
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RegionalPromptsEditor = () => {
|
export const RegionalPromptsEditor = () => {
|
||||||
const layerIds = useAppSelector(selectLayerIds);
|
const layerIdsReversed = useAppSelector(selectLayerIdsReversed);
|
||||||
return (
|
return (
|
||||||
<Flex gap={4}>
|
<Flex gap={4}>
|
||||||
<Flex flexDir="column" w={200}>
|
<Flex flexDir="column" w={200} gap={4}>
|
||||||
<AddLayerButton />
|
<AddLayerButton />
|
||||||
<BrushSize />
|
<BrushSize />
|
||||||
{layerIds.map((id) => (
|
{layerIdsReversed.map((id) => (
|
||||||
<LayerListItem key={id} id={id} />
|
<LayerListItem key={id} id={id} />
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -5,7 +5,7 @@ import { PromptOverlayButtonWrapper } from 'features/parameters/components/Promp
|
|||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
import { usePrompt } from 'features/prompt/usePrompt';
|
import { usePrompt } from 'features/prompt/usePrompt';
|
||||||
import { useLayer } from 'features/regionalPrompts/hooks/useLayer';
|
import { useLayerPrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
|
||||||
import { promptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { promptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { SDXLConcatButton } from 'features/sdxl/components/SDXLPrompts/SDXLConcatButton';
|
import { SDXLConcatButton } from 'features/sdxl/components/SDXLPrompts/SDXLConcatButton';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
@ -18,21 +18,21 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RegionalPromptsPrompt = memo((props: Props) => {
|
export const RegionalPromptsPrompt = memo((props: Props) => {
|
||||||
const layer = useLayer(props.layerId);
|
const prompt = useLayerPrompt(props.layerId);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const baseModel = useAppSelector((s) => s.generation.model)?.base;
|
const baseModel = useAppSelector((s) => s.generation.model)?.base;
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleChange = useCallback(
|
const _onChange = useCallback(
|
||||||
(v: string) => {
|
(v: string) => {
|
||||||
dispatch(promptChanged({ layerId: props.layerId, prompt: v }));
|
dispatch(promptChanged({ layerId: props.layerId, prompt: v }));
|
||||||
},
|
},
|
||||||
[dispatch, props.layerId]
|
[dispatch, props.layerId]
|
||||||
);
|
);
|
||||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({
|
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({
|
||||||
prompt: layer.prompt,
|
prompt,
|
||||||
textareaRef: textareaRef,
|
textareaRef,
|
||||||
onChange: handleChange,
|
onChange: _onChange,
|
||||||
});
|
});
|
||||||
const focus: HotkeyCallback = useCallback(
|
const focus: HotkeyCallback = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
@ -51,7 +51,7 @@ export const RegionalPromptsPrompt = memo((props: Props) => {
|
|||||||
id="prompt"
|
id="prompt"
|
||||||
name="prompt"
|
name="prompt"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={layer.prompt}
|
value={prompt}
|
||||||
placeholder={t('parameters.positivePromptPlaceholder')}
|
placeholder={t('parameters.positivePromptPlaceholder')}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
minH={28}
|
minH={28}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { chakra } from '@invoke-ai/ui-library';
|
import { chakra } from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { BrushPreview } from 'features/regionalPrompts/components/BrushPreview';
|
import { BrushPreviewFill, BrushPreviewOutline } from 'features/regionalPrompts/components/BrushPreview';
|
||||||
import { LineComponent } from 'features/regionalPrompts/components/LineComponent';
|
import { LineComponent } from 'features/regionalPrompts/components/LineComponent';
|
||||||
import { RectComponent } from 'features/regionalPrompts/components/RectComponent';
|
import { RectComponent } from 'features/regionalPrompts/components/RectComponent';
|
||||||
import {
|
import {
|
||||||
@ -10,13 +10,15 @@ import {
|
|||||||
useMouseLeave,
|
useMouseLeave,
|
||||||
useMouseMove,
|
useMouseMove,
|
||||||
useMouseUp,
|
useMouseUp,
|
||||||
} from 'features/regionalPrompts/hooks/useMouseDown';
|
} from 'features/regionalPrompts/hooks/mouseEventHooks';
|
||||||
import { $stage, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { $stage, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import { Group, Layer, Stage } from 'react-konva';
|
import { Layer, Stage } from 'react-konva';
|
||||||
|
|
||||||
const selectLayers = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => regionalPrompts.layers);
|
const selectVisibleLayers = createSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
|
||||||
|
regionalPrompts.layers.filter((l) => l.isVisible)
|
||||||
|
);
|
||||||
|
|
||||||
const ChakraStage = chakra(Stage, {
|
const ChakraStage = chakra(Stage, {
|
||||||
shouldForwardProp: (prop) => !['sx'].includes(prop),
|
shouldForwardProp: (prop) => !['sx'].includes(prop),
|
||||||
@ -27,7 +29,8 @@ const stageSx = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RegionalPromptsStage: React.FC = memo(() => {
|
export const RegionalPromptsStage: React.FC = memo(() => {
|
||||||
const layers = useAppSelector(selectLayers);
|
const layers = useAppSelector(selectVisibleLayers);
|
||||||
|
const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer);
|
||||||
const stageRef = useRef<Konva.Stage | null>(null);
|
const stageRef = useRef<Konva.Stage | null>(null);
|
||||||
const onMouseDown = useMouseDown(stageRef);
|
const onMouseDown = useMouseDown(stageRef);
|
||||||
const onMouseUp = useMouseUp(stageRef);
|
const onMouseUp = useMouseUp(stageRef);
|
||||||
@ -52,23 +55,24 @@ export const RegionalPromptsStage: React.FC = memo(() => {
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
sx={stageSx}
|
sx={stageSx}
|
||||||
>
|
>
|
||||||
|
{layers.map((layer) => (
|
||||||
|
<Layer key={layer.id}>
|
||||||
|
{layer.objects.map((obj) => {
|
||||||
|
if (obj.kind === 'line') {
|
||||||
|
return <LineComponent key={obj.id} line={obj} color={layer.color} />;
|
||||||
|
}
|
||||||
|
if (obj.kind === 'fillRect') {
|
||||||
|
return <RectComponent key={obj.id} rect={obj} color={layer.color} />;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
{layer.id === selectedLayer && <BrushPreviewFill />}
|
||||||
|
</Layer>
|
||||||
|
))}
|
||||||
<Layer>
|
<Layer>
|
||||||
{layers.map((layer) => (
|
<BrushPreviewOutline />
|
||||||
<Group key={layer.id}>
|
|
||||||
{layer.objects.map((obj) => {
|
|
||||||
if (obj.kind === 'line') {
|
|
||||||
return <LineComponent key={obj.id} line={obj} color={layer.color} />;
|
|
||||||
}
|
|
||||||
if (obj.kind === 'fillRect') {
|
|
||||||
return <RectComponent key={obj.id} rect={obj} color={layer.color} />;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
<BrushPreview />
|
|
||||||
</Layer>
|
</Layer>
|
||||||
</ChakraStage>
|
</ChakraStage>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RegionalPromptsStage.displayName = 'RegionalPromptingEditor';
|
RegionalPromptsStage.displayName = 'RegionalPromptsStage';
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { Button } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import { layerReset } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ResetLayerButton = ({ id }: Props) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const onClick = useCallback(() => {
|
|
||||||
dispatch(layerReset(id));
|
|
||||||
}, [dispatch, id]);
|
|
||||||
|
|
||||||
return <Button onClick={onClick} flexShrink={0}>Reset</Button>;
|
|
||||||
};
|
|
@ -0,0 +1,46 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
export const useLayer = (layerId: string) => {
|
||||||
|
const selectLayer = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
|
||||||
|
regionalPrompts.layers.find((l) => l.id === layerId)
|
||||||
|
),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const layer = useAppSelector(selectLayer);
|
||||||
|
assert(layer !== undefined, `Layer ${layerId} doesn't exist!`);
|
||||||
|
return layer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLayerPrompt = (layerId: string) => {
|
||||||
|
const selectLayer = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
selectRegionalPromptsSlice,
|
||||||
|
(regionalPrompts) => regionalPrompts.layers.find((l) => l.id === layerId)?.prompt
|
||||||
|
),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const prompt = useAppSelector(selectLayer);
|
||||||
|
assert(prompt !== undefined, `Layer ${layerId} doesn't exist!`);
|
||||||
|
return prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLayerIsVisible = (layerId: string) => {
|
||||||
|
const selectLayer = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
selectRegionalPromptsSlice,
|
||||||
|
(regionalPrompts) => regionalPrompts.layers.find((l) => l.id === layerId)?.isVisible
|
||||||
|
),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const isVisible = useAppSelector(selectLayer);
|
||||||
|
assert(isVisible !== undefined, `Layer ${layerId} doesn't exist!`);
|
||||||
|
return isVisible;
|
||||||
|
};
|
@ -33,7 +33,6 @@ export const useMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) =>
|
|||||||
if (!stageRef.current) {
|
if (!stageRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Mouse down');
|
|
||||||
const pos = syncCursorPos(stageRef.current);
|
const pos = syncCursorPos(stageRef.current);
|
||||||
if (!pos) {
|
if (!pos) {
|
||||||
return;
|
return;
|
||||||
@ -55,7 +54,6 @@ export const useMouseUp = (stageRef: MutableRefObject<Konva.Stage | null>) => {
|
|||||||
if (!stageRef.current) {
|
if (!stageRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Mouse up');
|
|
||||||
if ($tool.get() === 'brush' && $isMouseDown.get()) {
|
if ($tool.get() === 'brush' && $isMouseDown.get()) {
|
||||||
// Add another point to the last line.
|
// Add another point to the last line.
|
||||||
$isMouseDown.set(false);
|
$isMouseDown.set(false);
|
||||||
@ -78,7 +76,6 @@ export const useMouseMove = (stageRef: MutableRefObject<Konva.Stage | null>) =>
|
|||||||
if (!stageRef.current) {
|
if (!stageRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Mouse move');
|
|
||||||
const pos = syncCursorPos(stageRef.current);
|
const pos = syncCursorPos(stageRef.current);
|
||||||
if (!pos) {
|
if (!pos) {
|
||||||
return;
|
return;
|
||||||
@ -98,7 +95,6 @@ export const useMouseLeave = (stageRef: MutableRefObject<Konva.Stage | null>) =>
|
|||||||
if (!stageRef.current) {
|
if (!stageRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Mouse leave');
|
|
||||||
$isMouseOver.set(false);
|
$isMouseOver.set(false);
|
||||||
$isMouseDown.set(false);
|
$isMouseDown.set(false);
|
||||||
$cursorPosition.set(null);
|
$cursorPosition.set(null);
|
||||||
@ -115,7 +111,6 @@ export const useMouseEnter = (stageRef: MutableRefObject<Konva.Stage | null>) =>
|
|||||||
if (!stageRef.current) {
|
if (!stageRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Mouse enter');
|
|
||||||
$isMouseOver.set(true);
|
$isMouseOver.set(true);
|
||||||
const pos = syncCursorPos(stageRef.current);
|
const pos = syncCursorPos(stageRef.current);
|
||||||
if (!pos) {
|
if (!pos) {
|
@ -1,18 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { assert } from 'tsafe';
|
|
||||||
|
|
||||||
export const useLayer = (layerId: string) => {
|
|
||||||
const selectLayer = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
|
|
||||||
regionalPrompts.layers.find((l) => l.id === layerId)
|
|
||||||
),
|
|
||||||
[layerId]
|
|
||||||
);
|
|
||||||
const layer = useAppSelector(selectLayer);
|
|
||||||
assert(layer, `Layer ${layerId} doesn't exist!`);
|
|
||||||
return layer;
|
|
||||||
};
|
|
@ -1,6 +1,7 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
|
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import type { Vector2d } from 'konva/lib/types';
|
import type { Vector2d } from 'konva/lib/types';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
@ -13,7 +14,7 @@ type LayerObjectBase = {
|
|||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ImageObject = LayerObjectBase & {
|
type ImageObject = LayerObjectBase & {
|
||||||
kind: 'image';
|
kind: 'image';
|
||||||
imageName: string;
|
imageName: string;
|
||||||
x: number;
|
x: number;
|
||||||
@ -38,26 +39,30 @@ export type FillRectObject = LayerObjectBase & {
|
|||||||
|
|
||||||
export type LayerObject = ImageObject | LineObject | FillRectObject;
|
export type LayerObject = ImageObject | LineObject | FillRectObject;
|
||||||
|
|
||||||
export type PromptRegionLayer = {
|
type LayerBase = {
|
||||||
id: string;
|
id: string;
|
||||||
|
isVisible: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PromptRegionLayer = LayerBase & {
|
||||||
kind: 'promptRegionLayer';
|
kind: 'promptRegionLayer';
|
||||||
objects: LayerObject[];
|
objects: LayerObject[];
|
||||||
prompt: string;
|
prompt: string;
|
||||||
color: RgbColor;
|
color: RgbColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Layer = PromptRegionLayer;
|
type Layer = PromptRegionLayer;
|
||||||
|
|
||||||
export type Tool = 'brush';
|
type Tool = 'brush';
|
||||||
|
|
||||||
export type RegionalPromptsState = {
|
type RegionalPromptsState = {
|
||||||
_version: 1;
|
_version: 1;
|
||||||
selectedLayer: string | null;
|
selectedLayer: string | null;
|
||||||
layers: PromptRegionLayer[];
|
layers: PromptRegionLayer[];
|
||||||
brushSize: number;
|
brushSize: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialRegionalPromptsState: RegionalPromptsState = {
|
const initialRegionalPromptsState: RegionalPromptsState = {
|
||||||
_version: 1,
|
_version: 1,
|
||||||
selectedLayer: null,
|
selectedLayer: null,
|
||||||
brushSize: 40,
|
brushSize: 40,
|
||||||
@ -73,7 +78,7 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
layerAdded: {
|
layerAdded: {
|
||||||
reducer: (state, action: PayloadAction<Layer['kind'], string, { id: string }>) => {
|
reducer: (state, action: PayloadAction<Layer['kind'], string, { id: string }>) => {
|
||||||
const newLayer = buildLayer(action.meta.id, action.payload, state.layers.length);
|
const newLayer = buildLayer(action.meta.id, action.payload, state.layers.length);
|
||||||
state.layers.push(newLayer);
|
state.layers.unshift(newLayer);
|
||||||
state.selectedLayer = newLayer.id;
|
state.selectedLayer = newLayer.id;
|
||||||
},
|
},
|
||||||
prepare: (payload: Layer['kind']) => ({ payload, meta: { id: uuidv4() } }),
|
prepare: (payload: Layer['kind']) => ({ payload, meta: { id: uuidv4() } }),
|
||||||
@ -81,6 +86,13 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
layerSelected: (state, action: PayloadAction<string>) => {
|
layerSelected: (state, action: PayloadAction<string>) => {
|
||||||
state.selectedLayer = action.payload;
|
state.selectedLayer = action.payload;
|
||||||
},
|
},
|
||||||
|
layerIsVisibleToggled: (state, action: PayloadAction<string>) => {
|
||||||
|
const layer = state.layers.find((l) => l.id === action.payload);
|
||||||
|
if (!layer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layer.isVisible = !layer.isVisible;
|
||||||
|
},
|
||||||
layerReset: (state, action: PayloadAction<string>) => {
|
layerReset: (state, action: PayloadAction<string>) => {
|
||||||
const layer = state.layers.find((l) => l.id === action.payload);
|
const layer = state.layers.find((l) => l.id === action.payload);
|
||||||
if (!layer) {
|
if (!layer) {
|
||||||
@ -92,6 +104,24 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
state.layers = state.layers.filter((l) => l.id !== action.payload);
|
state.layers = state.layers.filter((l) => l.id !== action.payload);
|
||||||
state.selectedLayer = state.layers[0]?.id ?? null;
|
state.selectedLayer = state.layers[0]?.id ?? null;
|
||||||
},
|
},
|
||||||
|
layerMovedForward: (state, action: PayloadAction<string>) => {
|
||||||
|
const cb = (l: Layer) => l.id === action.payload;
|
||||||
|
moveForward(state.layers, cb);
|
||||||
|
},
|
||||||
|
layerMovedToFront: (state, action: PayloadAction<string>) => {
|
||||||
|
const cb = (l: Layer) => l.id === action.payload;
|
||||||
|
// Because the layers are in reverse order, moving to the front is equivalent to moving to the back
|
||||||
|
moveToBack(state.layers, cb);
|
||||||
|
},
|
||||||
|
layerMovedBackward: (state, action: PayloadAction<string>) => {
|
||||||
|
const cb = (l: Layer) => l.id === action.payload;
|
||||||
|
moveBackward(state.layers, cb);
|
||||||
|
},
|
||||||
|
layerMovedToBack: (state, action: PayloadAction<string>) => {
|
||||||
|
const cb = (l: Layer) => l.id === action.payload;
|
||||||
|
// Because the layers are in reverse order, moving to the back is equivalent to moving to the front
|
||||||
|
moveToFront(state.layers, cb);
|
||||||
|
},
|
||||||
promptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
|
promptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
|
||||||
const { layerId, prompt } = action.payload;
|
const { layerId, prompt } = action.payload;
|
||||||
const layer = state.layers.find((l) => l.id === layerId);
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
@ -144,12 +174,13 @@ const DEFAULT_COLORS = [
|
|||||||
{ r: 200, g: 0, b: 200 },
|
{ r: 200, g: 0, b: 200 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const buildLayer = (id: string, kind: Layer['kind'], layerCount: number) => {
|
const buildLayer = (id: string, kind: Layer['kind'], layerCount: number): Layer => {
|
||||||
if (kind === 'promptRegionLayer') {
|
if (kind === 'promptRegionLayer') {
|
||||||
const color = DEFAULT_COLORS[layerCount % DEFAULT_COLORS.length];
|
const color = DEFAULT_COLORS[layerCount % DEFAULT_COLORS.length];
|
||||||
assert(color, 'Color not found');
|
assert(color, 'Color not found');
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
isVisible: true,
|
||||||
kind,
|
kind,
|
||||||
prompt: '',
|
prompt: '',
|
||||||
objects: [],
|
objects: [],
|
||||||
@ -172,11 +203,16 @@ export const {
|
|||||||
layerSelected,
|
layerSelected,
|
||||||
layerReset,
|
layerReset,
|
||||||
layerDeleted,
|
layerDeleted,
|
||||||
|
layerIsVisibleToggled,
|
||||||
promptChanged,
|
promptChanged,
|
||||||
lineAdded,
|
lineAdded,
|
||||||
pointsAdded,
|
pointsAdded,
|
||||||
promptRegionLayerColorChanged,
|
promptRegionLayerColorChanged,
|
||||||
brushSizeChanged,
|
brushSizeChanged,
|
||||||
|
layerMovedForward,
|
||||||
|
layerMovedToFront,
|
||||||
|
layerMovedBackward,
|
||||||
|
layerMovedToBack,
|
||||||
} = regionalPromptsSlice.actions;
|
} = regionalPromptsSlice.actions;
|
||||||
|
|
||||||
export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts;
|
export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts;
|
||||||
@ -195,7 +231,6 @@ export const regionalPromptsPersistConfig: PersistConfig<RegionalPromptsState> =
|
|||||||
|
|
||||||
export const $isMouseDown = atom(false);
|
export const $isMouseDown = atom(false);
|
||||||
export const $isMouseOver = atom(false);
|
export const $isMouseOver = atom(false);
|
||||||
export const $isFocused = atom(false);
|
|
||||||
export const $cursorPosition = atom<Vector2d | null>(null);
|
export const $cursorPosition = atom<Vector2d | null>(null);
|
||||||
export const $tool = atom<Tool>('brush');
|
export const $tool = atom<Tool>('brush');
|
||||||
export const $stage = atom<Konva.Stage | null>(null);
|
export const $stage = atom<Konva.Stage | null>(null);
|
||||||
|
Loading…
Reference in New Issue
Block a user