Skip to content

Commit

Permalink
#2909 - movement of structure ceases when it reaches the boundaries o…
Browse files Browse the repository at this point in the history
…f canvas requiring the consistent movement of mouse to continue its motion (#3342)

* #2909 - Add smooth movement

* #2909 - Refactor code

* #2909 - Make canvas movement framerate independent

* #2909 - Refactor

* #2909 - Remove scrollMultiplier

* #2909 - Add type for previousMouseMoveEvent
  • Loading branch information
nanoblit authored Sep 25, 2023
1 parent 802097c commit 4d84438
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 84 deletions.
67 changes: 36 additions & 31 deletions packages/ketcher-core/src/application/render/canvasExtension.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,47 @@
import { Vec2 } from 'domain/entities';

const edgeOffset = 150;
const scrollMultiplier = 2;
const moveDelta = 1;
let lastX = 0;
let lastY = 0;

export function getDirections(event) {
const { layerX, layerY } = event;
const isMovingRight = layerX - lastX > moveDelta;
const isMovingLeft = lastX - layerX > moveDelta;
const isMovingTop = lastY - layerY > moveDelta;
const isMovingBottom = layerY - lastY > moveDelta;
lastX = layerX;
lastY = layerY;
return { isMovingRight, isMovingLeft, isMovingTop, isMovingBottom };
}

export function isCloseToEdgeOfScreen(event) {
const { clientX, clientY } = event;
const body = document.body;

const isCloseToLeftEdgeOfScreen = clientX <= edgeOffset;
const isCloseToTopEdgeOfScreen = clientY <= edgeOffset;
const isCloseToRightEdgeOfScreen = body.clientWidth - clientX <= edgeOffset;
const isCloseToBottomEdgeOfScreen = body.clientHeight - clientY <= edgeOffset;
const isCloseToSomeEdgeOfScreen =
isCloseToLeftEdgeOfScreen ||
isCloseToTopEdgeOfScreen ||
isCloseToRightEdgeOfScreen ||
isCloseToBottomEdgeOfScreen;

return {
isCloseToLeftEdgeOfScreen,
isCloseToTopEdgeOfScreen,
isCloseToRightEdgeOfScreen,
isCloseToBottomEdgeOfScreen,
isCloseToSomeEdgeOfScreen,
};
}

export function isCloseToEdgeOfCanvas(event, canvasSize) {
const { layerX, layerY } = event;
const isCloseToLeftEdgeOfCanvas = layerX <= edgeOffset;
const isCloseToTopEdgeOfCanvas = layerY <= edgeOffset;
const isCloseToRightEdgeOfCanvas = canvasSize.x - layerX <= edgeOffset;
const isCloseToBottomEdgeOfCanvas = canvasSize.y - layerY <= edgeOffset;
export function isCloseToEdgeOfCanvas(clientArea) {
const {
scrollTop,
scrollLeft,
clientWidth,
clientHeight,
scrollWidth,
scrollHeight,
} = clientArea;

const isCloseToLeftEdgeOfCanvas = scrollLeft <= edgeOffset;
const isCloseToTopEdgeOfCanvas = scrollTop <= edgeOffset;
const isCloseToRightEdgeOfCanvas =
scrollLeft + clientWidth + edgeOffset >= scrollWidth;
const isCloseToBottomEdgeOfCanvas =
scrollTop + clientHeight + edgeOffset >= scrollHeight;

return {
isCloseToLeftEdgeOfCanvas,
isCloseToTopEdgeOfCanvas,
Expand All @@ -53,24 +57,30 @@ export function calculateCanvasExtension(
) {
const newHorizontalScrollPosition = clientArea.scrollLeft + extensionVector.x;
const newVerticalScrollPosition = clientArea.scrollTop + extensionVector.y;

let horizontalExtension = 0;
let verticalExtension = 0;

if (newHorizontalScrollPosition > currentCanvasSize.x) {
horizontalExtension = newHorizontalScrollPosition - currentCanvasSize.x;
}

if (newHorizontalScrollPosition < 0) {
horizontalExtension = Math.abs(newHorizontalScrollPosition);
}

if (newVerticalScrollPosition > currentCanvasSize.y) {
verticalExtension = newVerticalScrollPosition - currentCanvasSize.y;
}

if (newVerticalScrollPosition < 0) {
verticalExtension = Math.abs(newVerticalScrollPosition);
}

return new Vec2(horizontalExtension, verticalExtension, 0);
}

export function shiftAndExtendCanvasByVector(vector: Vec2, render) {
export function extendCanvasByVector(vector: Vec2, render) {
const clientArea = render.clientArea;
const extensionVector = calculateCanvasExtension(
clientArea,
Expand All @@ -83,19 +93,14 @@ export function shiftAndExtendCanvasByVector(vector: Vec2, render) {
render.setOffset(render.options.offset.add(vector));
render.ctab.translate(vector);
render.setViewBox(render.options.zoom);
/**
* When canvas extends previous (0, 0) coordinates may become (100, 100)
*/
lastX += extensionVector.x;
lastY += extensionVector.y;
}

scrollByVector(vector, render);
render.update(false);
}

export function scrollByVector(vector: Vec2, render) {
export function scrollCanvasByVector(vector: Vec2, render) {
const clientArea = render.clientArea;
clientArea.scrollLeft += (vector.x * render.options.scale) / scrollMultiplier;
clientArea.scrollTop += (vector.y * render.options.scale) / scrollMultiplier;

clientArea.scrollLeft += vector.x * render.options.scale;
clientArea.scrollTop += vector.y * render.options.scale;
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ describe('Rotate controller', () => {
selectTool.mouseup(new MouseEvent('mouseup'));

expect(updateRender).toBeCalled();
expect(selectTool.isMousedDown).toBe(false);
expect(selectTool.isMouseDown).toBe(false);

expect(editor.historyStack).toHaveLength(0);
});
Expand Down
126 changes: 76 additions & 50 deletions packages/ketcher-react/src/script/editor/tool/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ import {
Vec2,
Atom,
Bond,
getDirections,
isCloseToEdgeOfCanvas,
isCloseToEdgeOfScreen,
scrollByVector,
shiftAndExtendCanvasByVector,
scrollCanvasByVector,
extendCanvasByVector,
getItemsToFuse,
vectorUtils,
} from 'ketcher-core';
Expand All @@ -54,12 +53,30 @@ import { handleMovingPosibilityCursor } from '../utils';

type SelectMode = 'lasso' | 'fragment' | 'rectangle';

enum Direction {
LEFT,
TOP,
RIGHT,
DOWN,
}

let lastTimestamp = 0;
const canvasOffsetPerSecond = 100;

const directionOffsetMap: Record<Direction, Vec2> = {
[Direction.LEFT]: new Vec2(-canvasOffsetPerSecond, 0),
[Direction.TOP]: new Vec2(0, -canvasOffsetPerSecond),
[Direction.RIGHT]: new Vec2(canvasOffsetPerSecond, 0),
[Direction.DOWN]: new Vec2(0, canvasOffsetPerSecond),
};

class SelectTool implements Tool {
readonly #mode: SelectMode;
readonly #lassoHelper: LassoHelper;
private readonly editor: Editor;
private dragCtx: any;
isMousedDown = false;
private previousMouseMoveEvent?: MouseEvent;
isMouseDown = false;
readonly isMoving = false;

constructor(editor: Editor, mode: SelectMode) {
Expand All @@ -77,7 +94,7 @@ class SelectTool implements Tool {
}

mousedown(event) {
this.isMousedDown = true;
this.isMouseDown = true;
const rnd = this.editor.render;
const ctab = rnd.ctab;
const molecule = ctab.molecule;
Expand Down Expand Up @@ -150,10 +167,14 @@ class SelectTool implements Tool {
this.editor.selection(null);
this.editor.selection(isSelected(selection, ci) ? selection : sel);
}

this.moveCanvas();

return true;
}

mousemove(event) {
this.previousMouseMoveEvent = event;
const editor = this.editor;
const rnd = editor.render;
const restruct = editor.render.ctab;
Expand Down Expand Up @@ -224,7 +245,6 @@ class SelectTool implements Tool {
dragCtx.mergeItems = getItemsToFuse(editor, visibleSelectedItems);
editor.hover(getHoverToFuse(dragCtx.mergeItems));

resizeCanvas(rnd, event);
editor.update(dragCtx.action, true, { resizeCanvas: false });
return true;
}
Expand Down Expand Up @@ -253,10 +273,10 @@ class SelectTool implements Tool {
}

mouseup(event) {
if (!this.isMousedDown) {
if (!this.isMouseDown) {
return;
}
this.isMousedDown = false;
this.isMouseDown = false;

const editor = this.editor;
const selected = editor.selection();
Expand Down Expand Up @@ -508,66 +528,72 @@ class SelectTool implements Tool {
reArrow.isResizing = isResizing;
}
}

private moveCanvas() {
const event = this.previousMouseMoveEvent;
const render = this.editor.render;

const oneSecondInMilliseconds = 1000;
const now = Date.now();
const deltaTime =
(lastTimestamp === 0 ? 0 : now - lastTimestamp) / oneSecondInMilliseconds;
lastTimestamp = Date.now();

if (!event) {
return;
}

const { isCloseToSomeEdgeOfScreen } = isCloseToEdgeOfScreen(event);

if (isCloseToSomeEdgeOfScreen) {
this.mousemove(event);
resizeCanvas(render, event, deltaTime);
}

if (this.isMouseDown) {
requestAnimationFrame(() => this.moveCanvas());
}
}
}

function resizeCanvas(render, event) {
const offset = 1;
function resizeCanvas(render, event, deltaTime: number) {
const {
isCloseToLeftEdgeOfCanvas,
isCloseToTopEdgeOfCanvas,
isCloseToRightEdgeOfCanvas,
isCloseToBottomEdgeOfCanvas,
} = isCloseToEdgeOfCanvas(event, render.sz);
} = isCloseToEdgeOfCanvas(render.clientArea);

const {
isCloseToLeftEdgeOfScreen,
isCloseToTopEdgeOfScreen,
isCloseToRightEdgeOfScreen,
isCloseToBottomEdgeOfScreen,
} = isCloseToEdgeOfScreen(event);
const { isMovingLeft, isMovingRight, isMovingTop, isMovingBottom } =
getDirections(event);

if (isCloseToLeftEdgeOfCanvas && isMovingLeft) {
shiftAndExtendCanvasByVector(new Vec2(-offset, 0, 0), render);
}

if (isCloseToTopEdgeOfCanvas && isMovingTop) {
shiftAndExtendCanvasByVector(new Vec2(0, -offset, 0), render);
}

if (isCloseToRightEdgeOfCanvas && isMovingRight) {
shiftAndExtendCanvasByVector(new Vec2(offset, 0, 0), render);
}

if (isCloseToBottomEdgeOfCanvas && isMovingBottom) {
shiftAndExtendCanvasByVector(new Vec2(0, offset, 0), render);
}

const isCloseToSomeEdgeOfCanvas = [
isCloseToTopEdgeOfCanvas && isMovingTop,
isCloseToRightEdgeOfCanvas && isMovingRight,
isCloseToBottomEdgeOfCanvas && isMovingBottom,
isCloseToLeftEdgeOfCanvas && isMovingLeft,
].some((isCloseToEdge) => isCloseToEdge);

if (isCloseToSomeEdgeOfCanvas) {
return;
}
const directionChecksMap: Record<Direction, [boolean, boolean]> = {
[Direction.LEFT]: [isCloseToLeftEdgeOfScreen, isCloseToLeftEdgeOfCanvas],
[Direction.TOP]: [isCloseToTopEdgeOfScreen, isCloseToTopEdgeOfCanvas],
[Direction.RIGHT]: [isCloseToRightEdgeOfScreen, isCloseToRightEdgeOfCanvas],
[Direction.DOWN]: [
isCloseToBottomEdgeOfScreen,
isCloseToBottomEdgeOfCanvas,
],
};

if (isCloseToTopEdgeOfScreen && isMovingTop) {
scrollByVector(new Vec2(0, -offset), render);
}
for (const [direction, offset] of Object.entries(directionOffsetMap)) {
const [isCloseToScreenEdge, isCloseToCanvasEdge] =
directionChecksMap[direction];

if (isCloseToBottomEdgeOfScreen && isMovingBottom) {
scrollByVector(new Vec2(0, offset), render);
}
const scaledOffset = offset.scaled(deltaTime);

if (isCloseToLeftEdgeOfScreen && isMovingLeft) {
scrollByVector(new Vec2(-offset, 0), render);
}
if (isCloseToScreenEdge) {
if (isCloseToCanvasEdge) {
extendCanvasByVector(scaledOffset, render);
}

if (isCloseToRightEdgeOfScreen && isMovingRight) {
scrollByVector(new Vec2(offset, 0), render);
scrollCanvasByVector(scaledOffset, render);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
Vec2,
EditorSelection,
fromMultipleMove,
scrollByVector,
scrollCanvasByVector,
ReAtom,
} from 'ketcher-core';

Expand Down Expand Up @@ -41,7 +41,7 @@ export function moveSelectedItems(
editor.update(action, false, { resizeCanvas: true });
const isClose = isCloseToTheEdgeOfCanvas(selectedItems, editor, key);
if (isClose) {
scrollByVector(distinationVector, editor.render);
scrollCanvasByVector(distinationVector, editor.render);
}
editor.rotateController.rerender();
}
Expand Down

0 comments on commit 4d84438

Please sign in to comment.