From f405e472ea9fcefafc951529daf39dcdd5fb596f Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 19 Jul 2024 13:16:02 +1000
Subject: [PATCH] fix(ui): move tool fixes, add transform tool
---
.../controlLayers/components/ToolChooser.tsx | 2 +
.../components/TransformToolButton.tsx | 35 +++++
.../controlLayers/konva/CanvasLayer.ts | 131 ++++++++++++------
3 files changed, 127 insertions(+), 41 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
index 706d51b74c..b9b6c8ca84 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
@@ -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 = () => {
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx
new file mode 100644
index 0000000000..e8ac2e2577
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx
@@ -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 (
+ }
+ variant={isSelected ? 'solid' : 'outline'}
+ onClick={onClick}
+ isDisabled={isDisabled}
+ />
+ );
+});
+
+TransformToolButton.displayName = 'TransformToolButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts
index df8392c4f5..e3073df65b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts
@@ -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;
- 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();