Skip to content

Commit 69d86a7

Browse files
feat(ui): address feedback
1 parent 56db1a9 commit 69d86a7

File tree

6 files changed

+122
-37
lines changed

6 files changed

+122
-37
lines changed

invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer.ts

+24-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getEmptyRect,
1010
getKonvaNodeDebugAttrs,
1111
getPrefixedId,
12+
offsetCoord,
1213
} from 'features/controlLayers/konva/util';
1314
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
1415
import type { Coordinate, Rect, RectWithRotation } from 'features/controlLayers/store/types';
@@ -548,18 +549,33 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
548549
}
549550

550551
const pixelRect = this.$pixelRect.get();
551-
this.nudgePosition(-pixelRect.x, -pixelRect.y);
552-
};
553552

554-
nudgePosition = (x: number, y: number) => {
555-
// Nudge the position by (x, y) pixels
556553
const position = {
557-
x: this.konva.proxyRect.x() + x,
558-
y: this.konva.proxyRect.y() + y,
554+
x: this.konva.proxyRect.x() - pixelRect.x,
555+
y: this.konva.proxyRect.y() - pixelRect.y,
559556
};
560-
// Push state to redux
561-
this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.entityIdentifier, position });
557+
562558
this.log.trace({ position }, 'Position changed');
559+
this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.entityIdentifier, position });
560+
};
561+
562+
nudgeBy = (offset: Coordinate) => {
563+
// We can immediately move both the proxy rect and layer objects so we don't have to wait for a redux round-trip,
564+
// which can take up to 2ms in my testing. This is optional, but can make the interaction feel more responsive,
565+
// especially on lower-end devices.
566+
// Get the relative position of the layer's objects, according to konva
567+
const position = this.konva.proxyRect.position();
568+
// Offset the position by the nudge amount
569+
const newPosition = offsetCoord(position, offset);
570+
// Set the new position of the proxy rect - this doesn't move the layer objects - only the outline rect
571+
this.konva.proxyRect.setAttrs(newPosition);
572+
// Sync the layer objects with the proxy rect - moves them to the new position
573+
this.syncObjectGroupWithProxyRect();
574+
575+
// Push to redux. The state change will do a round-trip, and eventually make it back to the canvas classes, at
576+
// which point the layer will be moved to the new position.
577+
this.manager.stateApi.moveEntityBy({ entityIdentifier: this.parent.entityIdentifier, offset });
578+
this.log.trace({ offset }, 'Nudged');
563579
};
564580

