mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): rects on regional prompt UI
This commit is contained in:
parent
cfddbda578
commit
4895875ded
@ -1522,6 +1522,7 @@
|
|||||||
"autoNegative": "Auto Negative",
|
"autoNegative": "Auto Negative",
|
||||||
"toggleVisibility": "Toggle Layer Visibility",
|
"toggleVisibility": "Toggle Layer Visibility",
|
||||||
"resetRegion": "Reset Region",
|
"resetRegion": "Reset Region",
|
||||||
"debugLayers": "Debug Layers"
|
"debugLayers": "Debug Layers",
|
||||||
|
"rectangle": "Rectangle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { useMouseEvents } from 'features/regionalPrompts/hooks/mouseEventHooks';
|
import { useMouseEvents } from 'features/regionalPrompts/hooks/mouseEventHooks';
|
||||||
import {
|
import {
|
||||||
$cursorPosition,
|
$cursorPosition,
|
||||||
|
$lastMouseDownPos,
|
||||||
$tool,
|
$tool,
|
||||||
isVectorMaskLayer,
|
isVectorMaskLayer,
|
||||||
layerBboxChanged,
|
layerBboxChanged,
|
||||||
@ -13,7 +14,7 @@ import {
|
|||||||
layerTranslated,
|
layerTranslated,
|
||||||
selectRegionalPromptsSlice,
|
selectRegionalPromptsSlice,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { renderBbox, renderLayers,renderToolPreview } from 'features/regionalPrompts/util/renderers';
|
import { renderBbox, renderLayers, renderToolPreview } from 'features/regionalPrompts/util/renderers';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
@ -40,6 +41,7 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
|||||||
const tool = useStore($tool);
|
const tool = useStore($tool);
|
||||||
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave } = useMouseEvents();
|
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave } = useMouseEvents();
|
||||||
const cursorPosition = useStore($cursorPosition);
|
const cursorPosition = useStore($cursorPosition);
|
||||||
|
const lastMouseDownPos = useStore($lastMouseDownPos);
|
||||||
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
|
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
|
||||||
|
|
||||||
const onLayerPosChanged = useCallback(
|
const onLayerPosChanged = useCallback(
|
||||||
@ -130,8 +132,8 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
|||||||
if (!stage) {
|
if (!stage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderToolPreview(stage, tool, selectedLayerIdColor, cursorPosition, state.brushSize);
|
renderToolPreview(stage, tool, selectedLayerIdColor, cursorPosition, lastMouseDownPos, state.brushSize);
|
||||||
}, [stage, tool, cursorPosition, state.brushSize, selectedLayerIdColor]);
|
}, [stage, tool, selectedLayerIdColor, cursorPosition, lastMouseDownPos, state.brushSize]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering layers');
|
log.trace('Rendering layers');
|
||||||
|
@ -1,27 +1,33 @@
|
|||||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { $tool } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { $tool } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold } from 'react-icons/pi';
|
import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ToolChooser: React.FC = () => {
|
export const ToolChooser: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const isDisabled = useAppSelector((s) => s.regionalPrompts.present.layers.length === 0);
|
||||||
const tool = useStore($tool);
|
const tool = useStore($tool);
|
||||||
|
|
||||||
const setToolToBrush = useCallback(() => {
|
const setToolToBrush = useCallback(() => {
|
||||||
$tool.set('brush');
|
$tool.set('brush');
|
||||||
}, []);
|
}, []);
|
||||||
useHotkeys('b', setToolToBrush, []);
|
useHotkeys('b', setToolToBrush, { enabled: !isDisabled }, [isDisabled]);
|
||||||
const setToolToEraser = useCallback(() => {
|
const setToolToEraser = useCallback(() => {
|
||||||
$tool.set('eraser');
|
$tool.set('eraser');
|
||||||
}, []);
|
}, []);
|
||||||
useHotkeys('e', setToolToEraser, []);
|
useHotkeys('e', setToolToEraser, { enabled: !isDisabled }, [isDisabled]);
|
||||||
|
const setToolToRect = useCallback(() => {
|
||||||
|
$tool.set('rect');
|
||||||
|
}, []);
|
||||||
|
useHotkeys('u', setToolToRect, { enabled: !isDisabled }, [isDisabled]);
|
||||||
const setToolToMove = useCallback(() => {
|
const setToolToMove = useCallback(() => {
|
||||||
$tool.set('move');
|
$tool.set('move');
|
||||||
}, []);
|
}, []);
|
||||||
useHotkeys('v', setToolToMove, []);
|
useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup isAttached>
|
<ButtonGroup isAttached>
|
||||||
@ -31,6 +37,7 @@ export const ToolChooser: React.FC = () => {
|
|||||||
icon={<PiPaintBrushBold />}
|
icon={<PiPaintBrushBold />}
|
||||||
variant={tool === 'brush' ? 'solid' : 'outline'}
|
variant={tool === 'brush' ? 'solid' : 'outline'}
|
||||||
onClick={setToolToBrush}
|
onClick={setToolToBrush}
|
||||||
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.eraser')} (E)`}
|
aria-label={`${t('unifiedCanvas.eraser')} (E)`}
|
||||||
@ -38,6 +45,15 @@ export const ToolChooser: React.FC = () => {
|
|||||||
icon={<PiEraserBold />}
|
icon={<PiEraserBold />}
|
||||||
variant={tool === 'eraser' ? 'solid' : 'outline'}
|
variant={tool === 'eraser' ? 'solid' : 'outline'}
|
||||||
onClick={setToolToEraser}
|
onClick={setToolToEraser}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label={`${t('regionalPrompts.rectangle')} (U)`}
|
||||||
|
tooltip={`${t('regionalPrompts.rectangle')} (U)`}
|
||||||
|
icon={<PiRectangleBold />}
|
||||||
|
variant={tool === 'rect' ? 'solid' : 'outline'}
|
||||||
|
onClick={setToolToRect}
|
||||||
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.move')} (V)`}
|
aria-label={`${t('unifiedCanvas.move')} (V)`}
|
||||||
@ -45,6 +61,7 @@ export const ToolChooser: React.FC = () => {
|
|||||||
icon={<PiArrowsOutCardinalBold />}
|
icon={<PiArrowsOutCardinalBold />}
|
||||||
variant={tool === 'move' ? 'solid' : 'outline'}
|
variant={tool === 'move' ? 'solid' : 'outline'}
|
||||||
onClick={setToolToMove}
|
onClick={setToolToMove}
|
||||||
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable i18next/no-literal-string */
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { redoRegionalPrompts, undoRegionalPrompts } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { redo, undo } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -12,30 +12,30 @@ export const UndoRedoButtonGroup = memo(() => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const mayUndo = useAppSelector((s) => s.regionalPrompts.past.length > 0);
|
const mayUndo = useAppSelector((s) => s.regionalPrompts.past.length > 0);
|
||||||
const undo = useCallback(() => {
|
const handleUndo = useCallback(() => {
|
||||||
dispatch(undoRegionalPrompts());
|
dispatch(undo());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
useHotkeys(['meta+z', 'ctrl+z'], undo, { enabled: mayUndo, preventDefault: true }, [mayUndo, undo]);
|
useHotkeys(['meta+z', 'ctrl+z'], handleUndo, { enabled: mayUndo, preventDefault: true }, [mayUndo, undo]);
|
||||||
|
|
||||||
const mayRedo = useAppSelector((s) => s.regionalPrompts.future.length > 0);
|
const mayRedo = useAppSelector((s) => s.regionalPrompts.future.length > 0);
|
||||||
const redo = useCallback(() => {
|
const handleRedo = useCallback(() => {
|
||||||
dispatch(redoRegionalPrompts());
|
dispatch(redo());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
useHotkeys(['meta+shift+z', 'ctrl+shift+z'], redo, { enabled: mayRedo, preventDefault: true }, [mayRedo, redo]);
|
useHotkeys(['meta+shift+z', 'ctrl+shift+z'], handleRedo, { enabled: mayRedo, preventDefault: true }, [mayRedo, redo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('unifiedCanvas.undo')}
|
aria-label={t('unifiedCanvas.undo')}
|
||||||
tooltip={t('unifiedCanvas.undo')}
|
tooltip={t('unifiedCanvas.undo')}
|
||||||
onClick={undo}
|
onClick={handleUndo}
|
||||||
icon={<PiArrowCounterClockwiseBold />}
|
icon={<PiArrowCounterClockwiseBold />}
|
||||||
isDisabled={!mayUndo}
|
isDisabled={!mayUndo}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('unifiedCanvas.redo')}
|
aria-label={t('unifiedCanvas.redo')}
|
||||||
tooltip={t('unifiedCanvas.redo')}
|
tooltip={t('unifiedCanvas.redo')}
|
||||||
onClick={redo}
|
onClick={handleRedo}
|
||||||
icon={<PiArrowClockwiseBold />}
|
icon={<PiArrowClockwiseBold />}
|
||||||
isDisabled={!mayRedo}
|
isDisabled={!mayRedo}
|
||||||
/>
|
/>
|
||||||
|
@ -4,9 +4,11 @@ import {
|
|||||||
$cursorPosition,
|
$cursorPosition,
|
||||||
$isMouseDown,
|
$isMouseDown,
|
||||||
$isMouseOver,
|
$isMouseOver,
|
||||||
|
$lastMouseDownPos,
|
||||||
$tool,
|
$tool,
|
||||||
maskLayerLineAdded,
|
maskLayerLineAdded,
|
||||||
maskLayerPointsAdded,
|
maskLayerPointsAdded,
|
||||||
|
maskLayerRectAdded,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
@ -58,6 +60,7 @@ export const useMouseEvents = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$isMouseDown.set(true);
|
$isMouseDown.set(true);
|
||||||
|
$lastMouseDownPos.set(pos);
|
||||||
if (!selectedLayerId) {
|
if (!selectedLayerId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -81,12 +84,26 @@ export const useMouseEvents = () => {
|
|||||||
if (!stage) {
|
if (!stage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// const tool = getTool();
|
$isMouseDown.set(false);
|
||||||
if ((tool === 'brush' || tool === 'eraser') && $isMouseDown.get()) {
|
const pos = $cursorPosition.get();
|
||||||
$isMouseDown.set(false);
|
const lastPos = $lastMouseDownPos.get();
|
||||||
|
const tool = $tool.get();
|
||||||
|
if (pos && lastPos && selectedLayerId && tool === 'rect') {
|
||||||
|
dispatch(
|
||||||
|
maskLayerRectAdded({
|
||||||
|
layerId: selectedLayerId,
|
||||||
|
rect: {
|
||||||
|
x: Math.min(pos.x, lastPos.x),
|
||||||
|
y: Math.min(pos.y, lastPos.y),
|
||||||
|
width: Math.abs(pos.x - lastPos.x),
|
||||||
|
height: Math.abs(pos.y - lastPos.y),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
$lastMouseDownPos.set(null);
|
||||||
},
|
},
|
||||||
[tool]
|
[dispatch, selectedLayerId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMouseMove = useCallback(
|
const onMouseMove = useCallback(
|
||||||
@ -99,7 +116,6 @@ export const useMouseEvents = () => {
|
|||||||
if (!pos || !selectedLayerId) {
|
if (!pos || !selectedLayerId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// const tool = getTool();
|
|
||||||
if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) {
|
if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) {
|
||||||
dispatch(maskLayerPointsAdded({ layerId: selectedLayerId, point: [Math.floor(pos.x), Math.floor(pos.y)] }));
|
dispatch(maskLayerPointsAdded({ layerId: selectedLayerId, point: [Math.floor(pos.x), Math.floor(pos.y)] }));
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
|
import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
|
||||||
import { createAction, createSlice, isAnyOf } from '@reduxjs/toolkit';
|
import { createSlice, isAnyOf } 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 { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
|
||||||
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
|
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
|
||||||
@ -13,7 +13,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
|
|
||||||
type DrawingTool = 'brush' | 'eraser';
|
type DrawingTool = 'brush' | 'eraser';
|
||||||
|
|
||||||
export type RPTool = DrawingTool | 'move';
|
export type Tool = DrawingTool | 'move' | 'rect';
|
||||||
|
|
||||||
type VectorMaskLine = {
|
type VectorMaskLine = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -81,7 +81,7 @@ export const initialRegionalPromptsState: RegionalPromptsState = {
|
|||||||
brushSize: 100,
|
brushSize: 100,
|
||||||
brushColor: { r: 255, g: 0, b: 0, a: 1 },
|
brushColor: { r: 255, g: 0, b: 0, a: 1 },
|
||||||
layers: [],
|
layers: [],
|
||||||
globalMaskLayerOpacity: 0.5, // This currently doesn't work
|
globalMaskLayerOpacity: 0.5, // this globally changes all mask layers' opacity
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
name: 'regionalPrompts',
|
name: 'regionalPrompts',
|
||||||
initialState: initialRegionalPromptsState,
|
initialState: initialRegionalPromptsState,
|
||||||
reducers: {
|
reducers: {
|
||||||
//#region Any Layers
|
//#region All Layers
|
||||||
layerAdded: {
|
layerAdded: {
|
||||||
reducer: (state, action: PayloadAction<Layer['kind'], string, { uuid: string }>) => {
|
reducer: (state, action: PayloadAction<Layer['kind'], string, { uuid: string }>) => {
|
||||||
const kind = action.payload;
|
const kind = action.payload;
|
||||||
@ -189,6 +189,7 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
state.selectedLayerId = null;
|
state.selectedLayerId = null;
|
||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Mask Layers
|
//#region Mask Layers
|
||||||
maskLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
|
maskLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
|
||||||
const { layerId, prompt } = action.payload;
|
const { layerId, prompt } = action.payload;
|
||||||
@ -258,6 +259,29 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
layer.bboxNeedsUpdate = true;
|
layer.bboxNeedsUpdate = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
maskLayerRectAdded: {
|
||||||
|
reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect }, string, { uuid: string }>) => {
|
||||||
|
const { layerId, rect } = action.payload;
|
||||||
|
if (rect.height === 0 || rect.width === 0) {
|
||||||
|
// Ignore zero-area rectangles
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (layer) {
|
||||||
|
const id = getVectorMaskLayerRectId(layer.id, action.meta.uuid);
|
||||||
|
layer.objects.push({
|
||||||
|
kind: 'vector_mask_rect',
|
||||||
|
id,
|
||||||
|
x: rect.x - layer.x,
|
||||||
|
y: rect.y - layer.y,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
});
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload: { layerId: string; rect: IRect }) => ({ payload, meta: { uuid: uuidv4() } }),
|
||||||
|
},
|
||||||
maskLayerAutoNegativeChanged: (
|
maskLayerAutoNegativeChanged: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
|
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
|
||||||
@ -269,6 +293,7 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region General
|
//#region General
|
||||||
brushSizeChanged: (state, action: PayloadAction<number>) => {
|
brushSizeChanged: (state, action: PayloadAction<number>) => {
|
||||||
state.brushSize = action.payload;
|
state.brushSize = action.payload;
|
||||||
@ -282,6 +307,18 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
isEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
isEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isEnabled = action.payload;
|
state.isEnabled = action.payload;
|
||||||
},
|
},
|
||||||
|
undo: (state) => {
|
||||||
|
// Invalidate the bbox for all layers to prevent stale bboxes
|
||||||
|
for (const layer of state.layers) {
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
redo: (state) => {
|
||||||
|
// Invalidate the bbox for all layers to prevent stale bboxes
|
||||||
|
for (const layer of state.layers) {
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -318,7 +355,7 @@ class LayerColors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
// Any layer actions
|
// All layer actions
|
||||||
layerAdded,
|
layerAdded,
|
||||||
layerDeleted,
|
layerDeleted,
|
||||||
layerMovedBackward,
|
layerMovedBackward,
|
||||||
@ -331,17 +368,20 @@ export const {
|
|||||||
layerBboxChanged,
|
layerBboxChanged,
|
||||||
layerVisibilityToggled,
|
layerVisibilityToggled,
|
||||||
allLayersDeleted,
|
allLayersDeleted,
|
||||||
// Vector mask layer actions
|
// Mask layer actions
|
||||||
maskLayerAutoNegativeChanged,
|
maskLayerAutoNegativeChanged,
|
||||||
maskLayerPreviewColorChanged,
|
maskLayerPreviewColorChanged,
|
||||||
maskLayerLineAdded,
|
maskLayerLineAdded,
|
||||||
maskLayerNegativePromptChanged,
|
maskLayerNegativePromptChanged,
|
||||||
maskLayerPointsAdded,
|
maskLayerPointsAdded,
|
||||||
maskLayerPositivePromptChanged,
|
maskLayerPositivePromptChanged,
|
||||||
|
maskLayerRectAdded,
|
||||||
// General actions
|
// General actions
|
||||||
isEnabledChanged,
|
isEnabledChanged,
|
||||||
brushSizeChanged,
|
brushSizeChanged,
|
||||||
globalMaskLayerOpacityChanged,
|
globalMaskLayerOpacityChanged,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
} = regionalPromptsSlice.actions;
|
} = regionalPromptsSlice.actions;
|
||||||
|
|
||||||
export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts;
|
export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts;
|
||||||
@ -353,24 +393,29 @@ const migrateRegionalPromptsState = (state: any): any => {
|
|||||||
|
|
||||||
export const $isMouseDown = atom(false);
|
export const $isMouseDown = atom(false);
|
||||||
export const $isMouseOver = atom(false);
|
export const $isMouseOver = atom(false);
|
||||||
export const $tool = atom<RPTool>('brush');
|
export const $lastMouseDownPos = atom<Vector2d | null>(null);
|
||||||
|
export const $tool = atom<Tool>('brush');
|
||||||
export const $cursorPosition = atom<Vector2d | null>(null);
|
export const $cursorPosition = atom<Vector2d | null>(null);
|
||||||
|
|
||||||
// IDs for singleton layers and objects
|
// IDs for singleton Konva layers and objects
|
||||||
export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer';
|
export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer';
|
||||||
export const BRUSH_FILL_ID = 'brush_fill';
|
export const TOOL_PREVIEW_BRUSH_GROUP_ID = 'tool_preview_layer.brush_group';
|
||||||
export const BRUSH_BORDER_INNER_ID = 'brush_border_inner';
|
export const TOOL_PREVIEW_BRUSH_FILL_ID = 'tool_preview_layer.brush_fill';
|
||||||
export const BRUSH_BORDER_OUTER_ID = 'brush_border_outer';
|
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';
|
||||||
|
|
||||||
// Names (aka classes) for Konva layers and objects
|
// Names (aka classes) for Konva layers and objects
|
||||||
export const VECTOR_MASK_LAYER_NAME = 'vector_mask_layer';
|
export const VECTOR_MASK_LAYER_NAME = 'vector_mask_layer';
|
||||||
export const VECTOR_MASK_LAYER_LINE_NAME = 'vector_mask_layer.line';
|
export const VECTOR_MASK_LAYER_LINE_NAME = 'vector_mask_layer.line';
|
||||||
export const VECTOR_MASK_LAYER_OBJECT_GROUP_NAME = 'vector_mask_layer.object_group';
|
export const VECTOR_MASK_LAYER_OBJECT_GROUP_NAME = 'vector_mask_layer.object_group';
|
||||||
|
export const VECTOR_MASK_LAYER_RECT_NAME = 'vector_mask_layer.rect';
|
||||||
export const LAYER_BBOX_NAME = 'layer.bbox';
|
export const LAYER_BBOX_NAME = 'layer.bbox';
|
||||||
|
|
||||||
// Getters for non-singleton layer and object IDs
|
// Getters for non-singleton layer and object IDs
|
||||||
const getVectorMaskLayerId = (layerId: string) => `${VECTOR_MASK_LAYER_NAME}_${layerId}`;
|
const getVectorMaskLayerId = (layerId: string) => `${VECTOR_MASK_LAYER_NAME}_${layerId}`;
|
||||||
const getVectorMaskLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
|
const getVectorMaskLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
|
||||||
|
const getVectorMaskLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`;
|
||||||
export const getVectorMaskLayerObjectGroupId = (layerId: string, groupId: string) =>
|
export const getVectorMaskLayerObjectGroupId = (layerId: string, groupId: string) =>
|
||||||
`${layerId}.objectGroup_${groupId}`;
|
`${layerId}.objectGroup_${groupId}`;
|
||||||
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
|
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
|
||||||
@ -382,28 +427,25 @@ export const regionalPromptsPersistConfig: PersistConfig<RegionalPromptsState> =
|
|||||||
persistDenylist: [],
|
persistDenylist: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Payload-less actions for `redux-undo`
|
|
||||||
export const undoRegionalPrompts = createAction(`${regionalPromptsSlice.name}/undo`);
|
|
||||||
export const redoRegionalPrompts = createAction(`${regionalPromptsSlice.name}/redo`);
|
|
||||||
|
|
||||||
// These actions are _individually_ grouped together as single undoable actions
|
// These actions are _individually_ grouped together as single undoable actions
|
||||||
const undoableGroupByMatcher = isAnyOf(
|
const undoableGroupByMatcher = isAnyOf(
|
||||||
|
layerTranslated,
|
||||||
brushSizeChanged,
|
brushSizeChanged,
|
||||||
globalMaskLayerOpacityChanged,
|
globalMaskLayerOpacityChanged,
|
||||||
isEnabledChanged,
|
isEnabledChanged,
|
||||||
maskLayerPositivePromptChanged,
|
maskLayerPositivePromptChanged,
|
||||||
maskLayerNegativePromptChanged,
|
maskLayerNegativePromptChanged,
|
||||||
layerTranslated,
|
|
||||||
maskLayerPreviewColorChanged
|
maskLayerPreviewColorChanged
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// These are used to group actions into logical lines below (hate typos)
|
||||||
const LINE_1 = 'LINE_1';
|
const LINE_1 = 'LINE_1';
|
||||||
const LINE_2 = 'LINE_2';
|
const LINE_2 = 'LINE_2';
|
||||||
|
|
||||||
export const regionalPromptsUndoableConfig: UndoableOptions<RegionalPromptsState, UnknownAction> = {
|
export const regionalPromptsUndoableConfig: UndoableOptions<RegionalPromptsState, UnknownAction> = {
|
||||||
limit: 64,
|
limit: 64,
|
||||||
undoType: undoRegionalPrompts.type,
|
undoType: regionalPromptsSlice.actions.undo.type,
|
||||||
redoType: redoRegionalPrompts.type,
|
redoType: regionalPromptsSlice.actions.redo.type,
|
||||||
groupBy: (action, state, history) => {
|
groupBy: (action, state, history) => {
|
||||||
// Lines are started with `maskLayerLineAdded` and may have any number of subsequent `maskLayerPointsAdded` events.
|
// Lines are started with `maskLayerLineAdded` and may have any number of subsequent `maskLayerPointsAdded` events.
|
||||||
// We can use a double-buffer-esque trick to group each "logical" line as a single undoable action, without grouping
|
// We can use a double-buffer-esque trick to group each "logical" line as a single undoable action, without grouping
|
||||||
@ -423,7 +465,7 @@ export const regionalPromptsUndoableConfig: UndoableOptions<RegionalPromptsState
|
|||||||
},
|
},
|
||||||
filter: (action, _state, _history) => {
|
filter: (action, _state, _history) => {
|
||||||
// Ignore all actions from other slices
|
// Ignore all actions from other slices
|
||||||
if (!action.type.startsWith('regionalPrompts/')) {
|
if (!action.type.startsWith(regionalPromptsSlice.name)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// This action is triggered on state changes, including when we undo. If we do not ignore this action, when we
|
// This action is triggered on state changes, including when we undo. If we do not ignore this action, when we
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
import { getScaledFlooredCursorPosition } from 'features/regionalPrompts/hooks/mouseEventHooks';
|
import { getScaledFlooredCursorPosition } from 'features/regionalPrompts/hooks/mouseEventHooks';
|
||||||
import type { Layer, RPTool, VectorMaskLayer } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import type { Layer, Tool, VectorMaskLayer } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import {
|
import {
|
||||||
$isMouseOver,
|
$isMouseOver,
|
||||||
$tool,
|
$tool,
|
||||||
BRUSH_BORDER_INNER_ID,
|
|
||||||
BRUSH_BORDER_OUTER_ID,
|
|
||||||
BRUSH_FILL_ID,
|
|
||||||
getLayerBboxId,
|
getLayerBboxId,
|
||||||
getVectorMaskLayerObjectGroupId,
|
getVectorMaskLayerObjectGroupId,
|
||||||
isVectorMaskLayer,
|
isVectorMaskLayer,
|
||||||
LAYER_BBOX_NAME,
|
LAYER_BBOX_NAME,
|
||||||
|
TOOL_PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||||
|
TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||||
|
TOOL_PREVIEW_BRUSH_FILL_ID,
|
||||||
|
TOOL_PREVIEW_BRUSH_GROUP_ID,
|
||||||
TOOL_PREVIEW_LAYER_ID,
|
TOOL_PREVIEW_LAYER_ID,
|
||||||
|
TOOL_PREVIEW_RECT_ID,
|
||||||
VECTOR_MASK_LAYER_LINE_NAME,
|
VECTOR_MASK_LAYER_LINE_NAME,
|
||||||
VECTOR_MASK_LAYER_NAME,
|
VECTOR_MASK_LAYER_NAME,
|
||||||
VECTOR_MASK_LAYER_OBJECT_GROUP_NAME,
|
VECTOR_MASK_LAYER_OBJECT_GROUP_NAME,
|
||||||
|
VECTOR_MASK_LAYER_RECT_NAME,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { getKonvaLayerBbox } from 'features/regionalPrompts/util/bbox';
|
import { getKonvaLayerBbox } from 'features/regionalPrompts/util/bbox';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
@ -41,125 +44,163 @@ const getIsSelected = (layerId?: string | null) => {
|
|||||||
return layerId === getStore().getState().regionalPrompts.present.selectedLayerId;
|
return layerId === getStore().getState().regionalPrompts.present.selectedLayerId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectVectorMaskObjects = (node: Konva.Node) => {
|
||||||
|
return node.name() === VECTOR_MASK_LAYER_LINE_NAME || node.name() === VECTOR_MASK_LAYER_RECT_NAME;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the brush preview for the selected tool.
|
* Renders the brush preview for the selected tool.
|
||||||
* @param stage The konva stage to render on.
|
* @param stage The konva stage to render on.
|
||||||
* @param tool The selected tool.
|
* @param tool The selected tool.
|
||||||
* @param color The selected layer's color.
|
* @param color The selected layer's color.
|
||||||
* @param cursorPos The cursor position.
|
* @param cursorPos The cursor position.
|
||||||
|
* @param lastMouseDownPos The position of the last mouse down event - used for the rect tool.
|
||||||
* @param brushSize The brush size.
|
* @param brushSize The brush size.
|
||||||
*/
|
*/
|
||||||
export const renderToolPreview = (
|
export const renderToolPreview = (
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
tool: RPTool,
|
tool: Tool,
|
||||||
color: RgbColor | null,
|
color: RgbColor | null,
|
||||||
cursorPos: Vector2d | null,
|
cursorPos: Vector2d | null,
|
||||||
|
lastMouseDownPos: Vector2d | null,
|
||||||
brushSize: number
|
brushSize: number
|
||||||
) => {
|
) => {
|
||||||
const layerCount = stage.find(`.${VECTOR_MASK_LAYER_NAME}`).length;
|
const layerCount = stage.find(`.${VECTOR_MASK_LAYER_NAME}`).length;
|
||||||
// Update the stage's pointer style
|
// Update the stage's pointer style
|
||||||
if (tool === 'move') {
|
if (layerCount === 0) {
|
||||||
stage.container().style.cursor = 'default';
|
|
||||||
} else if (layerCount === 0) {
|
|
||||||
// We have no layers, so we should not render any tool
|
// We have no layers, so we should not render any tool
|
||||||
stage.container().style.cursor = 'default';
|
stage.container().style.cursor = 'default';
|
||||||
|
} else if (tool === 'move') {
|
||||||
|
// Move tool gets a pointer
|
||||||
|
stage.container().style.cursor = 'default';
|
||||||
|
} else if (tool === 'rect') {
|
||||||
|
// Move rect gets a crosshair
|
||||||
|
stage.container().style.cursor = 'crosshair';
|
||||||
} else {
|
} else {
|
||||||
|
// Else we use the brush preview
|
||||||
stage.container().style.cursor = 'none';
|
stage.container().style.cursor = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let toolPreviewLayer = stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`);
|
||||||
|
|
||||||
// Create the layer if it doesn't exist
|
// Create the layer if it doesn't exist
|
||||||
let layer = stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`);
|
if (!toolPreviewLayer) {
|
||||||
if (!layer) {
|
|
||||||
// Initialize the brush preview layer & add to the stage
|
// Initialize the brush preview layer & add to the stage
|
||||||
layer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, visible: tool !== 'move', listening: false });
|
toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, visible: tool !== 'move', listening: false });
|
||||||
stage.add(layer);
|
stage.add(toolPreviewLayer);
|
||||||
// The brush preview is hidden and shown as the mouse leaves and enters the stage
|
|
||||||
|
// Add handlers to show/hide the brush preview layer
|
||||||
stage.on('mousemove', (e) => {
|
stage.on('mousemove', (e) => {
|
||||||
|
const tool = $tool.get();
|
||||||
e.target
|
e.target
|
||||||
.getStage()
|
.getStage()
|
||||||
?.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)
|
?.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)
|
||||||
?.visible($tool.get() !== 'move');
|
?.visible(tool === 'brush' || tool === 'eraser');
|
||||||
});
|
});
|
||||||
stage.on('mouseleave', (e) => {
|
stage.on('mouseleave', (e) => {
|
||||||
e.target.getStage()?.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(false);
|
e.target.getStage()?.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(false);
|
||||||
});
|
});
|
||||||
stage.on('mouseenter', (e) => {
|
stage.on('mouseenter', (e) => {
|
||||||
|
const tool = $tool.get();
|
||||||
e.target
|
e.target
|
||||||
.getStage()
|
.getStage()
|
||||||
?.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)
|
?.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)
|
||||||
?.visible($tool.get() !== 'move');
|
?.visible(tool === 'brush' || tool === 'eraser');
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (!$isMouseOver.get()) {
|
// Create the brush preview group & circles
|
||||||
layer.visible(false);
|
const brushPreviewGroup = new Konva.Group({ id: TOOL_PREVIEW_BRUSH_GROUP_ID });
|
||||||
return;
|
const brushPreviewFill = new Konva.Circle({
|
||||||
}
|
id: TOOL_PREVIEW_BRUSH_FILL_ID,
|
||||||
|
|
||||||
// ...but we may want to hide it if it is visible, when using the move tool or when there are no layers
|
|
||||||
layer.visible(tool !== 'move' && layerCount > 0);
|
|
||||||
|
|
||||||
// No need to render the brush preview if the cursor position or color is missing
|
|
||||||
if (!cursorPos || !color) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and/or update the fill circle
|
|
||||||
let fill = layer.findOne<Konva.Circle>(`#${BRUSH_FILL_ID}`);
|
|
||||||
if (!fill) {
|
|
||||||
fill = new Konva.Circle({
|
|
||||||
id: BRUSH_FILL_ID,
|
|
||||||
listening: false,
|
listening: false,
|
||||||
strokeEnabled: false,
|
strokeEnabled: false,
|
||||||
});
|
});
|
||||||
layer.add(fill);
|
brushPreviewGroup.add(brushPreviewFill);
|
||||||
}
|
const brushPreviewBorderInner = new Konva.Circle({
|
||||||
fill.setAttrs({
|
id: TOOL_PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||||
x: cursorPos.x,
|
|
||||||
y: cursorPos.y,
|
|
||||||
radius: brushSize / 2,
|
|
||||||
fill: rgbColorToString(color),
|
|
||||||
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create and/or update the inner border of the brush preview
|
|
||||||
let borderInner = layer.findOne<Konva.Circle>(`#${BRUSH_BORDER_INNER_ID}`);
|
|
||||||
if (!borderInner) {
|
|
||||||
borderInner = new Konva.Circle({
|
|
||||||
id: BRUSH_BORDER_INNER_ID,
|
|
||||||
listening: false,
|
listening: false,
|
||||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
strokeEnabled: true,
|
strokeEnabled: true,
|
||||||
});
|
});
|
||||||
layer.add(borderInner);
|
brushPreviewGroup.add(brushPreviewBorderInner);
|
||||||
}
|
const brushPreviewBorderOuter = new Konva.Circle({
|
||||||
borderInner.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 });
|
id: TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||||
|
|
||||||
// Create and/or update the outer border of the brush preview
|
|
||||||
let borderOuter = layer.findOne<Konva.Circle>(`#${BRUSH_BORDER_OUTER_ID}`);
|
|
||||||
if (!borderOuter) {
|
|
||||||
borderOuter = new Konva.Circle({
|
|
||||||
id: BRUSH_BORDER_OUTER_ID,
|
|
||||||
listening: false,
|
listening: false,
|
||||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
strokeEnabled: true,
|
strokeEnabled: true,
|
||||||
});
|
});
|
||||||
layer.add(borderOuter);
|
brushPreviewGroup.add(brushPreviewBorderOuter);
|
||||||
|
toolPreviewLayer.add(brushPreviewGroup);
|
||||||
|
|
||||||
|
// Create the rect preview
|
||||||
|
const rectPreview = new Konva.Rect({ id: TOOL_PREVIEW_RECT_ID, listening: false, stroke: 'white', strokeWidth: 1 });
|
||||||
|
toolPreviewLayer.add(rectPreview);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isMouseOver.get() || layerCount === 0) {
|
||||||
|
// We can bail early if the mouse isn't over the stage or there are no layers
|
||||||
|
toolPreviewLayer.visible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolPreviewLayer.visible(true);
|
||||||
|
|
||||||
|
const brushPreviewGroup = stage.findOne<Konva.Group>(`#${TOOL_PREVIEW_BRUSH_GROUP_ID}`);
|
||||||
|
assert(brushPreviewGroup, 'Brush preview group not found');
|
||||||
|
|
||||||
|
const rectPreview = stage.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`);
|
||||||
|
assert(rectPreview, 'Rect preview not found');
|
||||||
|
|
||||||
|
// No need to render the brush preview if the cursor position or color is missing
|
||||||
|
if (cursorPos && color && (tool === 'brush' || tool === 'eraser')) {
|
||||||
|
// Update the fill circle
|
||||||
|
const brushPreviewFill = brushPreviewGroup.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`);
|
||||||
|
brushPreviewFill?.setAttrs({
|
||||||
|
x: cursorPos.x,
|
||||||
|
y: cursorPos.y,
|
||||||
|
radius: brushSize / 2,
|
||||||
|
fill: rgbColorToString(color),
|
||||||
|
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the inner border of the brush preview
|
||||||
|
const brushPreviewInner = toolPreviewLayer.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_BORDER_INNER_ID}`);
|
||||||
|
brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 });
|
||||||
|
|
||||||
|
// Update the outer border of the brush preview
|
||||||
|
const brushPreviewOuter = toolPreviewLayer.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID}`);
|
||||||
|
brushPreviewOuter?.setAttrs({
|
||||||
|
x: cursorPos.x,
|
||||||
|
y: cursorPos.y,
|
||||||
|
radius: brushSize / 2 + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
brushPreviewGroup.visible(true);
|
||||||
|
} else {
|
||||||
|
brushPreviewGroup.visible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorPos && lastMouseDownPos && tool === 'rect') {
|
||||||
|
const rectPreview = toolPreviewLayer.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`);
|
||||||
|
rectPreview?.setAttrs({
|
||||||
|
x: Math.min(cursorPos.x, lastMouseDownPos.x),
|
||||||
|
y: Math.min(cursorPos.y, lastMouseDownPos.y),
|
||||||
|
width: Math.abs(cursorPos.x - lastMouseDownPos.x),
|
||||||
|
height: Math.abs(cursorPos.y - lastMouseDownPos.y),
|
||||||
|
});
|
||||||
|
rectPreview?.visible(true);
|
||||||
|
} else {
|
||||||
|
rectPreview?.visible(false);
|
||||||
}
|
}
|
||||||
borderOuter.setAttrs({
|
|
||||||
x: cursorPos.x,
|
|
||||||
y: cursorPos.y,
|
|
||||||
radius: brushSize / 2 + 1,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderVectorMaskLayer = (
|
const renderVectorMaskLayer = (
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
vmLayer: VectorMaskLayer,
|
vmLayer: VectorMaskLayer,
|
||||||
vmLayerIndex: number,
|
vmLayerIndex: number,
|
||||||
tool: RPTool,
|
tool: Tool,
|
||||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
) => {
|
) => {
|
||||||
let konvaLayer = stage.findOne<Konva.Layer>(`#${vmLayer.id}`);
|
let konvaLayer = stage.findOne<Konva.Layer>(`#${vmLayer.id}`);
|
||||||
@ -233,7 +274,7 @@ const renderVectorMaskLayer = (
|
|||||||
let groupNeedsCache = false;
|
let groupNeedsCache = false;
|
||||||
|
|
||||||
const objectIds = vmLayer.objects.map(mapId);
|
const objectIds = vmLayer.objects.map(mapId);
|
||||||
for (const objectNode of konvaObjectGroup.find(`.${VECTOR_MASK_LAYER_LINE_NAME}`)) {
|
for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) {
|
||||||
if (!objectIds.includes(objectNode.id())) {
|
if (!objectIds.includes(objectNode.id())) {
|
||||||
objectNode.destroy();
|
objectNode.destroy();
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
@ -272,6 +313,26 @@ const renderVectorMaskLayer = (
|
|||||||
vectorMaskLine.stroke(rgbColor);
|
vectorMaskLine.stroke(rgbColor);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
|
} else if (reduxObject.kind === 'vector_mask_rect') {
|
||||||
|
let konvaObject = stage.findOne<Konva.Rect>(`#${reduxObject.id}`);
|
||||||
|
if (!konvaObject) {
|
||||||
|
konvaObject = new Konva.Rect({
|
||||||
|
id: reduxObject.id,
|
||||||
|
key: reduxObject.id,
|
||||||
|
name: VECTOR_MASK_LAYER_RECT_NAME,
|
||||||
|
x: reduxObject.x,
|
||||||
|
y: reduxObject.y,
|
||||||
|
width: reduxObject.width,
|
||||||
|
height: reduxObject.height,
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
konvaObjectGroup.add(konvaObject);
|
||||||
|
}
|
||||||
|
// Only update the color if it has changed.
|
||||||
|
if (konvaObject.fill() !== rgbColor) {
|
||||||
|
konvaObject.fill(rgbColor);
|
||||||
|
groupNeedsCache = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,7 +370,7 @@ const renderVectorMaskLayer = (
|
|||||||
export const renderLayers = (
|
export const renderLayers = (
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
reduxLayers: Layer[],
|
reduxLayers: Layer[],
|
||||||
tool: RPTool,
|
tool: Tool,
|
||||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
) => {
|
) => {
|
||||||
const reduxLayerIds = reduxLayers.map(mapId);
|
const reduxLayerIds = reduxLayers.map(mapId);
|
||||||
@ -342,16 +403,17 @@ export const renderBbox = (
|
|||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
reduxLayers: Layer[],
|
reduxLayers: Layer[],
|
||||||
selectedLayerId: string | null,
|
selectedLayerId: string | null,
|
||||||
tool: RPTool,
|
tool: Tool,
|
||||||
onBboxChanged: (layerId: string, bbox: IRect | null) => void,
|
onBboxChanged: (layerId: string, bbox: IRect | null) => void,
|
||||||
onBboxMouseDown: (layerId: string) => void
|
onBboxMouseDown: (layerId: string) => void
|
||||||
) => {
|
) => {
|
||||||
|
// Hide all bboxes so they don't interfere with getClientRect
|
||||||
|
for (const bboxRect of stage.find<Konva.Rect>(`.${LAYER_BBOX_NAME}`)) {
|
||||||
|
bboxRect.visible(false);
|
||||||
|
bboxRect.listening(false);
|
||||||
|
}
|
||||||
// No selected layer or not using the move tool - nothing more to do here
|
// No selected layer or not using the move tool - nothing more to do here
|
||||||
if (tool !== 'move') {
|
if (tool !== 'move') {
|
||||||
for (const bboxRect of stage.find<Konva.Rect>(`.${LAYER_BBOX_NAME}`)) {
|
|
||||||
bboxRect.visible(false);
|
|
||||||
bboxRect.listening(false);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,10 +468,10 @@ export const renderBbox = (
|
|||||||
rect.setAttrs({
|
rect.setAttrs({
|
||||||
visible: true,
|
visible: true,
|
||||||
listening: true,
|
listening: true,
|
||||||
x: bbox.x - 1,
|
x: bbox.x,
|
||||||
y: bbox.y - 1,
|
y: bbox.y,
|
||||||
width: bbox.width + 2,
|
width: bbox.width,
|
||||||
height: bbox.height + 2,
|
height: bbox.height,
|
||||||
stroke: reduxLayer.id === selectedLayerId ? BBOX_SELECTED_STROKE : BBOX_NOT_SELECTED_STROKE,
|
stroke: reduxLayer.id === selectedLayerId ? BBOX_SELECTED_STROKE : BBOX_NOT_SELECTED_STROKE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user