fix(ui): move tool fixes, add transform tool

This commit is contained in:
psychedelicious 2024-07-19 13:16:02 +10:00
parent 7bdfd3ef5f
commit f405e472ea
3 changed files with 127 additions and 41 deletions
invokeai/frontend/web/src/features/controlLayers

@ -5,6 +5,7 @@ import { BrushToolButton } from 'features/controlLayers/components/BrushToolButt
import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton';
import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton';
import { RectToolButton } from 'features/controlLayers/components/RectToolButton';
import { TransformToolButton } from 'features/controlLayers/components/TransformToolButton';
import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton';
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
@ -21,6 +22,7 @@ export const ToolChooser: React.FC = () => {
<EraserToolButton />
<RectToolButton />
<MoveToolButton />
<TransformToolButton />
<ViewToolButton />
<BboxToolButton />
</ButtonGroup>

@ -0,0 +1,35 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiResizeBold } from 'react-icons/pi';
export const TransformToolButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'transform');
const isDisabled = useAppSelector(
(s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging
);
const onClick = useCallback(() => {
dispatch(toolChanged('transform'));
}, [dispatch]);
useHotkeys(['ctrl+t', 'meta+t'], onClick, { enabled: !isDisabled }, [isDisabled, onClick]);
return (
<IconButton
aria-label={`${t('unifiedCanvas.transform')} (Ctrl+T)`}
tooltip={`${t('unifiedCanvas.transform')} (Ctrl+T)`}
icon={<PiResizeBold />}
variant={isSelected ? 'solid' : 'outline'}
onClick={onClick}
isDisabled={isDisabled}
/>
);
});
TransformToolButton.displayName = 'TransformToolButton';

