Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@
"title": "Rect Tool",
"desc": "Select the rect tool."
},
"selectLassoTool": {
"title": "Lasso Tool",
"desc": "Select the lasso tool."
},
"selectViewTool": {
"title": "View Tool",
"desc": "Select the view tool."
Expand Down Expand Up @@ -2570,10 +2574,16 @@
"radial": "Radial",
"clip": "Clip Gradient"
},
"lasso": {
"freehand": "Freehand",
"polygon": "Polygon",
"polygonHint": "Click to add points, click the first point to close."
},
"tool": {
"brush": "Brush",
"eraser": "Eraser",
"rectangle": "Rectangle",
"lasso": "Lasso",
"gradient": "Gradient",
"bbox": "Bbox",
"move": "Move",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ToolBboxButton } from 'features/controlLayers/components/Tool/ToolBboxB
import { ToolBrushButton } from 'features/controlLayers/components/Tool/ToolBrushButton';
import { ToolColorPickerButton } from 'features/controlLayers/components/Tool/ToolColorPickerButton';
import { ToolGradientButton } from 'features/controlLayers/components/Tool/ToolGradientButton';
import { ToolLassoButton } from 'features/controlLayers/components/Tool/ToolLassoButton';
import { ToolMoveButton } from 'features/controlLayers/components/Tool/ToolMoveButton';
import { ToolRectButton } from 'features/controlLayers/components/Tool/ToolRectButton';
import { ToolTextButton } from 'features/controlLayers/components/Tool/ToolTextButton';
Expand All @@ -20,6 +21,7 @@ export const ToolChooser: React.FC = () => {
<ToolRectButton />
<ToolGradientButton />
<ToolTextButton />
<ToolLassoButton />
<ToolMoveButton />
<ToolViewButton />
<ToolBboxButton />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiLassoBold } from 'react-icons/pi';

export const ToolLassoButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('lasso');
const selectLasso = useSelectTool('lasso');

useRegisteredHotkeys({
id: 'selectLassoTool',
category: 'canvas',
callback: selectLasso,
options: { enabled: !isSelected },
dependencies: [isSelected, selectLasso],
});

return (
<Tooltip label={`${t('controlLayers.tool.lasso', { defaultValue: 'Lasso' })} (L)`} placement="end">
<IconButton
aria-label={`${t('controlLayers.tool.lasso', { defaultValue: 'Lasso' })} (L)`}
icon={<PiLassoBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="solid"
onClick={selectLasso}
/>
</Tooltip>
);
});

ToolLassoButton.displayName = 'ToolLassoButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ButtonGroup, IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectLassoMode, settingsLassoModeChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPolygonBold, PiScribbleLoopBold } from 'react-icons/pi';

export const ToolLassoModeToggle = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const lassoMode = useAppSelector(selectLassoMode);

const setFreehand = useCallback(() => {
dispatch(settingsLassoModeChanged('freehand'));
}, [dispatch]);

const setPolygon = useCallback(() => {
dispatch(settingsLassoModeChanged('polygon'));
}, [dispatch]);

return (
<ButtonGroup isAttached size="sm">
<Tooltip label={t('controlLayers.lasso.freehand', { defaultValue: 'Freehand' })}>
<IconButton
aria-label={t('controlLayers.lasso.freehand', { defaultValue: 'Freehand' })}
icon={<PiScribbleLoopBold size={16} />}
colorScheme={lassoMode === 'freehand' ? 'invokeBlue' : 'base'}
variant="solid"
onClick={setFreehand}
/>
</Tooltip>
<Tooltip label={t('controlLayers.lasso.polygon', { defaultValue: 'Polygon' })}>
<IconButton
aria-label={t('controlLayers.lasso.polygonHint', {
defaultValue: 'Click to add points, click the first point to close.',
})}
icon={<PiPolygonBold size={16} />}
colorScheme={lassoMode === 'polygon' ? 'invokeBlue' : 'base'}
variant="solid"
onClick={setPolygon}
/>
</Tooltip>
</ButtonGroup>
);
});