565581
syncObjectGroupWithProxyRect = () => {

invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
controlLayerAdded,
2121
entityBrushLineAdded,
2222
entityEraserLineAdded,
23-
entityMoved,
23+
entityMovedBy,
24+
entityMovedTo,
2425
entityRasterized,
2526
entityRectAdded,
2627
entityReset,
@@ -40,7 +41,8 @@ import type {
4041
EntityBrushLineAddedPayload,
4142
EntityEraserLineAddedPayload,
4243
EntityIdentifierPayload,
43-
EntityMovedPayload,
44+
EntityMovedByPayload,
45+
EntityMovedToPayload,
4446
EntityRasterizedPayload,
4547
EntityRectAddedPayload,
4648
Rect,
@@ -139,8 +141,15 @@ export class CanvasStateApiModule extends CanvasModuleBase {
139141
/**
140142
* Updates an entity's position, pushing state to redux.
141143
*/
142-
setEntityPosition = (arg: EntityMovedPayload) => {
143-
this.store.dispatch(entityMoved(arg));
144+
setEntityPosition = (arg: EntityMovedToPayload) => {
145+
this.store.dispatch(entityMovedTo(arg));
146+
};
147+
148+
/**
149+
* Moves an entity by the give offset, pushing state to redux.
150+
*/
151+
moveEntityBy = (arg: EntityMovedByPayload) => {
152+
this.store.dispatch(entityMovedBy(arg));
144153
};
145154

146155
/**

invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasMoveToolModule.ts

+61-15
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,22 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
33
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
44
import type { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/CanvasToolModule';
55
import { getPrefixedId } from 'features/controlLayers/konva/util';
6+
import type { Coordinate } from 'features/controlLayers/store/types';
67
import type { Logger } from 'roarr';
78

9+
type CanvasMoveToolModuleConfig = {
10+
/**
11+
* The number of pixels to nudge the entity by when moving with the arrow keys.
12+
*/
13+
NUDGE_PX: number;
14+
};
15+
16+
const DEFAULT_CONFIG: CanvasMoveToolModuleConfig = {
17+
NUDGE_PX: 1,
18+
};
19+
20+
type NudgeKey = 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown';
21+
822
export class CanvasMoveToolModule extends CanvasModuleBase {
923
readonly type = 'move_tool';
1024
readonly id: string;
@@ -13,6 +27,9 @@ export class CanvasMoveToolModule extends CanvasModuleBase {
1327
readonly manager: CanvasManager;
1428
readonly log: Logger;
1529

30+
config: CanvasMoveToolModuleConfig = DEFAULT_CONFIG;
31+
nudgeOffsets: Record<NudgeKey, Coordinate>;
32+
1633
constructor(parent: CanvasToolModule) {
1734
super();
1835
this.id = getPrefixedId(this.type);
@@ -21,6 +38,17 @@ export class CanvasMoveToolModule extends CanvasModuleBase {
2138
this.path = this.manager.buildPath(this);
2239
this.log = this.manager.buildLogger(this);
2340
this.log.debug('Creating module');
41+
42+
this.nudgeOffsets = {
43+
ArrowLeft: { x: -this.config.NUDGE_PX, y: 0 },
44+
ArrowRight: { x: this.config.NUDGE_PX, y: 0 },
45+
ArrowUp: { x: 0, y: -this.config.NUDGE_PX },
46+
ArrowDown: { x: 0, y: this.config.NUDGE_PX },
47+
};
48+
}
49+
50+
isNudgeKey(key: string): key is NudgeKey {
51+
return this.nudgeOffsets[key as NudgeKey] !== undefined;
2452
}
2553

2654
syncCursorStyle = () => {
@@ -33,26 +61,44 @@ export class CanvasMoveToolModule extends CanvasModuleBase {
3361
}
3462
};
3563

36-
onKeyDown = (e: KeyboardEvent) => {
37-
// Support moving via arrow keys
38-
const OFFSET = 1; // How much to move, in px
39-
const offsets: Record<string, { x: number; y: number }> = {
40-
ArrowLeft: { x: -OFFSET, y: 0 },
41-
ArrowRight: { x: OFFSET, y: 0 },
42-
ArrowUp: { x: 0, y: -OFFSET },
43-
ArrowDown: { x: 0, y: OFFSET },
44-
};
45-
const { key } = e;
64+
nudge = (nudgeKey: NudgeKey) => {
65+
if ($focusedRegion.get() !== 'canvas') {
66+
return;
67+
}
68+
4669
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
47-
const { x: offsetX = 0, y: offsetY = 0 } = offsets[key] || {};
70+
71+
if (!selectedEntity) {
72+
return;
73+
}
4874

4975
if (
50-
!(selectedEntity && selectedEntity.$isInteractable.get() && $focusedRegion.get() === 'canvas') ||
51-
(offsetX === 0 && offsetY === 0)
76+
selectedEntity.$isDisabled.get() ||
77+
selectedEntity.$isEmpty.get() ||
78+
selectedEntity.$isLocked.get() ||
79+
selectedEntity.$isEntityTypeHidden.get()
5280
) {
53-
return; // Early return if no entity is selected or it is disabled or canvas is not focused
81+
return;
82+
}
83+
84+
const isBusy = this.manager.$isBusy.get();
85+
const isMoveToolSelected = this.parent.$tool.get() === 'move';
86+
const isThisEntityTransforming = this.manager.stateApi.$transformingAdapter.get() === selectedEntity;
87+
88+
if (isBusy) {
89+
// When the canvas is busy, we shouldn't allow nudging - except when the canvas is busy transforming the selected
90+
// entity. Nudging is allowed during transformation, regardless of the selected tool.
91+
if (!isThisEntityTransforming) {
92+
return;
93+
}
94+
} else {
95+
// Otherwise, the canvas is not busy, and we should only allow nudging when the move tool is selected.
96+
if (!isMoveToolSelected) {
97+
return;
98+
}
5499
}
55100

56-
selectedEntity.transformer.nudgePosition(offsetX, offsetY);
101+
const offset = this.nudgeOffsets[nudgeKey];
102+
selectedEntity.transformer.nudgeBy(offset);
57103
};
58104
}

invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -532,12 +532,9 @@ export class CanvasToolModule extends CanvasModuleBase {
532532
return;
533533
}
534534

535-
switch (
536-
this.$tool.get() // before repeat, as we may want to catch repeating keys
537-
) {
538-
case 'move':
539-
this.tools.move.onKeyDown(e);
540-
break;
535+
// Handle nudging - must be before repeat, as we may want to catch repeating keys
536+
if (this.tools.move.isNudgeKey(e.key)) {
537+
this.tools.move.nudge(e.key);
541538
}
542539

543540
if (e.repeat) {

invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
CanvasEntityType,
1919
CanvasInpaintMaskState,
2020
CanvasMetadata,
21+
EntityMovedByPayload,
2122
FillStyle,
2223
RegionalGuidanceReferenceImageState,
2324
RgbColor,
@@ -51,7 +52,7 @@ import type {
5152
EntityBrushLineAddedPayload,
5253
EntityEraserLineAddedPayload,
5354
EntityIdentifierPayload,
54-
EntityMovedPayload,
55+
EntityMovedToPayload,
5556
EntityRasterizedPayload,
5657
EntityRectAddedPayload,
5758
IPMethodV2,
@@ -1201,7 +1202,7 @@ export const canvasSlice = createSlice({
12011202
}
12021203
entity.fill.style = style;
12031204
},
1204-
entityMoved: (state, action: PayloadAction<EntityMovedPayload>) => {
1205+
entityMovedTo: (state, action: PayloadAction<EntityMovedToPayload>) => {
12051206
const { entityIdentifier, position } = action.payload;
12061207
const entity = selectEntity(state, entityIdentifier);
12071208
if (!entity) {
@@ -1212,6 +1213,20 @@ export const canvasSlice = createSlice({
12121213
entity.position = position;
12131214
}
12141215
},
1216+
entityMovedBy: (state, action: PayloadAction<EntityMovedByPayload>) => {
1217+
const { entityIdentifier, offset } = action.payload;
1218+
const entity = selectEntity(state, entityIdentifier);
1219+
if (!entity) {
1220+
return;
1221+
}
1222+
1223+
if (!isRenderableEntity(entity)) {
1224+
return;
1225+
}
1226+
1227+
entity.position.x += offset.x;
1228+
entity.position.y += offset.y;
1229+
},
12151230
entityRasterized: (state, action: PayloadAction<EntityRasterizedPayload>) => {
12161231
const { entityIdentifier, imageObject, position, replaceObjects, isSelected } = action.payload;
12171232
const entity = selectEntity(state, entityIdentifier);
@@ -1505,7 +1520,8 @@ export const {
15051520
entityIsLockedToggled,
15061521
entityFillColorChanged,
15071522
entityFillStyleChanged,
1508-
entityMoved,
1523+
entityMovedTo,
1524+
entityMovedBy,
15091525
entityDuplicated,
15101526
entityRasterized,
15111527
entityBrushLineAdded,

invokeai/frontend/web/src/features/controlLayers/store/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,8 @@ export type EntityIdentifierPayload<
439439
entityIdentifier: CanvasEntityIdentifier<U>;
440440
} & T;
441441

442-
export type EntityMovedPayload = EntityIdentifierPayload<{ position: Coordinate }>;
442+
export type EntityMovedToPayload = EntityIdentifierPayload<{ position: Coordinate }>;
443+
export type EntityMovedByPayload = EntityIdentifierPayload<{ offset: Coordinate }>;
443444
export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{
444445
brushLine: CanvasBrushLineState | CanvasBrushLineWithPressureState;
445446
}>;

0 commit comments

Comments
 (0)