diff --git a/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts index 08407b02e0..ddf6a89785 100644 --- a/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts +++ b/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; import { addMonomerToCanvas, + dragMouseTo, selectEraseTool, selectRectangleArea, selectRectangleSelectionTool, selectSingleBondTool, + takeEditorScreenshot, } from '@utils'; import { turnOnMacromoleculesEditor } from '@utils/macromolecules'; import { bondTwoMonomers } from '@utils/macromolecules/polymerBond'; @@ -69,4 +71,46 @@ test.describe('Rectangle Selection Tool', () => { path: 'tests/Macromolecule-editor/screenshots/rectangle-selection-tool3.png', }); }); + + test.skip('Move monomer bonded with another monomers', async ({ page }) => { + /* + Test case: #2367 - move items on the canvas + Description: check ability to move items on the canvas + */ + + // Choose peptide + await page.getByText('Tza').click(); + + // Create 4 peptides on canvas + await page.mouse.click(300, 300); + await page.mouse.click(400, 400); + await page.mouse.click(500, 500); + await page.mouse.click(600, 600); + + // Get 4 peptides locators + const peptides = await page.getByText('Tza').locator('..'); + const peptide1 = peptides.nth(0); + const peptide2 = peptides.nth(1); + const peptide3 = peptides.nth(2); + const peptide4 = peptides.nth(3); + + // Select bond tool + await selectSingleBondTool(page); + + // Create bonds between peptides + await bondTwoMonomers(page, peptide1, peptide2); + await bondTwoMonomers(page, peptide3, peptide2); + await bondTwoMonomers(page, peptide3, peptide4); + + await takeEditorScreenshot(page); + + // Move selected monomer + await selectRectangleSelectionTool(page); + await page.mouse.move(400, 400); + await dragMouseTo(500, 500, page); + await page.mouse.move(400, 400); + await dragMouseTo(200, 400, page); + + await takeEditorScreenshot(page); + }); }); diff --git a/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts-snapshots/Rectangle-Selection-Tool-Move-monomer-bonded-with-another-monomers-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts-snapshots/Rectangle-Selection-Tool-Move-monomer-bonded-with-another-monomers-1-chromium-linux.png new file mode 100644 index 0000000000..daa061a6ea Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts-snapshots/Rectangle-Selection-Tool-Move-monomer-bonded-with-another-monomers-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts-snapshots/Rectangle-Selection-Tool-Move-monomer-bonded-with-another-monomers-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts-snapshots/Rectangle-Selection-Tool-Move-monomer-bonded-with-another-monomers-2-chromium-linux.png new file mode 100644 index 0000000000..43842803f7 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts-snapshots/Rectangle-Selection-Tool-Move-monomer-bonded-with-another-monomers-2-chromium-linux.png differ diff --git a/packages/ketcher-core/__tests__/application/editor/tools/SelectRectangleTool.test.ts b/packages/ketcher-core/__tests__/application/editor/tools/SelectRectangleTool.test.ts index 14e1bf6f41..2565e3afde 100644 --- a/packages/ketcher-core/__tests__/application/editor/tools/SelectRectangleTool.test.ts +++ b/packages/ketcher-core/__tests__/application/editor/tools/SelectRectangleTool.test.ts @@ -7,6 +7,8 @@ import { } from '../../../mock-data'; import { createPolymerEditorCanvas } from '../../../helpers/dom'; import { SelectRectangle } from 'application/editor/tools/SelectRectangle'; +import { Vec2 } from 'domain/entities/vec2'; +import { BaseMonomerRenderer } from 'application/render/renderers'; jest.mock('d3', () => { return { @@ -69,4 +71,44 @@ describe('Select Rectangle Tool', () => { canvas.dispatchEvent(new Event('mouseover', { bubbles: true })); expect(onShow).toHaveBeenCalled(); }); + + it('should move selected entity', () => { + const canvas: SVGSVGElement = createPolymerEditorCanvas(); + const editor = new CoreEditor({ + theme: polymerEditorTheme, + canvas, + }); + + const modelChanges = editor.drawingEntitiesManager.addMonomer( + peptideMonomerItem, + new Vec2(0, 0), + ); + editor.renderersContainer.update(modelChanges); + const peptide = Array.from(editor.drawingEntitiesManager.monomers)[0][1]; + const onMove = jest.fn(); + jest + .spyOn(BaseMonomerRenderer.prototype, 'moveSelection') + .mockImplementation(onMove); + + const selectRectangleTool = new SelectRectangle(editor); + + const initialPosition = peptide.position; + const event = { + target: { + __data__: peptide.renderer, + }, + pageX: initialPosition.x, + pageY: initialPosition.y, + }; + + editor.drawingEntitiesManager.selectDrawingEntity(peptide); + selectRectangleTool.mousedown(event); + editor.lastCursorPosition.x = initialPosition.x + 100; + editor.lastCursorPosition.y = initialPosition.y + 100; + + selectRectangleTool.mousemove(); + selectRectangleTool.mouseup(event); + + expect(onMove).toHaveBeenCalled(); + }); }); diff --git a/packages/ketcher-core/src/application/editor/operations/drawingEntity/index.ts b/packages/ketcher-core/src/application/editor/operations/drawingEntity/index.ts index 1520285f9c..83238d1794 100644 --- a/packages/ketcher-core/src/application/editor/operations/drawingEntity/index.ts +++ b/packages/ketcher-core/src/application/editor/operations/drawingEntity/index.ts @@ -17,3 +17,10 @@ export class DrawingEntitySelectOperation implements Operation { renderersManager.selectDrawingEntity(this.drawingEntity); } } +export class DrawingEntityMoveOperation implements Operation { + constructor(private drawingEntity: DrawingEntity) {} + + public execute(renderersManager: RenderersManager) { + renderersManager.moveDrawingEntity(this.drawingEntity); + } +} diff --git a/packages/ketcher-core/src/application/editor/tools/SelectRectangle.ts b/packages/ketcher-core/src/application/editor/tools/SelectRectangle.ts index 91ee112b4a..b243242ff9 100644 --- a/packages/ketcher-core/src/application/editor/tools/SelectRectangle.ts +++ b/packages/ketcher-core/src/application/editor/tools/SelectRectangle.ts @@ -23,6 +23,8 @@ import { BaseTool } from 'application/editor/tools/Tool'; class SelectRectangle implements BaseTool { private brush; private brushArea; + private moveStarted; + private mousePositionAfterMove; constructor(private editor: CoreEditor) { this.editor = editor; @@ -69,9 +71,18 @@ class SelectRectangle implements BaseTool { const renderer = event.target.__data__; let modelChanges: Command; if (renderer instanceof BaseRenderer) { - modelChanges = this.editor.drawingEntitiesManager.selectDrawingEntity( - renderer.drawingEntity, - ); + if (renderer.drawingEntity.selected) { + this.moveStarted = true; + this.mousePositionAfterMove = [ + this.editor.lastCursorPosition.x, + this.editor.lastCursorPosition.y, + ]; + return; + } else { + modelChanges = this.editor.drawingEntitiesManager.selectDrawingEntity( + renderer.drawingEntity, + ); + } } else { modelChanges = this.editor.drawingEntitiesManager.unselectAllDrawingEntities(); @@ -79,6 +90,30 @@ class SelectRectangle implements BaseTool { this.editor.renderersContainer.update(modelChanges); } + mousemove() { + if (this.moveStarted) { + const modelChanges = + this.editor.drawingEntitiesManager.moveSelectedDrawingEntities( + new Vec2( + this.editor.lastCursorPosition.x - this.mousePositionAfterMove[0], + this.editor.lastCursorPosition.y - this.mousePositionAfterMove[1], + ), + ); + this.mousePositionAfterMove = [ + this.editor.lastCursorPosition.x, + this.editor.lastCursorPosition.y, + ]; + this.editor.renderersContainer.update(modelChanges); + } + } + + mouseup(event) { + const renderer = event.target.__data__; + if (this.moveStarted && renderer.drawingEntity.selected) { + this.moveStarted = false; + } + } + mouseOverDrawingEntity(event) { const renderer = event.target.__data__; const modelChanges = diff --git a/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts b/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts index 0ee647165b..4be1061f78 100644 --- a/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts +++ b/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts @@ -278,6 +278,12 @@ export abstract class BaseMonomerRenderer extends BaseRenderer { } } + public moveSelection() { + assert(this.rootElement); + this.appendSelection(); + this.move(); + } + public move() { this.rootElement?.attr( 'transform', diff --git a/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts b/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts index 4fc92a8f4c..e6f54505b0 100644 --- a/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts +++ b/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts @@ -85,6 +85,7 @@ export abstract class BaseRenderer implements IBaseRenderer { public abstract show(theme): void; public abstract drawSelection(): void; + public abstract moveSelection(): void; protected abstract appendHover( hoverArea, ): D3SvgElementSelection | void; diff --git a/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer.ts b/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer.ts index a62077be9d..97740d4504 100644 --- a/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer.ts +++ b/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer.ts @@ -30,6 +30,12 @@ export class PolymerBondRenderer extends BaseRenderer { return this.rootBBox?.height || 0; } + public moveSelection() { + assert(this.rootElement); + this.moveStart(); + this.moveEnd(); + } + public appendBond(rootElement) { this.bodyElement = rootElement .append('line') diff --git a/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts b/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts index 37e2f82894..90e93dba70 100644 --- a/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts +++ b/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts @@ -26,6 +26,11 @@ export class RenderersManager { drawingEntity.baseRenderer.drawSelection(); } + public moveDrawingEntity(drawingEntity: DrawingEntity) { + assert(drawingEntity.baseRenderer); + drawingEntity.baseRenderer.moveSelection(); + } + public addMonomer(monomer: BaseMonomer, callback?: () => void) { const [, MonomerRenderer] = monomerFactory(monomer.monomerItem); const monomerRenderer = new MonomerRenderer(monomer); diff --git a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts index 58de7a69eb..0d5faeaaf3 100644 --- a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts +++ b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts @@ -14,6 +14,7 @@ import { import { DrawingEntityHoverOperation, DrawingEntitySelectOperation, + DrawingEntityMoveOperation, } from 'application/editor/operations/drawingEntity'; import { PolymerBondAddOperation, @@ -116,6 +117,38 @@ export class DrawingEntitiesManager { return command; } + public moveSelectedDrawingEntities(offset: Vec2) { + const command = new Command(); + + this.monomers.forEach((drawingEntity) => { + if (drawingEntity.selected) { + drawingEntity.moveRelative(offset); + command.merge(this.createDrawingEntityMovingCommand(drawingEntity)); + } + }); + + this.polymerBonds.forEach((drawingEntity) => { + if ( + drawingEntity.selected || + drawingEntity.firstMonomer.selected || + drawingEntity.secondMonomer?.selected + ) { + drawingEntity.moveToLinkedMonomers(); + command.merge(this.createDrawingEntityMovingCommand(drawingEntity)); + } + }); + return command; + } + + public createDrawingEntityMovingCommand(drawingEntity: DrawingEntity) { + const command = new Command(); + + const movingCommand = new DrawingEntityMoveOperation(drawingEntity); + command.addOperation(movingCommand); + + return command; + } + public deleteMonomer(monomer: BaseMonomer) { this.monomers.delete(monomer.id); const command = new Command(); @@ -402,6 +435,7 @@ export class DrawingEntitiesManager { let monomerAddOperation; if (previousMonomer) { const polymerBond = new PolymerBond(previousMonomer); + this.polymerBonds.set(polymerBond.id, polymerBond); monomerAddOperation = new MonomerAddOperation(monomer, () => { polymerBond.moveToLinkedMonomers(); });