-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow moving layers using the keyboard arrow keys #7202
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Users will be very pleased to have this feature - its been requested quite a few times.
I noticed two issues, and suggested some changes for efficiency and clarity.
const OFFSET = 1; // How much to move, in px | ||
const offsets: Record<string, { x: number; y: number }> = { | ||
ArrowLeft: { x: -OFFSET, y: 0 }, | ||
ArrowRight: { x: OFFSET, y: 0 }, | ||
ArrowUp: { x: 0, y: -OFFSET }, | ||
ArrowDown: { x: 0, y: OFFSET }, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move these constants outside the callback, either to class property or outside the class, so we don't need to recreate them every keydown
}; | ||
const { key } = e; | ||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter(); | ||
const { x: offsetX = 0, y: offsetY = 0 } = offsets[key] || {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's clearer to do this:
const offsets = KEY_TO_OFFSETS[e.key]
if (!offsets) {
return
}
// ...
selectedEntity.transformer.nudgePosition(offset.x, offset.y);
const { x: offsetX = 0, y: offsetY = 0 } = offsets[key] || {}; | ||
|
||
if ( | ||
!(selectedEntity && selectedEntity.$isInteractable.get() && $focusedRegion.get() === 'canvas') || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting the selected entity is the most expensive part check, so we can be a bit more verbose to improve perf when checking if we should bail early. For example:
if ($focusedRegion.get() !== 'canvas') {
return;
}
const offsets = KEY_TO_OFFSETS[e.key];
if (!offsets) {
return;
}
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (!selectedEntity || !selectedEntity.$isInteractable.get()) {
return;
}
Also, I forgot to mention we should also bail if this.manager.$isBusy.get() === true
. This flag indicates that something else is happening on the canvas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't work quite how we need it to, because an entity's objects are positioned relative to the entity's position. For example:
Here, the entity position is 0,0
, but the objects - represented by the interaction proxy rect - are at a different position.
As a result, when we use do a single-pixel nudge, we offset the entity by the object position + the single-pixel nudge offsets.
Screen.Recording.2024-10-29.at.1.24.59.pm.mov
Instead of handling this with logic in the transformer class, I think it's best to create a redux action specifically for moving an entity by a certain amount. Proposal:
- Rename the
entityMoved
redux action toentityMovedTo
. That's the one here: https://github.com/hippalectryon-0/InvokeAI/blob/keyboard_move/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts#L1170-L1180 - Create a new
entityMovedBy
redux action which accepts aoffset: Coordinate
. - Add a method to
CanvasStateApiModule
to dispatchentityMovedBy
action. Could be called something likemoveEntityBy
. - Update
CanvasEntityTransformer.nudgePosition
to call this new method without doing any konva stuff.* - Revert the changes to
CanvasEntityTransformer.onDragEnd
.
Optionally, you could do something like this to immediately move the entity and its objects before waiting for the redux round-trip (untested):
nudgePosition = (offset: Coordinate) => {
//#region Optional perf optimization
// We can immediately move both the proxy rect and layer objects so we don't have to wait for a redux round-trip,
// which can take up to 2ms in my testing. This is optional, but can make the interaction feel more responsive,
// especially on lower-end devices.
// Get the relative position of the layer's objects, according to konva
const position = this.konva.proxyRect.position();
// Offset the position by the nudge amount
const newPosition = offsetCoord(position, offset);
// Set the new position of the proxy rect - this doesn't move the layer objects - only the outline rect
this.konva.proxyRect.setAttrs(newPosition);
// Sync the layer objects with the proxy rect - moves them to the new position
this.syncObjectGroupWithProxyRect();
//#endregion
// Push to redux. The state change will do a round-trip, and eventually make it back to the canvas classes, at
// which point the layer will be moved to the new position.
this.manager.stateApi.moveEntityBy({ entityIdentifier: this.parent.entityIdentifier, offset });
};
Summary
When the move tool is selected, this PR allows to use the keyboard keys to move the selected layer by 1px.
Related Issues / Discussions
#7120
Known issues
The layer moves even if it's disabledfixedChecklist