@ -14,6 +14,7 @@ export class CanvasLayer {
static NAME_PREFIX = 'layer';
static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`;
static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`;
static INTERACTION_RECT_NAME = `${CanvasLayer.NAME_PREFIX}_interaction-rect`;
static GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_group`;
static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`;
static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`;
@ -26,13 +27,15 @@ export class CanvasLayer {
konva: {
layer: Konva.Layer;
bbox: Konva.Rect;
group: Konva.Group;
bbox: Konva.Rect;
objectGroup: Konva.Group;
transformer: Konva.Transformer;
interactionRect: Konva.Rect;
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
bbox: Rect | null;
bbox: Rect;
getBbox = debounce(this._getBbox, 300);
@ -41,38 +44,63 @@ export class CanvasLayer {
this.manager = manager;
this.konva = {
layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }),
group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false, draggable: true }),
group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: true, draggable: true }),
bbox: new Konva.Rect({
listening: false,
draggable: false,
name: CanvasLayer.BBOX_NAME,
stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400
fill: '',
perfectDrawEnabled: false,
strokeHitEnabled: false,
}),
objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
transformer: new Konva.Transformer({
name: CanvasLayer.TRANSFORMER_NAME,
shouldOverdrawWholeArea: true,
draggable: false,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
listening: false,
}),
interactionRect: new Konva.Rect({
name: CanvasLayer.INTERACTION_RECT_NAME,
listening: false,
draggable: false,
fill: 'rgba(255,0,0,0.5)',
}),
};
this.konva.group.add(this.konva.objectGroup);
this.konva.group.add(this.konva.bbox);
this.konva.layer.add(this.konva.group);
this.konva.layer.add(this.konva.transformer);
this.konva.group.add(this.konva.objectGroup);
this.konva.group.add(this.konva.interactionRect);
this.konva.group.add(this.konva.bbox);
this.konva.transformer.on('transform', () => {
console.log(this.konva.interactionRect.position());
this.konva.objectGroup.setAttrs({
scaleX: this.konva.interactionRect.scaleX(),
scaleY: this.konva.interactionRect.scaleY(),
// rotation: this.konva.interactionRect.rotation(),
x: this.konva.interactionRect.x(),
t: this.konva.interactionRect.y(),
});
});
this.konva.transformer.on('transformend', () => {
console.log(this.bbox);
this.bbox = {
x: this.konva.interactionRect.x(),
y: this.konva.interactionRect.y(),
width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX(),
height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY(),
};
console.log(this.bbox);
this.renderBbox();
this.manager.stateApi.onScaleChanged(
{
id: this.id,
scale: this.konva.group.scaleX(),
position: { x: this.konva.group.x(), y: this.konva.group.y() },
scale: this.konva.objectGroup.scaleX(),
position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() },
},
'layer'
);
@ -83,12 +111,15 @@ export class CanvasLayer {
'layer'
);
});
this.konva.layer.add(this.konva.transformer);
this.objects = new Map();
this.drawingBuffer = null;
this.state = state;
this.bbox = null;
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
}
private static get DEFAULT_BBOX_RECT() {
return { x: 0, y: 0, width: 0, height: 0 };
}
destroy(): void {
@ -235,48 +266,50 @@ export class CanvasLayer {
if (this.objects.size > 0) {
this.getBbox();
} else {
this.bbox = null;
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
this.renderBbox();
}
}
this.konva.layer.visible(true);
this.konva.group.opacity(this.state.opacity);
this.konva.objectGroup.opacity(this.state.opacity);
const isSelected = this.manager.stateApi.getIsSelected(this.id);
const selectedTool = this.manager.stateApi.getToolState().selected;
const transformerListening = selectedTool === 'transform' && isSelected;
const bboxListening = selectedTool === 'move' && isSelected;
const isTransforming = selectedTool === 'transform' && isSelected;
const isMoving = selectedTool === 'move' && isSelected;
this.konva.layer.listening(transformerListening || bboxListening);
this.konva.transformer.listening(transformerListening);
this.konva.group.listening(bboxListening);
this.konva.bbox.listening(bboxListening);
this.konva.layer.listening(isTransforming || isMoving);
this.konva.transformer.listening(isTransforming);
this.konva.bbox.visible(isMoving);
this.konva.interactionRect.listening(isMoving);
if (this.objects.size === 0) {
// If the layer is totally empty, reset the cache and bail out.
this.konva.transformer.nodes([]);
if (this.konva.group.isCached()) {
this.konva.group.clearCache();
if (this.konva.objectGroup.isCached()) {
this.konva.objectGroup.clearCache();
}
} else if (isSelected && selectedTool === 'transform') {
// When the layer is selected and being moved, we should always cache it.
// We should update the cache if we drew to the layer.
if (!this.konva.group.isCached() || didDraw) {
// this.konva.group.cache();
if (!this.konva.objectGroup.isCached() || didDraw) {
// this.konva.objectGroup.cache();
}
// Activate the transformer
this.konva.transformer.nodes([this.konva.group]);
this.konva.transformer.nodes([this.konva.interactionRect]);
this.konva.transformer.enabledAnchors(['top-left', 'top-right', 'bottom-left', 'bottom-right']);
this.konva.transformer.forceUpdate();
this.konva.transformer.visible(true);
} else if (selectedTool === 'move') {
// When the layer is selected and being moved, we should always cache it.
// We should update the cache if we drew to the layer.
if (!this.konva.group.isCached() || didDraw) {
// this.konva.group.cache();
if (!this.konva.objectGroup.isCached() || didDraw) {
// this.konva.objectGroup.cache();
}
// Activate the transformer
this.konva.transformer.nodes([]);
this.konva.transformer.nodes([this.konva.interactionRect]);
this.konva.transformer.enabledAnchors([]);
this.konva.transformer.forceUpdate();
this.konva.transformer.visible(false);
} else if (isSelected) {
@ -286,14 +319,14 @@ export class CanvasLayer {
if (isDrawingTool(selectedTool)) {
// We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we
// should never be cached.
if (this.konva.group.isCached()) {
this.konva.group.clearCache();
if (this.konva.objectGroup.isCached()) {
this.konva.objectGroup.clearCache();
}
} else {
// We are using a non-drawing tool (move, view, bbox), so we should cache the layer.
// We should update the cache if we drew to the layer.
if (!this.konva.group.isCached() || didDraw) {
// this.konva.group.cache();
if (!this.konva.objectGroup.isCached() || didDraw) {
// this.konva.objectGroup.cache();
}
}
} else if (!isSelected) {
@ -301,8 +334,8 @@ export class CanvasLayer {
// The transformer also does not need to be active.
this.konva.transformer.nodes([]);
// Update the layer's cache if it's not already cached or we drew to it.
if (!this.konva.group.isCached() || didDraw) {
// this.konva.group.cache();
if (!this.konva.objectGroup.isCached() || didDraw) {
// this.konva.objectGroup.cache();
}
}
}
@ -310,17 +343,33 @@ export class CanvasLayer {
renderBbox() {
const isSelected = this.manager.stateApi.getIsSelected(this.id);
const selectedTool = this.manager.stateApi.getToolState().selected;
const hasBbox = this.bbox.width !== 0 && this.bbox.height !== 0;
this.konva.bbox.visible(hasBbox);
this.konva.interactionRect.visible(hasBbox);
this.konva.bbox.setAttrs({
...this.bbox,
x: this.bbox.x,
y: this.bbox.y,
width: this.bbox.width,
height: this.bbox.height,
scaleX: 1,
scaleY: 1,
strokeWidth: 1 / this.manager.stage.scaleX(),
visible: this.bbox !== null && selectedTool === 'move' && isSelected,
});
this.konva.interactionRect.setAttrs({
x: this.bbox.x,
y: this.bbox.y,
width: this.bbox.width,
height: this.bbox.height,
scaleX: 1,
scaleY: 1,
});
}
private _getBbox() {
if (this.objects.size === 0) {
this.bbox = null;
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
this.renderBbox();
return;
}
@ -338,7 +387,7 @@ export class CanvasLayer {
if (!needsPixelBbox) {
if (rect.width === 0 || rect.height === 0) {
this.bbox = null;
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
} else {
this.bbox = rect;
}
@ -370,13 +419,13 @@ export class CanvasLayer {
if (extents) {
const { minX, minY, maxX, maxY } = extents;
this.bbox = {
x: minX + rect.x - Math.floor(this.konva.layer.x()),
y: minY + rect.y - Math.floor(this.konva.layer.y()),
x: rect.x + minX,
y: rect.y + minY,
width: maxX - minX,
height: maxY - minY,
};
} else {
this.bbox = null;
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
}
this.renderBbox();
clone.destroy();