ToolLassoModeToggle.displayName = 'ToolLassoModeToggle';
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'
import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolGradientClipToggle } from 'features/controlLayers/components/Tool/ToolGradientClipToggle';
import { ToolGradientModeToggle } from 'features/controlLayers/components/Tool/ToolGradientModeToggle';
import { ToolLassoModeToggle } from 'features/controlLayers/components/Tool/ToolLassoModeToggle';
import { ToolOptionsRowContainer } from 'features/controlLayers/components/Tool/ToolOptionsRowContainer';
import { ToolWidthPicker } from 'features/controlLayers/components/Tool/ToolWidthPicker';
import { CanvasToolbarFitBboxToLayersButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton';
Expand All @@ -31,6 +32,7 @@ export const CanvasToolbar = memo(() => {
const isBrushSelected = useToolIsSelected('brush');
const isEraserSelected = useToolIsSelected('eraser');
const isTextSelected = useToolIsSelected('text');
const isLassoSelected = useToolIsSelected('lasso');
const isGradientSelected = useToolIsSelected('gradient');
const showToolWithPicker = useMemo(() => {
return !isTextSelected && (isBrushSelected || isEraserSelected);
Expand All @@ -57,6 +59,11 @@ export const CanvasToolbar = memo(() => {
<ToolGradientModeToggle />
</Box>
)}
{isLassoSelected && (
<Box ms={2} mt="-2px" display="flex" alignItems="center" gap={2}>
<ToolLassoModeToggle />
</Box>
)}
{isTextSelected ? <TextToolOptions /> : showToolWithPicker && <ToolWidthPicker />}
</ToolOptionsRowContainer>
<Flex alignItems="center" h="full">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObjec
import { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
import { CanvasObjectGradient } from 'features/controlLayers/konva/CanvasObject/CanvasObjectGradient';
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
import { CanvasObjectLasso } from 'features/controlLayers/konva/CanvasObject/CanvasObjectLasso';
import { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
import type { AnyObjectRenderer, AnyObjectState } from 'features/controlLayers/konva/CanvasObject/types';
import { getPrefixedId } from 'features/controlLayers/konva/util';
Expand Down Expand Up @@ -152,6 +153,15 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
this.konva.group.add(this.renderer.konva.group);
}

didRender = this.renderer.update(this.state, true);
} else if (this.state.type === 'lasso') {
assert(this.renderer instanceof CanvasObjectLasso || !this.renderer);

if (!this.renderer) {
this.renderer = new CanvasObjectLasso(this.state, this);
this.konva.group.add(this.renderer.konva.group);
}

didRender = this.renderer.update(this.state, true);
} else if (this.state.type === 'gradient') {
assert(this.renderer instanceof CanvasObjectGradient || !this.renderer);
Expand Down Expand Up @@ -247,6 +257,9 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
case 'rect':
this.manager.stateApi.addRect({ entityIdentifier, rect: this.state });
break;
case 'lasso':
this.manager.stateApi.addLasso({ entityIdentifier, lasso: this.state });
break;
case 'gradient':
this.manager.stateApi.addGradient({ entityIdentifier, gradient: this.state });
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObjec
import { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
import { CanvasObjectGradient } from 'features/controlLayers/konva/CanvasObject/CanvasObjectGradient';
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
import { CanvasObjectLasso } from 'features/controlLayers/konva/CanvasObject/CanvasObjectLasso';
import { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
import type { AnyObjectRenderer, AnyObjectState } from 'features/controlLayers/konva/CanvasObject/types';
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters';
Expand Down Expand Up @@ -401,6 +402,16 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
this.konva.objectGroup.add(renderer.konva.group);
}

didRender = renderer.update(objectState, force || isFirstRender);
} else if (objectState.type === 'lasso') {
assert(renderer instanceof CanvasObjectLasso || !renderer);

if (!renderer) {
renderer = new CanvasObjectLasso(objectState, this);
this.renderers.set(renderer.id, renderer);
this.konva.objectGroup.add(renderer.konva.group);
}

didRender = renderer.update(objectState, force || isFirstRender);
} else if (objectState.type === 'gradient') {
assert(renderer instanceof CanvasObjectGradient || !renderer);
Expand Down Expand Up @@ -437,17 +448,21 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
* these visually transparent shapes in its calculation:
*
* - Eraser lines, which are normal lines with a globalCompositeOperation of 'destination-out'.
* - Subtracting lasso shapes, which use a globalCompositeOperation of 'destination-out'.
* - Clipped portions of any shape.
* - Images, which may have transparent areas.
*/
needsPixelBbox = (): boolean => {
let needsPixelBbox = false;
for (const renderer of this.renderers.values()) {
const isEraserLine = renderer instanceof CanvasObjectEraserLine;
const isEraserLine =
renderer instanceof CanvasObjectEraserLine || renderer instanceof CanvasObjectEraserLineWithPressure;
const isSubtractingLasso =
renderer instanceof CanvasObjectLasso && renderer.state.compositeOperation === 'destination-out';
const isImage = renderer instanceof CanvasObjectImage;
const imageIgnoresTransparency = isImage && renderer.state.usePixelBbox === false;
const hasClip = renderer instanceof CanvasObjectBrushLine && renderer.state.clip;
if (isEraserLine || hasClip || (isImage && !imageIgnoresTransparency)) {
if (isEraserLine || isSubtractingLasso || hasClip || (isImage && !imageIgnoresTransparency)) {
needsPixelBbox = true;
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { deepClone } from 'common/util/deepClone';
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer';
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import type { CanvasLassoState } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { Logger } from 'roarr';

export class CanvasObjectLasso extends CanvasModuleBase {
readonly type = 'object_lasso';
readonly id: string;
readonly path: string[];
readonly parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer;
readonly manager: CanvasManager;
readonly log: Logger;

state: CanvasLassoState;
konva: {
group: Konva.Group;
line: Konva.Line;
};

constructor(state: CanvasLassoState, parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer) {
super();
this.id = state.id;
this.parent = parent;
this.manager = parent.manager;
this.path = this.manager.buildPath(this);
this.log = this.manager.buildLogger(this);

this.log.debug({ state }, 'Creating module');

this.konva = {
group: new Konva.Group({
name: `${this.type}:group`,
listening: false,
}),
line: new Konva.Line({
name: `${this.type}:line`,
listening: false,
closed: true,
fill: 'white',
strokeEnabled: false,
perfectDrawEnabled: false,
}),
};
this.konva.group.add(this.konva.line);
this.state = state;
}

update(state: CanvasLassoState, force = false): boolean {
if (force || this.state !== state) {
this.log.trace({ state }, 'Updating lasso');
this.konva.line.setAttrs({
points: state.points,
globalCompositeOperation: state.compositeOperation,
});
this.state = state;
return true;
}

return false;
}

setVisibility(isVisible: boolean): void {
this.log.trace({ isVisible }, 'Setting lasso visibility');
this.konva.group.visible(isVisible);
}

destroy = () => {
this.log.debug('Destroying module');
this.konva.group.destroy();
};

repr = () => {
return {
id: this.id,
type: this.type,
path: this.path,
parent: this.parent.id,
state: deepClone(this.state),
};
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { CanvasObjectEraserLine } from 'features/controlLayers/konva/Canvas
import type { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
import type { CanvasObjectGradient } from 'features/controlLayers/konva/CanvasObject/CanvasObjectGradient';
import type { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
import type { CanvasObjectLasso } from 'features/controlLayers/konva/CanvasObject/CanvasObjectLasso';
import type { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
import type {
CanvasBrushLineState,
Expand All @@ -12,6 +13,7 @@ import type {
CanvasEraserLineWithPressureState,
CanvasGradientState,
CanvasImageState,
CanvasLassoState,
CanvasRectState,
} from 'features/controlLayers/store/types';

Expand All @@ -25,6 +27,7 @@ export type AnyObjectRenderer =
| CanvasObjectEraserLine
| CanvasObjectEraserLineWithPressure
| CanvasObjectRect
| CanvasObjectLasso
| CanvasObjectImage
| CanvasObjectGradient;
/**
Expand All @@ -37,4 +40,5 @@ export type AnyObjectState =
| CanvasEraserLineWithPressureState
| CanvasImageState
| CanvasRectState
| CanvasLassoState
| CanvasGradientState;
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
entityBrushLineAdded,
entityEraserLineAdded,
entityGradientAdded,
entityLassoAdded,
entityMovedBy,
entityMovedTo,
entityRasterized,
Expand All @@ -43,6 +44,7 @@ import type {
EntityEraserLineAddedPayload,
EntityGradientAddedPayload,
EntityIdentifierPayload,
EntityLassoAddedPayload,
EntityMovedByPayload,
EntityMovedToPayload,
EntityRasterizedPayload,
Expand Down Expand Up @@ -175,6 +177,13 @@ export class CanvasStateApiModule extends CanvasModuleBase {
this.store.dispatch(entityRectAdded(arg));
};

/**
* Adds a lasso object to an entity, pushing state to redux.
*/
addLasso = (arg: EntityLassoAddedPayload) => {
this.store.dispatch(entityLassoAdded(arg));
};

/**
* Adds a gradient to an entity, pushing state to redux.
*/
Expand Down
Loading