diff --git a/ketcher-autotests/playwright.config.ts b/ketcher-autotests/playwright.config.ts index 6d3094ea41..080480e718 100644 --- a/ketcher-autotests/playwright.config.ts +++ b/ketcher-autotests/playwright.config.ts @@ -134,6 +134,10 @@ const config: PlaywrightTestConfig = { launchOptions: { headless: true, }, + contextOptions: { + // chromium-specific permissions + permissions: ['clipboard-read', 'clipboard-write'], + }, }, }, diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts new file mode 100644 index 0000000000..d0db3d5ae6 --- /dev/null +++ b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts @@ -0,0 +1,56 @@ +import { test } from '@playwright/test'; +import { + takeEditorScreenshot, + waitForPageInit, + openFileAndAddToCanvasMacro, + zoomWithMouseWheel, + selectRectangleArea, + clickUndo, +} from '@utils'; +import { turnOnMacromoleculesEditor } from '@utils/macromolecules'; + +const startX = 300; +const startY = 300; +const endX = 600; +const endY = 600; +test.describe('Flex mode copy&paste', () => { + test.beforeEach(async ({ page }) => { + await waitForPageInit(page); + await turnOnMacromoleculesEditor(page); + const ZOOM_OUT_VALUE = 400; + + await openFileAndAddToCanvasMacro('KET/monomers-chains.ket', page); + await zoomWithMouseWheel(page, ZOOM_OUT_VALUE); + }); + + test('Copy & paste selection with rectangular tool and undo', async ({ + page, + }) => { + await selectRectangleArea(page, startX, startY, endX, endY); + await page.keyboard.press('Control+c'); + + await page.mouse.move(-startX, 0); + await page.keyboard.press('Control+v'); + await takeEditorScreenshot(page); + + await clickUndo(page); + await takeEditorScreenshot(page); + }); + + test('Copy & paste selection with Shift + Click and undo', async ({ + page, + }) => { + await page.keyboard.down('Shift'); + await page.getByText('SMCC').locator('..').first().click(); + await page.getByText('Test-6-Ch').locator('..').first().click(); + await page.keyboard.up('Shift'); + await page.keyboard.press('Control+c'); + + await page.mouse.move(startX, startY); + await page.keyboard.press('Control+v'); + await takeEditorScreenshot(page); + + await clickUndo(page); + await takeEditorScreenshot(page); + }); +}); diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..2d154bb187 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..61da31912e Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..a3a6ab3249 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..738bdd0725 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/flex-mode-copy-paste.spec.ts-snapshots/Flex-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts new file mode 100644 index 0000000000..e2f17dc1db --- /dev/null +++ b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts @@ -0,0 +1,149 @@ +import { test } from '@playwright/test'; +import { + takeEditorScreenshot, + waitForPageInit, + openFileAndAddToCanvasMacro, + selectSequenceLayoutModeTool, + zoomWithMouseWheel, + scrollDown, + selectRectangleArea, + moveMouseAway, + clickUndo, + openPasteFromClipboard, + readFileContents, +} from '@utils'; +import { turnOnMacromoleculesEditor } from '@utils/macromolecules'; +import { + clickOnSequenceSymbol, + getSequenceSymbolLocator, +} from '@utils/macromolecules/sequence'; + +const ZOOM_OUT_VALUE = 400; +const SCROLL_DOWN_VALUE = 250; +test.describe('Sequence mode copy&paste for view mode', () => { + test.beforeEach(async ({ page }) => { + await waitForPageInit(page); + await turnOnMacromoleculesEditor(page); + + await openFileAndAddToCanvasMacro('KET/monomers-chains.ket', page); + await selectSequenceLayoutModeTool(page); + await zoomWithMouseWheel(page, ZOOM_OUT_VALUE); + await scrollDown(page, SCROLL_DOWN_VALUE); + }); + + test('Copy & paste selection with rectangular tool', async ({ page }) => { + const startX = 100; + const startY = 100; + const endX = 500; + const endY = 500; + + await selectRectangleArea(page, startX, startY, endX, endY); + await takeEditorScreenshot(page); + await page.keyboard.press('Control+c'); + await page.keyboard.press('Control+v'); + await takeEditorScreenshot(page); + }); + + test('Select entire chain with Ctrl+Lclick then copy and paste and undo', async ({ + page, + }) => { + await page.keyboard.down('Control'); + await getSequenceSymbolLocator(page, 'G').click(); + await page.keyboard.up('Control'); + await takeEditorScreenshot(page); + await page.keyboard.press('Control+c'); + await page.keyboard.press('Control+v'); + await takeEditorScreenshot(page); + + await clickUndo(page); + await takeEditorScreenshot(page); + }); +}); + +test.describe('Sequence mode copy&paste for edit mode', () => { + test.beforeEach(async ({ page }) => { + await waitForPageInit(page); + await turnOnMacromoleculesEditor(page); + + await openFileAndAddToCanvasMacro('KET/monomers-chains.ket', page); + await selectSequenceLayoutModeTool(page); + await zoomWithMouseWheel(page, ZOOM_OUT_VALUE); + await getSequenceSymbolLocator(page, 'G').click({ button: 'right' }); + await page.getByTestId('edit_sequence').click(); + }); + + test('Copy & paste selection with LClick+drag and undo', async ({ page }) => { + await getSequenceSymbolLocator(page, 'G').hover(); + await page.mouse.down(); + const gNthNumber = 1; + await getSequenceSymbolLocator(page, 'G', gNthNumber).hover(); + + await page.mouse.up(); + await moveMouseAway(page); + await takeEditorScreenshot(page); + + await page.keyboard.press('Control+c'); + const cNthNumber = 5; + await getSequenceSymbolLocator(page, 'C', cNthNumber).click(); + await page.keyboard.press('Control+v'); + await takeEditorScreenshot(page); + + await clickUndo(page); + await takeEditorScreenshot(page); + }); + + test('Select letters with Shift & ArrowLeft then paste ket from clipboard and undo', async ({ + page, + }) => { + const fileContent = await readFileContents( + 'tests/test-data/KET/single-fragment-for-paste.ket', + ); + await openPasteFromClipboard(page, fileContent); + await page.keyboard.press('Control+a'); + await page.keyboard.press('Control+c'); + await page.getByTitle('Close window').click(); + + await clickOnSequenceSymbol(page, 'G'); + const arrowCount = 8; + await page.keyboard.down('Shift'); + for (let i = 0; i < arrowCount; i++) { + await page.keyboard.press('ArrowLeft'); + } + await page.keyboard.up('Shift'); + await takeEditorScreenshot(page); + + await page.keyboard.press('Control+v'); + await takeEditorScreenshot(page); + + await clickUndo(page); + await clickUndo(page); + await takeEditorScreenshot(page); + }); + + test('Select letters with Shift & ArrowRight then paste sequence from clipboard and undo', async ({ + page, + }) => { + await openPasteFromClipboard(page, 'atc'); + await page.keyboard.press('Control+a'); + await page.keyboard.press('Control+c'); + await page.getByTitle('Close window').click(); + + await clickOnSequenceSymbol(page, 'G'); + const arrowCount = 10; + await page.keyboard.down('Shift'); + for (let i = 0; i < arrowCount; i++) { + await page.keyboard.press('ArrowRight'); + } + await page.keyboard.up('Shift'); + await moveMouseAway(page); + + await takeEditorScreenshot(page); + + await page.keyboard.press('Control+v'); + await takeEditorScreenshot(page); + + await clickUndo(page); + await clickUndo(page); + await takeEditorScreenshot(page); + }); +}); diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..a84447e600 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..de49f1b33f Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-3-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-3-chromium-linux.png new file mode 100644 index 0000000000..f8137835d7 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-3-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--1104a-owLeft-then-paste-ket-from-clipboard-and-undo-3-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--1104a-owLeft-then-paste-ket-from-clipboard-and-undo-3-chromium-linux.png new file mode 100644 index 0000000000..9ddcb062df Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--1104a-owLeft-then-paste-ket-from-clipboard-and-undo-3-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--18554-t-then-paste-sequence-from-clipboard-and-undo-3-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--18554-t-then-paste-sequence-from-clipboard-and-undo-3-chromium-linux.png new file mode 100644 index 0000000000..058f2a0f77 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--18554-t-then-paste-sequence-from-clipboard-and-undo-3-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--49151-t-then-paste-sequence-from-clipboard-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--49151-t-then-paste-sequence-from-clipboard-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..c970a3d902 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--49151-t-then-paste-sequence-from-clipboard-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--68b8f-owLeft-then-paste-ket-from-clipboard-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--68b8f-owLeft-then-paste-ket-from-clipboard-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..4124005a4b Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--68b8f-owLeft-then-paste-ket-from-clipboard-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--d04bc-t-then-paste-sequence-from-clipboard-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--d04bc-t-then-paste-sequence-from-clipboard-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..0255e83c88 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--d04bc-t-then-paste-sequence-from-clipboard-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--fad51-owLeft-then-paste-ket-from-clipboard-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--fad51-owLeft-then-paste-ket-from-clipboard-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..327cee9125 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--fad51-owLeft-then-paste-ket-from-clipboard-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-1-chromium-linux.png new file mode 100644 index 0000000000..06b5e3878f Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-2-chromium-linux.png new file mode 100644 index 0000000000..f586841d24 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select--0f3ce-with-Ctrl-Lclick-then-copy-and-paste-and-undo-3-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select--0f3ce-with-Ctrl-Lclick-then-copy-and-paste-and-undo-3-chromium-linux.png new file mode 100644 index 0000000000..fee7bdec30 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select--0f3ce-with-Ctrl-Lclick-then-copy-and-paste-and-undo-3-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select--87e1a-with-Ctrl-Lclick-then-copy-and-paste-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select--87e1a-with-Ctrl-Lclick-then-copy-and-paste-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..a36b3b64c7 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select--87e1a-with-Ctrl-Lclick-then-copy-and-paste-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select--e2c16-with-Ctrl-Lclick-then-copy-and-paste-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select--e2c16-with-Ctrl-Lclick-then-copy-and-paste-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..5642af1097 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select--e2c16-with-Ctrl-Lclick-then-copy-and-paste-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts new file mode 100644 index 0000000000..4342b2f8c1 --- /dev/null +++ b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts @@ -0,0 +1,62 @@ +import { test } from '@playwright/test'; +import { + takeEditorScreenshot, + waitForPageInit, + openFileAndAddToCanvasMacro, + zoomWithMouseWheel, + scrollDown, + selectRectangleArea, + clickUndo, + selectSnakeLayoutModeTool, +} from '@utils'; +import { turnOnMacromoleculesEditor } from '@utils/macromolecules'; + +test.describe('Snake mode copy&paste', () => { + test.beforeEach(async ({ page }) => { + await waitForPageInit(page); + await turnOnMacromoleculesEditor(page); + const ZOOM_OUT_VALUE = 400; + const SCROLL_DOWN_VALUE = 250; + + await openFileAndAddToCanvasMacro('KET/monomers-chains.ket', page); + await selectSnakeLayoutModeTool(page); + await zoomWithMouseWheel(page, ZOOM_OUT_VALUE); + await scrollDown(page, SCROLL_DOWN_VALUE); + }); + + test('Copy & paste selection with rectangular tool and undo', async ({ + page, + }) => { + const startX = 200; + const startY = 200; + const endX = 500; + const endY = 400; + + await selectRectangleArea(page, startX, startY, endX, endY); + await takeEditorScreenshot(page); + + await page.keyboard.press('Control+c'); + await page.keyboard.press('Control+v'); + await takeEditorScreenshot(page); + + await clickUndo(page); + await takeEditorScreenshot(page); + }); + + test('Copy & paste selection with Shift + Click and undo', async ({ + page, + }) => { + await page.keyboard.down('Shift'); + await page.getByText('D').locator('..').first().click(); + await page.getByText('F').locator('..').first().click(); + await page.keyboard.up('Shift'); + await page.keyboard.press('Control+c'); + await takeEditorScreenshot(page); + + await page.keyboard.press('Control+v'); + await takeEditorScreenshot(page); + + await clickUndo(page); + await takeEditorScreenshot(page); + }); +}); diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..044067bbcb Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..ee09b648cc Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-3-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-3-chromium-linux.png new file mode 100644 index 0000000000..ff2743888d Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-Shift-Click-and-undo-3-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..4c8a500f8c Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..4740ec3b21 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-3-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-3-chromium-linux.png new file mode 100644 index 0000000000..1815269c5d Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Copy-And-Paste/snake-mode-copy-paste.spec.ts-snapshots/Snake-mode-copy-paste-Copy-paste-selection-with-rectangular-tool-and-undo-3-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..e061947231 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..e061947231 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-3-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-3-chromium-linux.png new file mode 100644 index 0000000000..c7996ec1bc Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Copy-paste-selection-with-LClick-drag-and-undo-3-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--08201-t-then-paste-sequence-from-clipboard-and-undo-3-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--08201-t-then-paste-sequence-from-clipboard-and-undo-3-chromium-linux.png new file mode 100644 index 0000000000..c7996ec1bc Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--08201-t-then-paste-sequence-from-clipboard-and-undo-3-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--0c662-t-then-paste-sequence-from-clipboard-and-undo-4-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--0c662-t-then-paste-sequence-from-clipboard-and-undo-4-chromium-linux.png new file mode 100644 index 0000000000..c7996ec1bc Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--0c662-t-then-paste-sequence-from-clipboard-and-undo-4-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--7d2a1-owLeft-then-paste-ket-from-clipboard-and-undo-3-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--7d2a1-owLeft-then-paste-ket-from-clipboard-and-undo-3-chromium-linux.png new file mode 100644 index 0000000000..c7996ec1bc Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--7d2a1-owLeft-then-paste-ket-from-clipboard-and-undo-3-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--7da46-owLeft-then-paste-ket-from-clipboard-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--7da46-owLeft-then-paste-ket-from-clipboard-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..3b5b159101 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--7da46-owLeft-then-paste-ket-from-clipboard-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--963a1-owLeft-then-paste-ket-from-clipboard-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--963a1-owLeft-then-paste-ket-from-clipboard-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..3b5b159101 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--963a1-owLeft-then-paste-ket-from-clipboard-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--e47cb-t-then-paste-sequence-from-clipboard-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--e47cb-t-then-paste-sequence-from-clipboard-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..f30d45c629 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--e47cb-t-then-paste-sequence-from-clipboard-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--e5d82-t-then-paste-sequence-from-clipboard-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--e5d82-t-then-paste-sequence-from-clipboard-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..f30d45c629 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--e5d82-t-then-paste-sequence-from-clipboard-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--f9a1d-owLeft-then-paste-ket-from-clipboard-and-undo-4-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--f9a1d-owLeft-then-paste-ket-from-clipboard-and-undo-4-chromium-linux.png new file mode 100644 index 0000000000..c7996ec1bc Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-edit-mode-Select--f9a1d-owLeft-then-paste-ket-from-clipboard-and-undo-4-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-and-undo-1-chromium-linux.png new file mode 100644 index 0000000000..c1be9247ca Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-and-undo-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-and-undo-2-chromium-linux.png new file mode 100644 index 0000000000..c7996ec1bc Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Copy-paste-selection-with-rectangular-tool-and-undo-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select-entire-chain-with-Ctrl-Lclick-then-copy-and-paste-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select-entire-chain-with-Ctrl-Lclick-then-copy-and-paste-1-chromium-linux.png new file mode 100644 index 0000000000..65b6bcbcdf Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select-entire-chain-with-Ctrl-Lclick-then-copy-and-paste-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select-entire-chain-with-Ctrl-Lclick-then-copy-and-paste-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select-entire-chain-with-Ctrl-Lclick-then-copy-and-paste-2-chromium-linux.png new file mode 100644 index 0000000000..2970ef2975 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-copy-paste.spec.ts-snapshots/Sequence-mode-copy-paste-for-view-mode-Select-entire-chain-with-Ctrl-Lclick-then-copy-and-paste-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts index 2df0a4864c..1ff813b68f 100644 --- a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts +++ b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts @@ -12,6 +12,7 @@ import { } from '@utils'; import { turnOnMacromoleculesEditor } from '@utils/macromolecules'; import { PHOSPHATE, SUGAR } from '@constants/testIdConstants'; +import { clickOnSequenceSymbol } from '@utils/macromolecules/sequence'; test.describe('Sequence mode edit in RNA Builder', () => { test.beforeEach(async ({ page }) => { @@ -23,8 +24,8 @@ test.describe('Sequence mode edit in RNA Builder', () => { }); test('Select one nucleotide and modify sugar', async ({ page }) => { - await page.getByText('T').first().click(); - await page.getByText('T').first().click({ button: 'right' }); + await clickOnSequenceSymbol(page, 'T'); + await clickOnSequenceSymbol(page, 'T', { button: 'right' }); // should see correct context menu title and available 'modify_in_rna_builder' button await takeEditorScreenshot(page); await page.getByTestId('modify_in_rna_builder').click(); @@ -47,8 +48,8 @@ test.describe('Sequence mode edit in RNA Builder', () => { }); test('Select one nucleotide and cancel modification', async ({ page }) => { - await page.getByText('T').first().click(); - await page.getByText('T').first().click({ button: 'right' }); + await clickOnSequenceSymbol(page, 'T'); + await clickOnSequenceSymbol(page, 'T', { button: 'right' }); await page.getByTestId('modify_in_rna_builder').click(); await page.getByTestId(SUGAR).click(); await page.getByTestId('25R___2,5-Ribose').click(); @@ -70,7 +71,7 @@ test.describe('Sequence mode edit in RNA Builder', () => { const endY = 200; await selectRectangleArea(page, startX, startY, endX, endY); await takeEditorScreenshot(page); - await page.getByText('T').first().click({ button: 'right' }); + await clickOnSequenceSymbol(page, 'T', { button: 'right' }); // should see correct context menu title and available 'modify_in_rna_builder' button await takeEditorScreenshot(page); await page.getByTestId('modify_in_rna_builder').click(); @@ -96,11 +97,11 @@ test.describe('Sequence mode edit in RNA Builder', () => { page, }) => { await page.keyboard.down('Control'); - await page.getByText('T').first().click(); + await clickOnSequenceSymbol(page, 'T'); await page.keyboard.up('Control'); // should see the whole chain selected await takeEditorScreenshot(page); - await page.getByText('T').first().click({ button: 'right' }); + await clickOnSequenceSymbol(page, 'T', { button: 'right' }); // should see correct context menu title and disabled 'modify_in_rna_builder' button await takeEditorScreenshot(page); }); diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts-snapshots/Sequence-mode-edit-in-RNA-Builder-Select-one-nucleotide-and-modify-sugar-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts-snapshots/Sequence-mode-edit-in-RNA-Builder-Select-one-nucleotide-and-modify-sugar-2-chromium-linux.png index 1bb73d99d8..a9a0ea51e1 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts-snapshots/Sequence-mode-edit-in-RNA-Builder-Select-one-nucleotide-and-modify-sugar-2-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts-snapshots/Sequence-mode-edit-in-RNA-Builder-Select-one-nucleotide-and-modify-sugar-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts-snapshots/Sequence-mode-edit-in-RNA-Builder-Select-one-nucleotide-and-modify-sugar-5-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts-snapshots/Sequence-mode-edit-in-RNA-Builder-Select-one-nucleotide-and-modify-sugar-5-chromium-linux.png index fcdb961526..8fa3fb67bd 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts-snapshots/Sequence-mode-edit-in-RNA-Builder-Select-one-nucleotide-and-modify-sugar-5-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit-in-rna-builder.spec.ts-snapshots/Sequence-mode-edit-in-RNA-Builder-Select-one-nucleotide-and-modify-sugar-5-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts index 130fa46a19..bac33a233c 100644 --- a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts +++ b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts @@ -6,7 +6,8 @@ import { startNewSequence, switchSequenceEnteringType, takeEditorScreenshot, - typeAllEnglishAlphabet, + typePeptideAlphabet, + typeRNADNAAlphabet, waitForPageInit, } from '@utils'; import { turnOnMacromoleculesEditor } from '@utils/macromolecules'; @@ -20,16 +21,14 @@ test.describe('Sequence edit mode', () => { test('Add/edit sequence', async ({ page }) => { await startNewSequence(page); - await typeAllEnglishAlphabet(page); + await typeRNADNAAlphabet(page); await switchSequenceEnteringType(page, SequenceType.DNA); - await typeAllEnglishAlphabet(page); + await typeRNADNAAlphabet(page); await switchSequenceEnteringType(page, SequenceType.PEPTIDE); - await typeAllEnglishAlphabet(page); + await typePeptideAlphabet(page); await page.keyboard.press('Enter'); - await typeAllEnglishAlphabet(page); + await typePeptideAlphabet(page); await takeEditorScreenshot(page); - // remove after fix the bug about opening sequence type dropdown on pressing Enter - await page.keyboard.press('Escape'); await selectSnakeLayoutModeTool(page); await takeEditorScreenshot(page); }); diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts-snapshots/Sequence-edit-mode-Add-edit-sequence-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts-snapshots/Sequence-edit-mode-Add-edit-sequence-1-chromium-linux.png index 2679a2d38b..d7fdd5dc12 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts-snapshots/Sequence-edit-mode-Add-edit-sequence-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts-snapshots/Sequence-edit-mode-Add-edit-sequence-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts-snapshots/Sequence-edit-mode-Add-edit-sequence-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts-snapshots/Sequence-edit-mode-Add-edit-sequence-2-chromium-linux.png index 9d66ec2b26..8a31d3186f 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts-snapshots/Sequence-edit-mode-Add-edit-sequence-2-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-edit.spec.ts-snapshots/Sequence-edit-mode-Add-edit-sequence-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts index 46bb482328..9aed0e3935 100644 --- a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts +++ b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts @@ -12,6 +12,7 @@ import { clickUndo, } from '@utils'; import { turnOnMacromoleculesEditor } from '@utils/macromolecules'; +import { getSequenceSymbolLocator } from '@utils/macromolecules/sequence'; test.describe('Sequence mode selection for view mode', () => { test.beforeEach(async ({ page }) => { @@ -39,15 +40,15 @@ test.describe('Sequence mode selection for view mode', () => { test('Select letters with Shift+Lclick', async ({ page }) => { await page.keyboard.down('Shift'); - await page.getByText('G').first().click(); - await page.getByText('T').first().click(); + await getSequenceSymbolLocator(page, 'G').click(); + await getSequenceSymbolLocator(page, 'G', 1).click(); await page.keyboard.up('Shift'); await takeEditorScreenshot(page); }); test('Select entire chain with Ctrl+Lclick', async ({ page }) => { await page.keyboard.down('Control'); - await page.getByText('G').first().click(); + await getSequenceSymbolLocator(page, 'G').click(); await page.keyboard.up('Control'); await takeEditorScreenshot(page); }); @@ -64,15 +65,15 @@ test.describe('Sequence mode selection for edit mode', () => { await selectSequenceLayoutModeTool(page); await zoomWithMouseWheel(page, ZOOM_OUT_VALUE); await scrollDown(page, SCROLL_DOWN_VALUE); - await page.getByText('G').first().click({ button: 'right' }); + await getSequenceSymbolLocator(page, 'G').click({ button: 'right' }); await page.getByTestId('edit_sequence').click(); }); test('Select letters with LClick+drag', async ({ page }) => { - await page.getByText('G').first().hover(); + await getSequenceSymbolLocator(page, 'G').hover(); await page.mouse.down(); const number = 5; - await page.getByText('G').nth(number).hover(); + await getSequenceSymbolLocator(page, 'G', number).hover(); await page.mouse.up(); await moveMouseAway(page); await takeEditorScreenshot(page); diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-LClick-drag-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-LClick-drag-1-chromium-linux.png index a26260a0ea..7f2fae837e 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-LClick-drag-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-LClick-drag-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-Shift-ArrowLeft-then-delete-and-undo-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-Shift-ArrowLeft-then-delete-and-undo-1-chromium-linux.png index 7d349cc61b..ab80a54289 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-Shift-ArrowLeft-then-delete-and-undo-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-Shift-ArrowLeft-then-delete-and-undo-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-Shift-ArrowRight-button-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-Shift-ArrowRight-button-2-chromium-linux.png index e08b8ed0fa..40417f6cec 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-Shift-ArrowRight-button-2-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-edit-mode-Select-letters-with-Shift-ArrowRight-button-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-entire-chain-with-Ctrl-Lclick-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-entire-chain-with-Ctrl-Lclick-1-chromium-linux.png index d48e727db5..a36b3b64c7 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-entire-chain-with-Ctrl-Lclick-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-entire-chain-with-Ctrl-Lclick-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-letters-with-Shift-Lclick-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-letters-with-Shift-Lclick-1-chromium-linux.png index c98ed8647c..0b9b9b0b93 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-letters-with-Shift-Lclick-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-letters-with-Shift-Lclick-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-letters-with-rectangular-selection-tool-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-letters-with-rectangular-selection-tool-1-chromium-linux.png index d73bc819c8..06b5e3878f 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-letters-with-rectangular-selection-tool-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode-selection.spec.ts-snapshots/Sequence-mode-selection-for-view-mode-Select-letters-with-rectangular-selection-tool-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Open-RNA-in-sequence-mode-switc-66f4b-onfirm-that-RNA-chain-layout-is-left-to-right-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Open-RNA-in-sequence-mode-switc-66f4b-onfirm-that-RNA-chain-layout-is-left-to-right-2-chromium-linux.png index 8ae1aa7736..6f2346ea83 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Open-RNA-in-sequence-mode-switc-66f4b-onfirm-that-RNA-chain-layout-is-left-to-right-2-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Open-RNA-in-sequence-mode-switc-66f4b-onfirm-that-RNA-chain-layout-is-left-to-right-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-1-chromium-linux.png index 9187a6a94a..40417f6cec 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-2-chromium-linux.png index 6ee7d3c184..d299d06bcc 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-2-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-3-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-3-chromium-linux.png index 8795486a4b..6fa2737b58 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-3-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Sequence-Mode/sequence-mode.spec.ts-snapshots/Sequence-Mode-Switch-from-flex-view-to-sequence-view-to-snake-view-and-back-to-flex-3-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-tests-After-placing-a-monomers-on-ca-2a2a3-key-Control-Z-and-Control-Y-are-working-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-tests-After-placing-a-monomers-on-ca-2a2a3-key-Control-Z-and-Control-Y-are-working-1-chromium-linux.png index 96d2fb07ef..02ea3caf85 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-tests-After-placing-a-monomers-on-ca-2a2a3-key-Control-Z-and-Control-Y-are-working-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-tests-After-placing-a-monomers-on-ca-2a2a3-key-Control-Z-and-Control-Y-are-working-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Zoom-Tool/zoom.spec.ts-snapshots/Zoom-Tool-Validate-that-mouse-scrolling-IN-OUT---zooms-into-center-of-current-mouse-position-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Zoom-Tool/zoom.spec.ts-snapshots/Zoom-Tool-Validate-that-mouse-scrolling-IN-OUT---zooms-into-center-of-current-mouse-position-1-chromium-linux.png index 849a59c1ca..801723e762 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Zoom-Tool/zoom.spec.ts-snapshots/Zoom-Tool-Validate-that-mouse-scrolling-IN-OUT---zooms-into-center-of-current-mouse-position-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Zoom-Tool/zoom.spec.ts-snapshots/Zoom-Tool-Validate-that-mouse-scrolling-IN-OUT---zooms-into-center-of-current-mouse-position-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Zoom-Tool/zoom.spec.ts-snapshots/Zoom-Tool-Validate-that-mouse-scrolling-IN-OUT---zooms-into-center-of-current-mouse-position-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Zoom-Tool/zoom.spec.ts-snapshots/Zoom-Tool-Validate-that-mouse-scrolling-IN-OUT---zooms-into-center-of-current-mouse-position-2-chromium-linux.png index 2482122b5d..8c2845d2d4 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Zoom-Tool/zoom.spec.ts-snapshots/Zoom-Tool-Validate-that-mouse-scrolling-IN-OUT---zooms-into-center-of-current-mouse-position-2-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Zoom-Tool/zoom.spec.ts-snapshots/Zoom-Tool-Validate-that-mouse-scrolling-IN-OUT---zooms-into-center-of-current-mouse-position-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/test-data/KET/single-fragment-for-paste.ket b/ketcher-autotests/tests/test-data/KET/single-fragment-for-paste.ket new file mode 100644 index 0000000000..71313ca26c --- /dev/null +++ b/ketcher-autotests/tests/test-data/KET/single-fragment-for-paste.ket @@ -0,0 +1,845 @@ +{ + "root": { + "nodes": [ + { + "$ref": "monomer370" + }, + { + "$ref": "monomer371" + }, + { + "$ref": "monomer372" + }, + { + "$ref": "monomer374" + }, + { + "$ref": "monomer375" + } + ], + "connections": [ + { + "connectionType": "single", + "endpoint1": { + "monomerId": "monomer370", + "attachmentPointId": "R1" + }, + "endpoint2": { + "monomerId": "monomer371", + "attachmentPointId": "R3" + } + }, + { + "connectionType": "single", + "endpoint1": { + "monomerId": "monomer371", + "attachmentPointId": "R2" + }, + "endpoint2": { + "monomerId": "monomer372", + "attachmentPointId": "R1" + } + }, + { + "connectionType": "single", + "endpoint1": { + "monomerId": "monomer375", + "attachmentPointId": "R1" + }, + "endpoint2": { + "monomerId": "monomer374", + "attachmentPointId": "R2" + } + }, + { + "connectionType": "single", + "endpoint1": { + "monomerId": "monomer372", + "attachmentPointId": "R2" + }, + "endpoint2": { + "monomerId": "monomer374", + "attachmentPointId": "R1" + } + } + ], + "templates": [ + { + "$ref": "monomerTemplate-A___Adenine" + }, + { + "$ref": "monomerTemplate-R___Ribose" + }, + { + "$ref": "monomerTemplate-P___Phosphate" + }, + { + "$ref": "monomerTemplate-Hcy___homocysteine" + } + ] + }, + "monomer370": { + "type": "monomer", + "id": "370", + "position": { + "x": 1.25, + "y": -2.5665 + }, + "alias": "A", + "templateId": "A___Adenine" + }, + "monomerTemplate-A___Adenine": { + "type": "monomerTemplate", + "atoms": [ + { + "label": "C", + "location": [ + 1.0354, + 0.2498, + 0 + ] + }, + { + "label": "C", + "location": [ + -0.0792, + -0.754, + 0 + ] + }, + { + "label": "C", + "location": [ + -1.5057, + -0.2906, + 0 + ] + }, + { + "label": "N", + "location": [ + -1.8177, + 1.1766, + 0 + ] + }, + { + "label": "C", + "location": [ + -0.7031, + 2.1804, + 0 + ] + }, + { + "label": "N", + "location": [ + 0.7235, + 1.717, + 0 + ] + }, + { + "label": "N", + "location": [ + -2.3871, + -1.5034, + 0 + ] + }, + { + "label": "C", + "location": [ + -1.5053, + -2.7168, + 0 + ] + }, + { + "label": "N", + "location": [ + -0.0787, + -2.2532, + 0 + ] + }, + { + "label": "N", + "location": [ + 2.1768, + -0.1209, + 0 + ] + }, + { + "label": "H", + "location": [ + -3.5871, + -1.5034, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 9 + ] + }, + { + "type": 2, + "atoms": [ + 0, + 5 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 8, + 1 + ] + }, + { + "type": 2, + "atoms": [ + 1, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 6, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 3 + ] + }, + { + "type": 2, + "atoms": [ + 3, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 4, + 5 + ] + }, + { + "type": 1, + "atoms": [ + 6, + 7 + ] + }, + { + "type": 1, + "atoms": [ + 6, + 10 + ] + }, + { + "type": 2, + "atoms": [ + 7, + 8 + ] + } + ], + "class": "Base", + "classHELM": "RNA", + "id": "A___Adenine", + "fullName": "Adenine", + "alias": "A", + "attachmentPoints": [ + { + "attachmentAtom": 6, + "leavingGroup": { + "atoms": [ + 10 + ] + }, + "type": "left" + } + ], + "naturalAnalogShort": "A" + }, + "monomer371": { + "type": "monomer", + "id": "371", + "position": { + "x": 1.25, + "y": -1.25 + }, + "alias": "R", + "templateId": "R___Ribose" + }, + "monomerTemplate-R___Ribose": { + "type": "monomerTemplate", + "atoms": [ + { + "label": "O", + "location": [ + -1.1017, + -1.0663, + 0 + ] + }, + { + "label": "C", + "location": [ + -0.5897, + 0.3436, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + 0.0809, + -1.9889, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + 0.9095, + 0.2924, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + 1.3239, + -1.1493, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "O", + "location": [ + 1.8285, + 1.4755, + 0 + ] + }, + { + "label": "O", + "location": [ + 2.4518, + -1.5589, + 0 + ] + }, + { + "label": "C", + "location": [ + -1.431, + 1.5834, + 0 + ] + }, + { + "label": "O", + "location": [ + 0.0399, + -3.1881, + 0 + ] + }, + { + "label": "O", + "location": [ + -2.9279, + 1.4755, + 0 + ] + }, + { + "label": "H", + "location": [ + -3.6017, + 2.4684, + 0 + ] + }, + { + "label": "H", + "location": [ + 3.0174, + 1.3125, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 7 + ], + "stereo": 6 + }, + { + "type": 1, + "atoms": [ + 2, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 8 + ], + "stereo": 6 + }, + { + "type": 1, + "atoms": [ + 3, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 3, + 5 + ], + "stereo": 1 + }, + { + "type": 1, + "atoms": [ + 4, + 6 + ], + "stereo": 1 + }, + { + "type": 1, + "atoms": [ + 5, + 11 + ] + }, + { + "type": 1, + "atoms": [ + 7, + 9 + ] + }, + { + "type": 1, + "atoms": [ + 9, + 10 + ] + } + ], + "class": "Sugar", + "classHELM": "RNA", + "id": "R___Ribose", + "fullName": "Ribose", + "alias": "R", + "attachmentPoints": [ + { + "attachmentAtom": 9, + "leavingGroup": { + "atoms": [ + 10 + ] + }, + "type": "left" + }, + { + "attachmentAtom": 5, + "leavingGroup": { + "atoms": [ + 11 + ] + }, + "type": "right" + }, + { + "attachmentAtom": 2, + "leavingGroup": { + "atoms": [ + 8 + ] + }, + "type": "side" + } + ], + "naturalAnalogShort": "R" + }, + "monomer372": { + "type": "monomer", + "id": "372", + "position": { + "x": 2.5187500000000003, + "y": -1.25 + }, + "alias": "P", + "templateId": "P___Phosphate" + }, + "monomerTemplate-P___Phosphate": { + "type": "monomerTemplate", + "atoms": [ + { + "label": "P", + "location": [ + -0.19991692871090108, + 0, + 0 + ] + }, + { + "label": "O", + "location": [ + -1.1999182394782262, + 0, + 0 + ] + }, + { + "label": "O", + "location": [ + 0.29983372634506966, + -0.8661678020096315, + 0 + ] + }, + { + "label": "O", + "location": [ + 0.800084382056424, + 0, + 0 + ] + }, + { + "label": "O", + "location": [ + 0.29983372634506966, + 0.8661678020096315, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 2, + "atoms": [ + 0, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 4 + ] + } + ], + "class": "Phosphate", + "classHELM": "RNA", + "id": "P___Phosphate", + "fullName": "Phosphate", + "alias": "P", + "attachmentPoints": [ + { + "attachmentAtom": 0, + "leavingGroup": { + "atoms": [ + 1 + ] + }, + "type": "left" + }, + { + "attachmentAtom": 0, + "leavingGroup": { + "atoms": [ + 3 + ] + }, + "type": "right" + } + ], + "naturalAnalogShort": "P" + }, + "monomer374": { + "type": "monomer", + "id": "374", + "position": { + "x": 3.85, + "y": -1.25 + }, + "alias": "Hcy", + "templateId": "Hcy___homocysteine" + }, + "monomerTemplate-Hcy___homocysteine": { + "type": "monomerTemplate", + "atoms": [ + { + "label": "C", + "location": [ + 1.21840298267326, + -1.1410870222550795, + 0 + ] + }, + { + "label": "C", + "location": [ + 0.26720454437806446, + -0.5929995185526877, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + 0.2655221724862781, + 0.5047847141398912, + 0 + ] + }, + { + "label": "S", + "location": [ + -0.6872854910967132, + 2.150656450534862, + 0 + ] + }, + { + "label": "N", + "location": [ + -0.6839938939171313, + -1.1410870222550795, + 0 + ] + }, + { + "label": "O", + "location": [ + 1.219719621545093, + -2.018846270143602, + 0 + ] + }, + { + "label": "O", + "location": [ + 1.978030465116786, + -0.701256492458939, + 0 + ] + }, + { + "label": "H", + "location": [ + -1.4440602559846012, + -0.7020611051028367, + 0 + ] + }, + { + "label": "H", + "location": [ + -1.4478638793921181, + 2.5888777550432067, + 0 + ] + }, + { + "label": "C", + "location": [ + -0.6856031192049269, + 1.052872217842283, + 0 + ] + } + ], + "bonds": [ + { + "type": 2, + "atoms": [ + 5, + 0 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 6 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 2 + ], + "stereo": 1 + }, + { + "type": 1, + "atoms": [ + 4, + 7 + ] + }, + { + "type": 1, + "atoms": [ + 3, + 8 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 9 + ] + }, + { + "type": 1, + "atoms": [ + 3, + 9 + ] + } + ], + "class": "AminoAcid", + "classHELM": "PEPTIDE", + "id": "Hcy___homocysteine", + "fullName": "homocysteine", + "alias": "Hcy", + "attachmentPoints": [ + { + "attachmentAtom": 4, + "leavingGroup": { + "atoms": [ + 7 + ] + }, + "type": "left" + }, + { + "attachmentAtom": 0, + "leavingGroup": { + "atoms": [ + 6 + ] + }, + "type": "right" + }, + { + "attachmentAtom": 3, + "leavingGroup": { + "atoms": [ + 8 + ] + }, + "type": "side" + } + ], + "naturalAnalogShort": "C" + }, + "monomer375": { + "type": "monomer", + "id": "375", + "position": { + "x": 5.3500000000000005, + "y": -1.25 + }, + "alias": "Hcy", + "templateId": "Hcy___homocysteine" + } +} \ No newline at end of file diff --git a/ketcher-autotests/tests/utils/canvas/tools/helpers.ts b/ketcher-autotests/tests/utils/canvas/tools/helpers.ts index fce44300c3..294dc65ba4 100644 --- a/ketcher-autotests/tests/utils/canvas/tools/helpers.ts +++ b/ketcher-autotests/tests/utils/canvas/tools/helpers.ts @@ -173,3 +173,11 @@ export async function saveStructureWithReaction(page: Page, format?: string) { export async function typeAllEnglishAlphabet(page: Page) { await page.keyboard.type('ABCDEFGHIJKLMNOPQRSTUVWXYZ'); } + +export async function typeRNADNAAlphabet(page: Page) { + await page.keyboard.type('ATGCU'); +} + +export async function typePeptideAlphabet(page: Page) { + await page.keyboard.type('ACDEFGHIKLMNPQRSTVWY'); +} diff --git a/ketcher-autotests/tests/utils/macromolecules/sequence.ts b/ketcher-autotests/tests/utils/macromolecules/sequence.ts new file mode 100644 index 0000000000..fe522cfdd5 --- /dev/null +++ b/ketcher-autotests/tests/utils/macromolecules/sequence.ts @@ -0,0 +1,21 @@ +import { Page } from '@playwright/test'; + +export async function clickOnSequenceSymbol( + page: Page, + symbolText: string, + clickOptions?: { button: 'right' | 'left' }, +) { + const symbolLocator = await getSequenceSymbolLocator(page, symbolText); + await symbolLocator.click(clickOptions); +} + +export function getSequenceSymbolLocator( + page: Page, + symbolText: string, + nthNumber = 0, +) { + return page + .getByTestId('ketcher-canvas') + .getByText(symbolText) + .nth(nthNumber); +} diff --git a/packages/ketcher-core/src/application/editor/Editor.ts b/packages/ketcher-core/src/application/editor/Editor.ts index d58b379e35..cc9511ca95 100644 --- a/packages/ketcher-core/src/application/editor/Editor.ts +++ b/packages/ketcher-core/src/application/editor/Editor.ts @@ -84,6 +84,7 @@ export class CoreEditor { this.domEventSetup(); this.setupContextMenuEvents(); this.setupKeyboardEvents(); + this.setupCopyPasteEvent(); this.canvasOffset = this.canvas.getBoundingClientRect(); this.zoomTool = ZoomTool.initInstance(this.drawingEntitiesManager); // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -133,6 +134,15 @@ export class CoreEditor { }); } + private setupCopyPasteEvent() { + document.addEventListener('copy', (event: ClipboardEvent) => { + this.mode.onCopy(event); + }); + document.addEventListener('paste', (event: ClipboardEvent) => { + this.mode.onPaste(event); + }); + } + private setupHotKeysEvents() { this.hotKeyEventHandler = (event) => this.handleHotKeyEvents(event); document.addEventListener('keydown', this.hotKeyEventHandler); diff --git a/packages/ketcher-core/src/application/editor/editorEvents.ts b/packages/ketcher-core/src/application/editor/editorEvents.ts index 9287532691..f013a1e7d7 100644 --- a/packages/ketcher-core/src/application/editor/editorEvents.ts +++ b/packages/ketcher-core/src/application/editor/editorEvents.ts @@ -16,6 +16,7 @@ export function resetEditorEvents() { layoutModeChange: new Subscription(), selectHistory: new Subscription(), error: new Subscription(), + openErrorModal: new Subscription(), openMonomerConnectionModal: new Subscription(), mouseOverPolymerBond: new Subscription(), mouseLeavePolymerBond: new Subscription(), diff --git a/packages/ketcher-core/src/application/editor/modes/BaseMode.ts b/packages/ketcher-core/src/application/editor/modes/BaseMode.ts index 0233088d54..cd389bc1c0 100644 --- a/packages/ketcher-core/src/application/editor/modes/BaseMode.ts +++ b/packages/ketcher-core/src/application/editor/modes/BaseMode.ts @@ -1,7 +1,26 @@ import { Command } from 'domain/entities/Command'; import { SelectLayoutModeOperation } from '../operations/polymerBond'; -import { CoreEditor } from '../internal'; +import { CoreEditor, EditorHistory } from '../internal'; import { LayoutMode, modesMap } from 'application/editor/modes'; +import { + getStructStringFromClipboardData, + initHotKeys, + isClipboardAPIAvailable, + keyNorm, + legacyCopy, + legacyPaste, +} from 'utilities'; +import { BaseMonomer, Struct, Vec2 } from 'domain/entities'; +import { + identifyStructFormat, + macromoleculesFilesInputFormats, + SupportedFormat, +} from 'application/formatters'; +import { KetSerializer } from 'domain/serializers'; +import { ChemicalMimeType } from 'domain/services'; +import { PolymerBond } from 'domain/entities/PolymerBond'; +import { ketcherProvider } from 'application/utils'; +import { DrawingEntitiesManager } from 'domain/entities/DrawingEntitiesManager'; export abstract class BaseMode { protected constructor( @@ -36,7 +55,240 @@ export abstract class BaseMode { return command; } - abstract onKeyDown(event: KeyboardEvent): void; + async onKeyDown(event: KeyboardEvent) { + await new Promise((resolve) => { + setTimeout(() => { + const editor = CoreEditor.provideEditorInstance(); + if (!this.checkIfTargetIsInput(event)) { + const hotKeys = initHotKeys(this.keyboardEventHandlers); + const shortcutKey = keyNorm.lookup(hotKeys, event); + this.keyboardEventHandlers[shortcutKey]?.handler(event); + } + editor.events.mouseLeaveSequenceItem.dispatch(); + resolve(); + }, 0); + }); + } + + get keyboardEventHandlers() { + return {}; + } + + abstract getNewNodePosition(); + + abstract applyAdditionalPasteOperations( + _drawingEntitiesManager: DrawingEntitiesManager, + ): Command; + + abstract isPasteAllowedByMode( + drawingEntitiesManager: DrawingEntitiesManager, + ): boolean; + + abstract scrollForView(): void; + + onCopy(event: ClipboardEvent) { + if (this.checkIfTargetIsInput(event)) { + return; + } + const editor = CoreEditor.provideEditorInstance(); + const drawingEntitiesManager = new DrawingEntitiesManager(); + editor.drawingEntitiesManager.selectedEntities.forEach(([, entity]) => { + if (entity instanceof BaseMonomer) { + drawingEntitiesManager.addMonomerChangeModel( + entity.monomerItem, + entity.position, + entity, + ); + } else if (entity instanceof PolymerBond && entity.secondMonomer) { + const firstAttachmentPoint = + entity.firstMonomer.getAttachmentPointByBond(entity); + const secondAttachmentPoint = + entity.secondMonomer?.getAttachmentPointByBond(entity); + if ( + firstAttachmentPoint && + secondAttachmentPoint && + entity.firstMonomer.selected && + entity.secondMonomer?.selected + ) { + drawingEntitiesManager.finishPolymerBondCreationModelChange( + entity.firstMonomer, + entity.secondMonomer, + firstAttachmentPoint, + secondAttachmentPoint, + entity, + ); + } + } + }); + const ketSerializer = new KetSerializer(); + const { serializedMacromolecules } = ketSerializer.serializeMacromolecules( + new Struct(), + drawingEntitiesManager, + ); + const clipboardItemString = JSON.stringify(serializedMacromolecules); + if (isClipboardAPIAvailable()) { + navigator.clipboard.writeText(clipboardItemString); + } else { + legacyCopy(event.clipboardData, { + 'text/plain': clipboardItemString, + }); + event.preventDefault(); + } + } + + async onPaste(event: ClipboardEvent) { + if (!this.checkIfTargetIsInput(event)) { + if (isClipboardAPIAvailable()) { + const clipboardData = await navigator.clipboard.read(); + this.pasteFromClipboard(clipboardData); + } else { + const clipboardData = legacyPaste(event.clipboardData, ['text/plain']); + this.pasteFromClipboard(clipboardData); + event.preventDefault(); + } + } + } + + async pasteFromClipboard(clipboardData) { + let modelChanges; + const editor = CoreEditor.provideEditorInstance(); + const pastedStr = await getStructStringFromClipboardData(clipboardData); + const format = identifyStructFormat(pastedStr, true); + if (format === SupportedFormat.ket) { + modelChanges = this.pasteKetFormatFragment(pastedStr); + } else { + let inputFormat; + let isSequenceOrFasta = false; + switch (format) { + case SupportedFormat.sequence: + inputFormat = + macromoleculesFilesInputFormats.seq[ + editor.sequenceTypeEnterMode.toLowerCase() + ]; + isSequenceOrFasta = true; + break; + case SupportedFormat.fasta: + inputFormat = + macromoleculesFilesInputFormats.fasta[ + editor.sequenceTypeEnterMode.toLowerCase() + ]; + isSequenceOrFasta = true; + break; + case SupportedFormat.molV3000: + inputFormat = macromoleculesFilesInputFormats.mol; + break; + default: + break; + } + + if (inputFormat) { + modelChanges = await this.pasteWithIndigoConversion( + pastedStr, + inputFormat, + isSequenceOrFasta, + ); + } else { + editor.events.error.dispatch('Pasted format could not be recognized.'); + } + } + + if (!modelChanges || modelChanges.operations.length === 0) { + return; + } + editor.renderersContainer.update(modelChanges); + new EditorHistory(editor).update(modelChanges); + this.scrollForView(); + } + + pasteKetFormatFragment(pastedStr: string) { + const editor = CoreEditor.provideEditorInstance(); + const ketSerializer = new KetSerializer(); + const deserialisedKet = + ketSerializer.deserializeToDrawingEntities(pastedStr); + if (!deserialisedKet) { + throw new Error('Error during parsing file'); + } + const drawingEntitiesManager = deserialisedKet?.drawingEntitiesManager; + if ( + !drawingEntitiesManager || + !this.isPasteAllowedByMode(drawingEntitiesManager) + ) { + return; + } + this.updateMonomersPosition(drawingEntitiesManager); + const { command: modelChanges, mergedDrawingEntities } = + drawingEntitiesManager.mergeInto(editor.drawingEntitiesManager); + + modelChanges.merge( + this.applyAdditionalPasteOperations(mergedDrawingEntities), + ); + + return modelChanges; + } + + async pasteWithIndigoConversion( + pastedStr: string, + inputFormat: ChemicalMimeType, + isSequenceOrFasta = false, + ) { + const indigo = ketcherProvider.getKetcher().indigo; + try { + const ketStruct = await indigo.convert( + isSequenceOrFasta ? pastedStr.toUpperCase() : pastedStr, + { + outputFormat: ChemicalMimeType.KET, + inputFormat, + }, + ); + + return this.pasteKetFormatFragment(ketStruct.struct); + } catch (error) { + const stringError = + typeof error === 'string' ? error : JSON.stringify(error); + const errorMessage = 'Convert error! ' + stringError; + + this.unsupportedSymbolsError(errorMessage, isSequenceOrFasta); + + return new Command(); + } + } + + private updateMonomersPosition( + drawingEntitiesManager: DrawingEntitiesManager, + ) { + let offset: Vec2; + let index = 0; + const newNodePosition = this.getNewNodePosition(); + drawingEntitiesManager.monomers.forEach((monomer) => { + let position; + if (index === 0) { + offset = Vec2.diff(newNodePosition, new Vec2(monomer.position)); + position = newNodePosition; + } else { + position = offset + ? new Vec2(monomer.position).add(offset) + : new Vec2(monomer.position); + } + monomer.moveAbsolute(position); + index++; + }); + } + + unsupportedSymbolsError(errorMessage: string, isSequenceOrFasta = false) { + const editor = CoreEditor.provideEditorInstance(); + editor.events.openErrorModal.dispatch({ + errorTitle: isSequenceOrFasta ? 'Unsupported symbols' : 'Error', + errorMessage, + }); + } + + private checkIfTargetIsInput(event: Event) { + return ( + event.target instanceof HTMLElement && + (event.target?.nodeName === 'INPUT' || + event.target?.nodeName === 'TEXTAREA') + ); + } public destroy() {} } diff --git a/packages/ketcher-core/src/application/editor/modes/FlexMode.ts b/packages/ketcher-core/src/application/editor/modes/FlexMode.ts index 9b710374b3..219708589b 100644 --- a/packages/ketcher-core/src/application/editor/modes/FlexMode.ts +++ b/packages/ketcher-core/src/application/editor/modes/FlexMode.ts @@ -1,6 +1,8 @@ -import { CoreEditor } from 'application/editor/internal'; import { LayoutMode } from 'application/editor/modes/types'; import { BaseMode } from 'application/editor/modes/internal'; +import { CoreEditor } from '../Editor'; +import { Coordinates } from '../internal'; +import { Command } from 'domain/entities/Command'; export class FlexMode extends BaseMode { constructor(previousMode?: LayoutMode) { super('flex-layout-mode', previousMode); @@ -18,5 +20,18 @@ export class FlexMode extends BaseMode { return command; } - public onKeyDown(_event: KeyboardEvent) {} + getNewNodePosition() { + const editor = CoreEditor.provideEditorInstance(); + return Coordinates.canvasToModel(editor.lastCursorPositionOfCanvas); + } + + applyAdditionalPasteOperations() { + return new Command(); + } + + isPasteAllowedByMode(): boolean { + return true; + } + + scrollForView(): void {} } diff --git a/packages/ketcher-core/src/application/editor/modes/SequenceMode.ts b/packages/ketcher-core/src/application/editor/modes/SequenceMode.ts index ecaa4b015b..9baf411fb2 100644 --- a/packages/ketcher-core/src/application/editor/modes/SequenceMode.ts +++ b/packages/ketcher-core/src/application/editor/modes/SequenceMode.ts @@ -7,7 +7,6 @@ import { NodesSelection, SequenceRenderer, } from 'application/render/renderers/sequence/SequenceRenderer'; -import { initHotKeys, keyNorm } from 'utilities'; import { AttachmentPointName, MonomerItemType } from 'domain/types'; import { Command } from 'domain/entities/Command'; import { BaseMonomer, SequenceType, Vec2 } from 'domain/entities'; @@ -16,8 +15,8 @@ import { EmptySequenceNode } from 'domain/entities/EmptySequenceNode'; import { Nucleoside } from 'domain/entities/Nucleoside'; import { Nucleotide } from 'domain/entities/Nucleotide'; import { - ReinitializeSequenceModeCommand, - RestoreSequenceCaretPositionCommand, + ReinitializeModeOperation, + RestoreSequenceCaretPositionOperation, } from 'application/editor/operations/modes'; import assert from 'assert'; import { @@ -31,7 +30,10 @@ import { } from 'domain/constants/monomers'; import { SubChainNode } from 'domain/entities/monomer-chains/types'; import { isNumber, uniq } from 'lodash'; -import { DrawingEntity } from 'domain/entities/DrawingEntity'; +import { ChainsCollection } from 'domain/entities/monomer-chains/ChainsCollection'; +import { DrawingEntitiesManager } from 'domain/entities/DrawingEntitiesManager'; +import { Chain } from 'domain/entities/monomer-chains/Chain'; +import { MonomerSequenceNode } from 'domain/entities/MonomerSequenceNode'; import { LabeledNucleotideWithPositionInSequence } from 'application/editor/tools/Tool'; const naturalAnalogues = uniq([ @@ -134,24 +136,6 @@ export class SequenceMode extends BaseMode { editor.events.toggleSequenceEditInRNABuilderMode.dispatch(false); } - public async onKeyDown(event: KeyboardEvent) { - if (!this.isEditMode) { - return; - } - - await new Promise((resolve) => { - setTimeout(() => { - const hotKeys = initHotKeys(this.keyboardEventHandlers); - const shortcutKey = keyNorm.lookup(hotKeys, event); - const editor = CoreEditor.provideEditorInstance(); - - this.keyboardEventHandlers[shortcutKey]?.handler(event); - editor.events.mouseLeaveSequenceItem.dispatch(); - resolve(); - }, 0); - }); - } - public startNewSequence() { if (!this.isEditMode) { this.turnOnEditMode(); @@ -215,7 +199,7 @@ export class SequenceMode extends BaseMode { } // Refresh UI - modelChanges.addOperation(new ReinitializeSequenceModeCommand()); + modelChanges.addOperation(new ReinitializeModeOperation()); editor.renderersContainer.update(modelChanges); history.update(modelChanges); } @@ -255,7 +239,6 @@ export class SequenceMode extends BaseMode { const eventData = event.target?.__data__; const isEventOnSequenceItem = eventData instanceof BaseSequenceItemRenderer; if (this.isEditMode && isEventOnSequenceItem && this.selectionStarted) { - const modelChanges = new Command(); const editor = CoreEditor.provideEditorInstance(); SequenceRenderer.setCaretPositionBySequenceItemRenderer( eventData as BaseSequenceItemRenderer, @@ -271,19 +254,12 @@ export class SequenceMode extends BaseMode { startCaretPosition, endCaretPosition, ); - const drawingEntities = monomers.reduce( - (drawingEntities: DrawingEntity[], monomer: BaseMonomer) => { - return drawingEntities.concat( - editor.drawingEntitiesManager.getAllSelectedEntities(monomer), - ); - }, - [], - ); - - modelChanges.merge( - editor.drawingEntitiesManager.selectDrawingEntities(drawingEntities), - ); - const moveCaretOperation = new RestoreSequenceCaretPositionCommand( + this.unselectAllEntities(); + const { command: modelChanges } = + editor.drawingEntitiesManager.getAllSelectedEntitiesForEntities( + monomers, + ); + const moveCaretOperation = new RestoreSequenceCaretPositionOperation( this.selectionStartCaretPosition, SequenceRenderer.caretPosition, ); @@ -300,8 +276,8 @@ export class SequenceMode extends BaseMode { private bondNodesThroughNewPhosphate( position: Vec2, - previousNode: SubChainNode, - nextNode: SubChainNode, + previousMonomer: BaseMonomer, + nextMonomer: BaseMonomer, ) { const editor = CoreEditor.provideEditorInstance(); const phosphateLibraryItem = getRnaPartLibraryItem( @@ -321,7 +297,7 @@ export class SequenceMode extends BaseMode { modelChanges.merge( editor.drawingEntitiesManager.createPolymerBond( - previousNode.lastMonomerInNode, + previousMonomer, additionalPhosphate, AttachmentPointName.R2, AttachmentPointName.R1, @@ -331,7 +307,7 @@ export class SequenceMode extends BaseMode { modelChanges.merge( editor.drawingEntitiesManager.createPolymerBond( additionalPhosphate, - nextNode.firstMonomerInNode, + nextMonomer, AttachmentPointName.R2, AttachmentPointName.R1, ), @@ -342,8 +318,6 @@ export class SequenceMode extends BaseMode { private handlePeptideNodeAddition( enteredSymbol: string, - currentNode: SubChainNode, - previousNodeInSameChain: SubChainNode, newNodePosition: Vec2, ) { if (!peptideNaturalAnalogues.includes(enteredSymbol)) { @@ -362,39 +336,11 @@ export class SequenceMode extends BaseMode { ); const newPeptide = peptideAddCommand.operations[0].monomer as BaseMonomer; + const newPeptideNode = new MonomerSequenceNode(newPeptide); modelChanges.merge(peptideAddCommand); - if (!(currentNode instanceof EmptySequenceNode)) { - if (previousNodeInSameChain) { - const r2Bond = - previousNodeInSameChain?.lastMonomerInNode.attachmentPointsToBonds.R2; - assert(r2Bond); - modelChanges.merge( - editor.drawingEntitiesManager.deletePolymerBond(r2Bond), - ); - } - - modelChanges.merge( - editor.drawingEntitiesManager.createPolymerBond( - newPeptide, - currentNode?.firstMonomerInNode as BaseMonomer, - AttachmentPointName.R2, - AttachmentPointName.R1, - ), - ); - } - - if (previousNodeInSameChain) { - modelChanges.merge( - editor.drawingEntitiesManager.createPolymerBond( - previousNodeInSameChain.lastMonomerInNode, - newPeptide, - AttachmentPointName.R2, - AttachmentPointName.R1, - ), - ); - } + modelChanges.merge(this.insertNewSequenceFragment(newPeptideNode)); return modelChanges; } @@ -402,7 +348,6 @@ export class SequenceMode extends BaseMode { private handleRnaDnaNodeAddition( enteredSymbol: string, currentNode: SubChainNode, - previousNodeInSameChain: SubChainNode, newNodePosition: Vec2, ) { if (!rnaDnaNaturalAnalogues.includes(enteredSymbol)) { @@ -410,7 +355,6 @@ export class SequenceMode extends BaseMode { } const modelChanges = new Command(); - const editor = CoreEditor.provideEditorInstance(); const { modelChanges: addedNodeModelChanges, node: nodeToAdd } = currentNode instanceof Nucleotide || currentNode instanceof Nucleoside ? Nucleotide.createOnCanvas(enteredSymbol, newNodePosition) @@ -418,46 +362,76 @@ export class SequenceMode extends BaseMode { modelChanges.merge(addedNodeModelChanges); - if (!(currentNode instanceof EmptySequenceNode)) { - if (previousNodeInSameChain) { - const r2Bond = - previousNodeInSameChain?.lastMonomerInNode.attachmentPointsToBonds.R2; - assert(r2Bond); - modelChanges.merge( - editor.drawingEntitiesManager.deletePolymerBond(r2Bond), - ); - } + modelChanges.merge(this.insertNewSequenceFragment(nodeToAdd)); - modelChanges.merge( - editor.drawingEntitiesManager.createPolymerBond( - nodeToAdd.lastMonomerInNode, - currentNode?.firstMonomerInNode as BaseMonomer, - AttachmentPointName.R2, - AttachmentPointName.R1, - ), + return modelChanges; + } + + private connectNodes( + node: SubChainNode | undefined, + nextNode: SubChainNode | undefined, + modelChanges: Command, + newNodePosition: Vec2, + ) { + if ( + !node || + node instanceof EmptySequenceNode || + !nextNode || + nextNode instanceof EmptySequenceNode + ) { + return; + } + + const editor = CoreEditor.provideEditorInstance(); + const nodeR2Bond = node.lastMonomerInNode.attachmentPointsToBonds?.R2; + const nextNodeR1Bond = + nextNode?.firstMonomerInNode?.attachmentPointsToBonds.R1; + + if (nodeR2Bond || nextNodeR1Bond) { + editor.events.error.dispatch( + 'No available attachment points to establish bonds for merge.', ); + + return; } - if (previousNodeInSameChain instanceof Nucleoside) { + if ( + node instanceof Nucleoside && + (nextNode instanceof Nucleotide || nextNode instanceof Nucleoside) + ) { modelChanges.merge( this.bondNodesThroughNewPhosphate( newNodePosition, - previousNodeInSameChain, - nodeToAdd, + node.lastMonomerInNode, + nextNode.firstMonomerInNode, ), ); - } else if (previousNodeInSameChain) { + } else { modelChanges.merge( editor.drawingEntitiesManager.createPolymerBond( - previousNodeInSameChain.lastMonomerInNode, - nodeToAdd.firstMonomerInNode, + node.lastMonomerInNode, + nextNode.firstMonomerInNode, AttachmentPointName.R2, AttachmentPointName.R1, ), ); } + } - return modelChanges; + private deleteBondToNextNodeInChain( + node: SubChainNode | undefined, + modelChanges: Command, + ) { + const editor = CoreEditor.provideEditorInstance(); + const nodeR2Bond = node?.lastMonomerInNode.attachmentPointsToBonds.R2; + + if (!nodeR2Bond) { + return; + } + + modelChanges.merge( + editor.drawingEntitiesManager.deletePolymerBond(nodeR2Bond), + ); } private finishNodesDeletion( @@ -467,13 +441,13 @@ export class SequenceMode extends BaseMode { ) { const editor = CoreEditor.provideEditorInstance(); const history = new EditorHistory(editor); - const moveCaretOperation = new RestoreSequenceCaretPositionCommand( + const moveCaretOperation = new RestoreSequenceCaretPositionOperation( previousCaretPosition, isNumber(newCaretPosition) ? newCaretPosition : SequenceRenderer.caretPosition, ); - modelChanges.addOperation(new ReinitializeSequenceModeCommand()); + modelChanges.addOperation(new ReinitializeModeOperation()); editor.renderersContainer.update(modelChanges); modelChanges.addOperation(moveCaretOperation); history.update(modelChanges); @@ -495,7 +469,11 @@ export class SequenceMode extends BaseMode { const nodeInSameChainBeforeSelection = SequenceRenderer.getPreviousNodeInSameChain(selectionStartNode); - if (!nodeInSameChainBeforeSelection && nodeAfterSelection) { + if ( + !nodeInSameChainBeforeSelection && + nodeAfterSelection && + !(nodeAfterSelection instanceof EmptySequenceNode) + ) { modelChanges.merge( editor.drawingEntitiesManager.moveMonomer( nodeAfterSelection.monomer, @@ -541,8 +519,8 @@ export class SequenceMode extends BaseMode { modelChanges.merge( this.bondNodesThroughNewPhosphate( this.getNewSequenceItemPosition(nodeBeforeSelection), - nodeBeforeSelection, - nodeAfterSelection, + nodeBeforeSelection.lastMonomerInNode, + nodeAfterSelection.firstMonomerInNode, ), ); } else { @@ -562,11 +540,14 @@ export class SequenceMode extends BaseMode { return modelChanges; } - private get keyboardEventHandlers() { + get keyboardEventHandlers() { return { delete: { shortcut: ['Backspace', 'Delete'], handler: () => { + if (!this.isEditMode) { + return; + } const editor = CoreEditor.provideEditorInstance(); const previousNode = SequenceRenderer.previousNode; const previousCaretPosition = SequenceRenderer.caretPosition; @@ -628,12 +609,18 @@ export class SequenceMode extends BaseMode { 'move-caret-forward': { shortcut: ['ArrowRight'], handler: () => { + if (!this.isEditMode) { + return; + } SequenceRenderer.moveCaretForward(); }, }, 'move-caret-back': { shortcut: ['ArrowLeft'], handler: () => { + if (!this.isEditMode) { + return; + } SequenceRenderer.moveCaretBack(); }, }, @@ -645,64 +632,26 @@ export class SequenceMode extends BaseMode { ), ], handler: (event) => { - const selections = SequenceRenderer.selections; - - if (selections.length > 1) { + if (!this.isEditMode) { return; } - - if (selections.length === 1) { - const deletionModelChanges = this.deleteSelectedDrawingEntities(); - - deletionModelChanges.merge(this.handleNodesDeletion(selections)); - this.finishNodesDeletion( - deletionModelChanges, - SequenceRenderer.caretPosition, - selections[0][0].nodeIndexOverall, - ); + if (!this.deleteSelection()) { + return; } - const enteredSymbol = event.code.replace('Key', ''); const editor = CoreEditor.provideEditorInstance(); const history = new EditorHistory(editor); - const currentNode = SequenceRenderer.currentEdittingNode; - const previousNode = - SequenceRenderer.previousFromCurrentEdittingMonomer; - const nodeBeforePreviousNode = previousNode - ? SequenceRenderer.getPreviousNodeInSameChain(previousNode) - : undefined; - const previousNodeInSameChain = - SequenceRenderer.previousNodeInSameChain; - - const newNodePosition = this.getNewSequenceItemPosition( - previousNode, - nodeBeforePreviousNode, + const modelChanges = this.insertNewSequenceItem( + editor, + enteredSymbol, ); - let modelChanges; - - if (editor.sequenceTypeEnterMode === SequenceType.PEPTIDE) { - modelChanges = this.handlePeptideNodeAddition( - enteredSymbol, - currentNode, - previousNodeInSameChain, - newNodePosition, - ); - } else { - modelChanges = this.handleRnaDnaNodeAddition( - enteredSymbol, - currentNode, - previousNodeInSameChain, - newNodePosition, - ); - } - // Case when user type symbol that does not exist in current sequence type mode if (!modelChanges) { return; } - modelChanges.addOperation(new ReinitializeSequenceModeCommand()); + modelChanges.addOperation(new ReinitializeModeOperation()); editor.renderersContainer.update(modelChanges); modelChanges.addOperation(SequenceRenderer.moveCaretForward()); history.update(modelChanges); @@ -730,6 +679,155 @@ export class SequenceMode extends BaseMode { }; } + private deleteSelection() { + const selections = SequenceRenderer.selections; + + if (selections.length > 1) { + return false; + } + + if (selections.length === 1) { + const deletionModelChanges = this.deleteSelectedDrawingEntities(); + + deletionModelChanges.merge(this.handleNodesDeletion(selections)); + this.finishNodesDeletion( + deletionModelChanges, + SequenceRenderer.caretPosition, + selections[0][0].nodeIndexOverall, + ); + } + return true; + } + + isPasteAllowedByMode(drawingEntitiesManager: DrawingEntitiesManager) { + const editor = CoreEditor.provideEditorInstance(); + const chainsCollection = ChainsCollection.fromMonomers([ + ...drawingEntitiesManager.monomers.values(), + ]); + if (!this.isEditMode) { + return true; + } + + if (chainsCollection.chains.length > 1) { + editor.events.error.dispatch( + 'Paste of several fragments is prohibited in text-editing mode.', + ); + return false; + } + + if (chainsCollection.chains.length === 0) { + editor.events.error.dispatch('No copied fragments.'); + return false; + } + + if (!this.deleteSelection()) { + return false; + } + + return true; + } + + applyAdditionalPasteOperations( + drawingEntitiesManager: DrawingEntitiesManager, + ) { + if (!this.isEditMode) { + const command = new Command(); + + command.addOperation(new ReinitializeModeOperation()); + + return command; + } + + const chainsCollection = ChainsCollection.fromMonomers([ + ...drawingEntitiesManager.monomers.values(), + ]); + const modelChanges = this.insertNewSequenceFragment(chainsCollection); + + modelChanges.addOperation(new ReinitializeModeOperation()); + + modelChanges.addOperation( + new RestoreSequenceCaretPositionOperation( + SequenceRenderer.caretPosition, + SequenceRenderer.caretPosition + chainsCollection.length, + ), + ); + + return modelChanges; + } + + private insertNewSequenceItem(editor: CoreEditor, enteredSymbol: string) { + const currentNode = SequenceRenderer.currentEdittingNode; + const newNodePosition = this.getNewNodePosition(); + let modelChanges; + + if (editor.sequenceTypeEnterMode === SequenceType.PEPTIDE) { + modelChanges = this.handlePeptideNodeAddition( + enteredSymbol, + newNodePosition, + ); + } else { + modelChanges = this.handleRnaDnaNodeAddition( + enteredSymbol, + currentNode, + newNodePosition, + ); + } + return modelChanges; + } + + private insertNewSequenceFragment( + chainsCollectionOrNode: ChainsCollection | SubChainNode, + ) { + const chainsCollection = + chainsCollectionOrNode instanceof ChainsCollection + ? chainsCollectionOrNode + : new ChainsCollection().add( + new Chain().addNode(chainsCollectionOrNode), + ); + const currentNode = SequenceRenderer.currentEdittingNode; + const previousNodeInSameChain = SequenceRenderer.previousNodeInSameChain; + const modelChanges = new Command(); + const lastNodeOfNewFragment = chainsCollection.lastNode; + const firstNodeOfNewFragment = chainsCollection.firstNode; + const newNodePosition = this.getNewNodePosition(); + + this.deleteBondToNextNodeInChain(previousNodeInSameChain, modelChanges); + + this.connectNodes( + previousNodeInSameChain, + firstNodeOfNewFragment, + modelChanges, + newNodePosition, + ); + + this.connectNodes( + lastNodeOfNewFragment, + currentNode, + modelChanges, + newNodePosition, + ); + + return modelChanges; + } + + getNewNodePosition() { + if (this.isEditMode) { + const previousNode = SequenceRenderer.previousFromCurrentEdittingMonomer; + const nodeBeforePreviousNode = previousNode + ? SequenceRenderer.getPreviousNodeInSameChain(previousNode) + : undefined; + const newNodePosition = this.getNewSequenceItemPosition( + previousNode, + nodeBeforePreviousNode, + ); + return newNodePosition; + } else { + return SequenceRenderer.chainsCollection.chains.length > 0 + ? SequenceRenderer.getNextChainPosition() + : new Vec2(0, 0); + } + } + private deleteSelectedDrawingEntities() { const editor = CoreEditor.provideEditorInstance(); const modelChanges = new Command(); @@ -759,6 +857,30 @@ export class SequenceMode extends BaseMode { } } + scrollForView() { + if (this.isEditMode) { + return; + } + + const zoom = ZoomTool.instance; + const drawnEntitiesBoundingBox = + SequenceRenderer.getRenderedStructuresBbox(); + + if (zoom.isFitToCanvasHeight(drawnEntitiesBoundingBox.height)) { + zoom.scrollTo( + new Vec2(drawnEntitiesBoundingBox.left, drawnEntitiesBoundingBox.top), + ); + } else { + zoom.scrollTo( + new Vec2( + drawnEntitiesBoundingBox.left, + drawnEntitiesBoundingBox.bottom, + ), + true, + ); + } + } + private unselectAllEntities() { const editor = CoreEditor.provideEditorInstance(); const modelChanges = diff --git a/packages/ketcher-core/src/application/editor/modes/SnakeMode.ts b/packages/ketcher-core/src/application/editor/modes/SnakeMode.ts index 45f2f60055..e66cc4c4e3 100644 --- a/packages/ketcher-core/src/application/editor/modes/SnakeMode.ts +++ b/packages/ketcher-core/src/application/editor/modes/SnakeMode.ts @@ -1,6 +1,12 @@ import { BaseMode } from 'application/editor/modes/BaseMode'; -import { CoreEditor } from 'application/editor/internal'; import { LayoutMode } from 'application/editor/modes'; +import ZoomTool from '../tools/Zoom'; +import { CoreEditor } from '../Editor'; +import { Coordinates } from '../internal'; +import { Command } from 'domain/entities/Command'; +import { ReinitializeModeOperation } from 'application/editor/operations/modes'; +import { Vec2 } from 'domain/entities'; +import { RenderersManager } from 'application/render/renderers/RenderersManager'; export class SnakeMode extends BaseMode { constructor(previousMode?: LayoutMode) { @@ -23,5 +29,45 @@ export class SnakeMode extends BaseMode { return command; } - public onKeyDown(_event: KeyboardEvent) {} + getNewNodePosition() { + const editor = CoreEditor.provideEditorInstance(); + + return Coordinates.modelToCanvas( + editor.drawingEntitiesManager.bottomRightMonomerPosition, + ); + } + + scrollForView() { + const zoom = ZoomTool.instance; + const drawnEntitiesBoundingBox = + RenderersManager.getRenderedStructuresBbox(); + + if (zoom.isFitToCanvasHeight(drawnEntitiesBoundingBox.height)) { + zoom.scrollTo( + new Vec2(drawnEntitiesBoundingBox.left, drawnEntitiesBoundingBox.top), + false, + 2, + ); + } else { + zoom.scrollTo( + new Vec2( + drawnEntitiesBoundingBox.left, + drawnEntitiesBoundingBox.bottom, + ), + true, + 2, + ); + } + } + + applyAdditionalPasteOperations() { + const command = new Command(); + command.addOperation(new ReinitializeModeOperation()); + + return command; + } + + isPasteAllowedByMode(): boolean { + return true; + } } diff --git a/packages/ketcher-core/src/application/editor/operations/modes/index.ts b/packages/ketcher-core/src/application/editor/operations/modes/index.ts index 3287f25f6b..3783d895a6 100644 --- a/packages/ketcher-core/src/application/editor/operations/modes/index.ts +++ b/packages/ketcher-core/src/application/editor/operations/modes/index.ts @@ -19,30 +19,37 @@ import { RenderersManager } from 'application/render/renderers/RenderersManager'; import { Operation } from 'domain/entities/Operation'; import { CoreEditor } from 'application/editor/internal'; -import { SequenceMode } from 'application/editor/modes'; +import { SequenceMode, SnakeMode } from 'application/editor/modes'; import { SequencePointer, SequenceRenderer, } from 'application/render/renderers/sequence/SequenceRenderer'; -import assert from 'assert'; -export class ReinitializeSequenceModeCommand implements Operation { +export class ReinitializeModeOperation implements Operation { constructor() {} public execute(_renderersManager: RenderersManager) { const editor = CoreEditor.provideEditorInstance(); - assert(editor.mode instanceof SequenceMode); - editor.mode.initialize(false); + if ( + editor.mode instanceof SequenceMode || + editor.mode instanceof SnakeMode + ) { + editor.mode.initialize(false); + } } public invert(_renderersManager: RenderersManager) { const editor = CoreEditor.provideEditorInstance(); - assert(editor.mode instanceof SequenceMode); - editor.mode.initialize(false); + if ( + editor.mode instanceof SequenceMode || + editor.mode instanceof SnakeMode + ) { + editor.mode.initialize(false); + } } } -export class RestoreSequenceCaretPositionCommand implements Operation { +export class RestoreSequenceCaretPositionOperation implements Operation { constructor( private previousPosition: SequencePointer, private nextPosition: SequencePointer, diff --git a/packages/ketcher-core/src/application/editor/operations/polymerBond/index.ts b/packages/ketcher-core/src/application/editor/operations/polymerBond/index.ts index 62123800d9..378c59ab2a 100644 --- a/packages/ketcher-core/src/application/editor/operations/polymerBond/index.ts +++ b/packages/ketcher-core/src/application/editor/operations/polymerBond/index.ts @@ -48,7 +48,9 @@ export class PolymerBondDeleteOperation implements Operation { private finishPolymerBondCreationModelChange: ( polymerBond?: PolymerBond, ) => PolymerBond, - ) {} + ) { + this.deletePolymerBondChangeModel(); + } public execute(renderersManager: RenderersManager) { this.deletePolymerBondChangeModel(); diff --git a/packages/ketcher-core/src/application/editor/tools/Bond.ts b/packages/ketcher-core/src/application/editor/tools/Bond.ts index 93204954be..c7f6dd1062 100644 --- a/packages/ketcher-core/src/application/editor/tools/Bond.ts +++ b/packages/ketcher-core/src/application/editor/tools/Bond.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************/ -import { BaseMonomerRenderer } from 'application/render/renderers'; +import { BaseMonomerRenderer } from 'application/render/renderers/BaseMonomerRenderer'; import { CoreEditor, EditorHistory } from 'application/editor/internal'; import { PolymerBondRenderer } from 'application/render/renderers/PolymerBondRenderer'; import assert from 'assert'; diff --git a/packages/ketcher-core/src/application/editor/tools/Clear.ts b/packages/ketcher-core/src/application/editor/tools/Clear.ts index eef1a7b2bb..868258d762 100644 --- a/packages/ketcher-core/src/application/editor/tools/Clear.ts +++ b/packages/ketcher-core/src/application/editor/tools/Clear.ts @@ -16,7 +16,7 @@ import { CoreEditor, EditorHistory } from 'application/editor/internal'; import { SequenceMode } from 'application/editor/modes'; import { BaseTool } from 'application/editor/tools/Tool'; -import { ReinitializeSequenceModeCommand } from 'application/editor/operations/modes'; +import { ReinitializeModeOperation } from 'application/editor/operations/modes'; class ClearTool implements BaseTool { private history: EditorHistory; @@ -29,7 +29,7 @@ class ClearTool implements BaseTool { this.editor.renderersContainer.update(modelChanges); if (editor.mode instanceof SequenceMode) { - modelChanges.addOperation(new ReinitializeSequenceModeCommand()); + modelChanges.addOperation(new ReinitializeModeOperation()); } this.history.update(modelChanges); diff --git a/packages/ketcher-core/src/application/editor/tools/SelectRectangle.ts b/packages/ketcher-core/src/application/editor/tools/SelectRectangle.ts index a259e47ca6..764511730f 100644 --- a/packages/ketcher-core/src/application/editor/tools/SelectRectangle.ts +++ b/packages/ketcher-core/src/application/editor/tools/SelectRectangle.ts @@ -160,40 +160,40 @@ class SelectRectangle implements BaseTool { ) { return; } - const drawingEntities = - this.editor.drawingEntitiesManager.getAllSelectedEntities( - renderer.drawingEntity, - false, - ); modelChanges = - this.editor.drawingEntitiesManager.selectDrawingEntities( - drawingEntities, + this.editor.drawingEntitiesManager.unselectAllDrawingEntities(); + const { command: selectModelChanges } = + this.editor.drawingEntitiesManager.getAllSelectedEntitiesForSingleEntity( + renderer.drawingEntity, ); + modelChanges.merge(selectModelChanges); } else if (renderer instanceof BaseRenderer && event.shiftKey) { if (renderer.drawingEntity.selected) { return; } - const drawingEntities = - this.editor.drawingEntitiesManager.getAllSelectedEntities( - renderer.drawingEntity, - ); - modelChanges = - this.editor.drawingEntitiesManager.addDrawingEntitiesToSelection( + const drawingEntities: DrawingEntity[] = [ + ...this.editor.drawingEntitiesManager.selectedEntitiesArr, + renderer.drawingEntity, + ]; + ({ command: modelChanges } = + this.editor.drawingEntitiesManager.getAllSelectedEntitiesForEntities( drawingEntities, - ); + )); } else if (renderer instanceof BaseSequenceItemRenderer && ModKey) { let drawingEntities: DrawingEntity[] = renderer.currentSubChain.nodes .map((node) => { - if (node instanceof Nucleoside) { - return [node.sugar, node.rnaBase]; - } else if (node instanceof Nucleotide) { - return [node.sugar, node.rnaBase, node.phosphate]; + if (node instanceof Nucleoside || node instanceof Nucleotide) { + return node.monomers; } else { return node.monomer; } }) .flat(); - drawingEntities = drawingEntities.concat(renderer.currentSubChain.bonds); + drawingEntities.forEach((entity) => entity.turnOnSelection()); + const bondsInsideCurrentChain = renderer.currentSubChain.bonds.filter( + (bond) => bond.firstMonomer.selected && bond.secondMonomer?.selected, + ); + drawingEntities = drawingEntities.concat(bondsInsideCurrentChain); modelChanges = this.editor.drawingEntitiesManager.selectDrawingEntities( drawingEntities, diff --git a/packages/ketcher-core/src/application/editor/tools/Zoom.ts b/packages/ketcher-core/src/application/editor/tools/Zoom.ts index ed614f22c9..a82ced3840 100644 --- a/packages/ketcher-core/src/application/editor/tools/Zoom.ts +++ b/packages/ketcher-core/src/application/editor/tools/Zoom.ts @@ -22,6 +22,11 @@ import { DrawingEntitiesManager } from 'domain/entities/DrawingEntitiesManager'; import { clamp } from 'lodash'; import { notifyRenderComplete } from 'application/render/internal'; +export enum SCROLL_POSITION { + CENTER = 'CENTER', + BOTTOM = 'BOTTOM', +} + interface ScrollBar { name: string; offsetStart: number; @@ -219,24 +224,48 @@ class ZoomTool implements BaseTool { } }; - public scrollTo(position: Vec2) { + public scrollTo( + position: Vec2, + stickToBottom = false, + xOffset = AUTO_SCROLL_OFFSET_X, + ) { const canvasWrapperHeight = this.canvasWrapper.node()?.height.baseVal.value || 0; const canvasWrapperWidth = this.canvasWrapper.node()?.width.baseVal.value || 0; + const offset = new Vec2( + canvasWrapperWidth / 2 - (canvasWrapperWidth * xOffset) / 100, + canvasWrapperHeight / 2 - + (canvasWrapperHeight * AUTO_SCROLL_OFFSET_Y) / 100, + ); + this.zoom?.translateTo( this.canvasWrapper, - position.x + - canvasWrapperWidth / 2 - - (canvasWrapperWidth * AUTO_SCROLL_OFFSET_X) / 100, - position.y + - canvasWrapperHeight / 2 - - (canvasWrapperHeight * AUTO_SCROLL_OFFSET_Y) / 100, + position.x + this.unzoomValue(offset.x), + position.y + this.unzoomValue(offset.y * (stickToBottom ? -1 : 1)), ); } + public scrollToVerticalCenter(structCenterY: number) { + const centerPointOfModel = + this.drawingEntitiesManager.getCurrentCenterPointOfCanvas(); + const offsetY = centerPointOfModel.y - structCenterY; + this.zoom?.translateBy(this.canvasWrapper, 0, offsetY); + } + + public scrollToVerticalBottom() { + this.drawScrollBars(); + if (this.scrollBars.vertical.offsetEnd < 0) { + this.zoom?.translateBy( + this.canvasWrapper, + 0, + this.scrollBars.vertical.offsetEnd / this.zoomLevel, + ); + } + } + mouseWheeled(event) { const isShiftKeydown = event.shiftKey; const boxNode = this.canvasWrapper.node(); @@ -346,6 +375,26 @@ class ZoomTool implements BaseTool { this.zoom = null; this.zoomEventHandlers = []; } + + isFitToCanvasHeight(height) { + const canvasWrapperHeight = this.canvasWrapperHeight; + + return ( + height < + this.unzoomValue( + canvasWrapperHeight - + (canvasWrapperHeight * AUTO_SCROLL_OFFSET_Y) / 100, + ) + ); + } + + private get canvasWrapperHeight() { + // TODO create class for Canvas and move this getter there + const canvasWrapperBbox = this.canvasWrapper + .node() + ?.getBoundingClientRect(); + return canvasWrapperBbox?.height || 0; + } } export default ZoomTool; diff --git a/packages/ketcher-core/src/application/formatters/constants.ts b/packages/ketcher-core/src/application/formatters/constants.ts new file mode 100644 index 0000000000..0020225d38 --- /dev/null +++ b/packages/ketcher-core/src/application/formatters/constants.ts @@ -0,0 +1,14 @@ +export const macromoleculesFilesInputFormats = { + ket: 'chemical/x-indigo-ket', + mol: 'chemical/x-mdl-molfile', + seq: { + rna: 'chemical/x-rna-sequence', + dna: 'chemical/x-dna-sequence', + peptide: 'chemical/x-peptide-sequence', + }, + fasta: { + rna: 'chemical/x-rna-fasta', + dna: 'chemical/x-dna-fasta', + peptide: 'chemical/x-peptide-fasta', + }, +}; diff --git a/packages/ketcher-core/src/application/formatters/identifyStructFormat.ts b/packages/ketcher-core/src/application/formatters/identifyStructFormat.ts index c44c384dbc..8c8bc8c763 100644 --- a/packages/ketcher-core/src/application/formatters/identifyStructFormat.ts +++ b/packages/ketcher-core/src/application/formatters/identifyStructFormat.ts @@ -19,6 +19,7 @@ import { SupportedFormat } from './structFormatter.types'; export function identifyStructFormat( stringifiedStruct: string, + isMacromolecules = false, ): SupportedFormat { // Mimic Indigo/molecule_auto_loader.cpp as much as possible const sanitizedString = stringifiedStruct.trim(); @@ -93,7 +94,7 @@ export function identifyStructFormat( return SupportedFormat.inChI; } - if (sanitizedString.indexOf('\n') === -1) { + if (sanitizedString.indexOf('\n') === -1 && !isMacromolecules) { // TODO: smiles regexp return SupportedFormat.smiles; } @@ -102,5 +103,13 @@ export function identifyStructFormat( return SupportedFormat.cdxml; } + if (sanitizedString[0] === '>') { + return SupportedFormat.fasta; + } + + if (/^[a-zA-Z\s\n]*$/.test(sanitizedString)) { + return SupportedFormat.sequence; + } + return SupportedFormat.unknown; } diff --git a/packages/ketcher-core/src/application/formatters/index.ts b/packages/ketcher-core/src/application/formatters/index.ts index a16ef87631..f0e347dcc7 100644 --- a/packages/ketcher-core/src/application/formatters/index.ts +++ b/packages/ketcher-core/src/application/formatters/index.ts @@ -2,3 +2,4 @@ export * from './formatProperties'; export * from './structFormatter.types'; export * from './formatterFactory'; export * from './identifyStructFormat'; +export * from './constants'; diff --git a/packages/ketcher-core/src/application/formatters/types/ket.ts b/packages/ketcher-core/src/application/formatters/types/ket.ts index 195affb42a..fedf1ce5de 100644 --- a/packages/ketcher-core/src/application/formatters/types/ket.ts +++ b/packages/ketcher-core/src/application/formatters/types/ket.ts @@ -6,7 +6,6 @@ export interface IKetMonomerNode { x: number; y: number; }; - chemistryPosition?: { x: number; y: number }; alias?: string; templateId: string; } diff --git a/packages/ketcher-core/src/application/indigo.ts b/packages/ketcher-core/src/application/indigo.ts index ac36da96c3..ac1d5865bb 100644 --- a/packages/ketcher-core/src/application/indigo.ts +++ b/packages/ketcher-core/src/application/indigo.ts @@ -52,6 +52,7 @@ const defaultCalcProps: Array = [ type ConvertOptions = { outputFormat?: ChemicalMimeType; + inputFormat?: ChemicalMimeType; }; type AutomapOptions = { mode?: AutomapMode; @@ -103,10 +104,12 @@ export class Indigo { options?: ConvertOptions, ): Promise { const outputFormat = options?.outputFormat || ChemicalMimeType.KET; + const inputFormat = options?.inputFormat; return this.#structService.convert({ struct: convertStructToString(struct, this.#ketSerializer), output_format: outputFormat, + input_format: inputFormat, }); } diff --git a/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts b/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts index 962c928ee5..10cec9c9c5 100644 --- a/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts +++ b/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts @@ -21,6 +21,7 @@ import { getRnaBaseFromSugar, isMonomerBeginningOfChain, } from 'domain/helpers/monomers'; +import { CoreEditor } from 'application/editor'; export class RenderersManager { private theme; @@ -365,4 +366,31 @@ export class RenderersManager { this.recalculateMonomersBeginning(); } } + + public static getRenderedStructuresBbox() { + let left; + let right; + let top; + let bottom; + const editor = CoreEditor.provideEditorInstance(); + + editor.drawingEntitiesManager.monomers.forEach((monomer) => { + const monomerPosition = monomer.renderer?.scaledMonomerPosition; + + assert(monomerPosition); + + left = left ? Math.min(left, monomerPosition.x) : monomerPosition.x; + right = right ? Math.max(right, monomerPosition.x) : monomerPosition.x; + top = top ? Math.min(top, monomerPosition.y) : monomerPosition.y; + bottom = bottom ? Math.max(bottom, monomerPosition.y) : monomerPosition.y; + }); + return { + left, + right, + top, + bottom, + width: right - left, + height: bottom - top, + }; + } } diff --git a/packages/ketcher-core/src/application/render/renderers/sequence/SequenceRenderer.ts b/packages/ketcher-core/src/application/render/renderers/sequence/SequenceRenderer.ts index 8e1aa0579b..9255faa19a 100644 --- a/packages/ketcher-core/src/application/render/renderers/sequence/SequenceRenderer.ts +++ b/packages/ketcher-core/src/application/render/renderers/sequence/SequenceRenderer.ts @@ -17,7 +17,7 @@ import { EmptySubChain } from 'domain/entities/monomer-chains/EmptySubChain'; import { SubChainNode } from 'domain/entities/monomer-chains/types'; import { CoreEditor } from 'application/editor/internal'; import { SequenceMode } from 'application/editor/modes'; -import { RestoreSequenceCaretPositionCommand } from 'application/editor/operations/modes'; +import { RestoreSequenceCaretPositionOperation } from 'application/editor/operations/modes'; import assert from 'assert'; import { BaseSubChain } from 'domain/entities/monomer-chains/BaseSubChain'; import { BaseMonomerRenderer } from 'application/render'; @@ -35,8 +35,8 @@ export type NodesSelection = NodeSelection[][]; export class SequenceRenderer { public static caretPosition: SequencePointer = -1; public static chainsCollection: ChainsCollection; + public static lastChainStartPosition: Vec2; private static emptySequenceItemRenderers: EmptySequenceItemRenderer[] = []; - private static lastChainStartPosition: Vec2; public static show(chainsCollection: ChainsCollection) { SequenceRenderer.chainsCollection = chainsCollection; this.removeEmptyNodes(); @@ -96,7 +96,7 @@ export class SequenceRenderer { node.monomer.renderer, ); renderer.show(); - node.monomer?.setRenderer(renderer); + node.monomers?.forEach((monomer) => monomer.setRenderer(renderer)); currentMonomerIndexInChain++; currentMonomerIndexOverall++; @@ -120,9 +120,9 @@ export class SequenceRenderer { this.lastChainStartPosition = currentChainStartPosition; } - private static getNextChainPosition( - currentChainStartPosition: Vec2, - lastChain: Chain, + public static getNextChainPosition( + currentChainStartPosition: Vec2 = SequenceRenderer.lastChainStartPosition, + lastChain: Chain = SequenceRenderer.lastChain, ) { return new Vec2( currentChainStartPosition.x, @@ -149,11 +149,13 @@ export class SequenceRenderer { handledMonomersToAttachmentPoints.set(node.monomer, new Set()); } node.monomer.forEachBond((polymerBond, attachmentPointName) => { + if (!subChain.bonds.includes(polymerBond)) { + subChain.bonds.push(polymerBond); + } if (!polymerBond.isSideChainConnection) { polymerBond.setRenderer( new BackBoneBondSequenceRenderer(polymerBond), ); - subChain.bonds.push(polymerBond); return; } @@ -194,7 +196,6 @@ export class SequenceRenderer { } bondRenderer.show(); polymerBond.setRenderer(bondRenderer); - subChain.bonds.push(polymerBond); handledAttachmentPoints.add(attachmentPointName); if (!handledMonomersToAttachmentPoints.get(anotherMonomer)) { @@ -312,14 +313,14 @@ export class SequenceRenderer { } public static moveCaretForward() { - return new RestoreSequenceCaretPositionCommand( + return new RestoreSequenceCaretPositionOperation( this.caretPosition, this.nextCaretPosition || this.caretPosition, ); } public static moveCaretBack() { - return new RestoreSequenceCaretPositionCommand( + return new RestoreSequenceCaretPositionOperation( this.caretPosition, this.previousCaretPosition === undefined ? this.caretPosition @@ -570,38 +571,55 @@ export class SequenceRenderer { public static shiftArrowSelectionInEditMode(event) { const editor = CoreEditor.provideEditorInstance(); - const selectDrawingEntities = (selectedNode: SubChainNode) => { - const drawingEntities = - editor.drawingEntitiesManager.getAllSelectedEntities( - selectedNode.monomer, - ); - const modelChanges = - editor.drawingEntitiesManager.addDrawingEntitiesToSelection( - drawingEntities, - ); - return modelChanges; - }; - - const modelChanges = new Command(); + let modelChanges; const arrowKey = event.code; if (arrowKey === 'ArrowRight') { - const modelChanges = selectDrawingEntities(this.currentEdittingNode); + modelChanges = SequenceRenderer.getShiftArrowChanges( + editor, + this.currentEdittingNode.monomer, + ); modelChanges.addOperation(this.moveCaretForward()); } else if (arrowKey === 'ArrowLeft') { - let modelChanges; if (this.previousNodeInSameChain) { - modelChanges = selectDrawingEntities(this.previousNodeInSameChain); + modelChanges = SequenceRenderer.getShiftArrowChanges( + editor, + this.previousNodeInSameChain.monomer, + ); } else if (SequenceRenderer.previousChain) { const previousChainLastEmptyNode = SequenceRenderer.getLastNode( SequenceRenderer.previousChain, ); - modelChanges = selectDrawingEntities(previousChainLastEmptyNode); + ({ command: modelChanges } = + editor.drawingEntitiesManager.getAllSelectedEntitiesForSingleEntity( + previousChainLastEmptyNode.monomer, + )); } modelChanges.addOperation(this.moveCaretBack()); } editor.renderersContainer.update(modelChanges); } + private static getShiftArrowChanges( + editor: CoreEditor, + monomer: BaseMonomer, + ) { + let modelChanges; + const needTurnOffSelection = monomer.selected; + const result = + editor.drawingEntitiesManager.getAllSelectedEntitiesForSingleEntity( + monomer, + ); + if (needTurnOffSelection) { + modelChanges = + editor.drawingEntitiesManager.addDrawingEntitiesToSelection( + result.drawingEntities, + ); + } else { + modelChanges = result.command; + } + return modelChanges; + } + public static unselectEmptySequenceNodes() { const command = new Command(); const editor = CoreEditor.provideEditorInstance(); @@ -635,4 +653,28 @@ export class SequenceRenderer { return selections; } + + public static getRenderedStructuresBbox() { + let left; + let right; + let top; + let bottom; + SequenceRenderer.forEachNode(({ node }) => { + assert(node.monomer.renderer instanceof BaseSequenceItemRenderer); + const nodePosition = + node.monomer.renderer?.scaledMonomerPositionForSequence; + left = left ? Math.min(left, nodePosition.x) : nodePosition.x; + right = right ? Math.max(right, nodePosition.x) : nodePosition.x; + top = top ? Math.min(top, nodePosition.y) : nodePosition.y; + bottom = bottom ? Math.max(bottom, nodePosition.y) : nodePosition.y; + }); + return { + left, + right, + top, + bottom, + width: right - left, + height: bottom - top, + }; + } } diff --git a/packages/ketcher-core/src/application/utils.ts b/packages/ketcher-core/src/application/utils.ts index 35addd2c48..14e60d1804 100644 --- a/packages/ketcher-core/src/application/utils.ts +++ b/packages/ketcher-core/src/application/utils.ts @@ -1,4 +1,4 @@ -import { Struct, Vec2 } from 'domain/entities'; +import { Struct } from 'domain/entities'; import { DrawingEntitiesManager } from 'domain/entities/DrawingEntitiesManager'; import { FormatterFactory, @@ -7,7 +7,7 @@ import { } from './formatters'; import { Ketcher } from './ketcher'; import { ChemicalMimeType, StructService } from 'domain/services'; -import { Coordinates, CoreEditor, EditorHistory } from './editor/internal'; +import { CoreEditor, EditorHistory } from './editor/internal'; import { KetSerializer } from 'domain/serializers'; import assert from 'assert'; import { EditorSelection } from './editor/editor.types'; @@ -103,19 +103,11 @@ export async function parseAndAddMacromoleculesOnCanvas( const deserialisedKet = ketSerializer.deserializeToDrawingEntities(ketStruct); assert(deserialisedKet); - const modelChanges = deserialisedKet.drawingEntitiesManager.mergeInto( - editor.drawingEntitiesManager, - ); + const { command: modelChanges } = + deserialisedKet.drawingEntitiesManager.mergeInto( + editor.drawingEntitiesManager, + ); new EditorHistory(editor).update(modelChanges); editor.renderersContainer.update(modelChanges); } - -export function getCurrentCenterPointOfCanvas() { - const editor = CoreEditor.provideEditorInstance(); - const originalCenterPointOfCanvas = new Vec2( - editor.canvasOffset.width / 2, - editor.canvasOffset.height / 2, - ); - return Coordinates.viewToCanvas(originalCenterPointOfCanvas); -} diff --git a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts index 016117f084..6f4612e322 100644 --- a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts +++ b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts @@ -42,7 +42,6 @@ import { monomerFactory, } from 'application/editor/operations/monomer/monomerFactory'; import { Coordinates, CoreEditor } from 'application/editor/internal'; -import { getCurrentCenterPointOfCanvas } from 'application/utils'; import { getNextMonomerInChain, getRnaBaseFromSugar, @@ -54,7 +53,7 @@ import { ChainsCollection } from 'domain/entities/monomer-chains/ChainsCollectio import { SequenceRenderer } from 'application/render/renderers/sequence/SequenceRenderer'; import { Nucleoside } from './Nucleoside'; import { Nucleotide } from './Nucleotide'; -import { SequenceMode } from 'application/editor/modes/SequenceMode'; +import { SequenceMode } from 'application/editor'; const HORIZONTAL_DISTANCE_FROM_MONOMER = 25; const VERTICAL_DISTANCE_FROM_MONOMER = 30; @@ -83,6 +82,31 @@ export class DrawingEntitiesManager { public monomers: Map = new Map(); public polymerBonds: Map = new Map(); public micromoleculesHiddenEntities: Struct = new Struct(); + public get bottomRightMonomerPosition(): Vec2 { + let position: Vec2 | null = null; + + this.monomers.forEach((monomer) => { + if ( + !position || + monomer.position.x + monomer.position.y > position.x + position.y + ) { + position = monomer.position; + } + }); + + return position || new Vec2(0, 0, 0); + } + + get selectedEntitiesArr() { + const selectedEntities: DrawingEntity[] = []; + this.allEntities.forEach(([, drawingEntity]) => { + if (drawingEntity.selected) { + selectedEntities.push(drawingEntity); + } + }); + return selectedEntities; + } + get selectedEntities() { return this.allEntities.filter( ([, drawingEntity]) => drawingEntity.selected, @@ -399,13 +423,24 @@ export class DrawingEntitiesManager { const isPreviousSelected = previousSelectedEntities.find( ([, entity]) => entity === drawingEntity, ); - const isValueChanged = drawingEntity.selectIfLocatedInRectangle( - rectangleTopLeftPoint, - rectangleBottomRightPoint, - !!isPreviousSelected, - shiftKey, - ); - + let isValueChanged; + const editor = CoreEditor.provideEditorInstance(); + if ( + editor.mode instanceof SequenceMode && + drawingEntity instanceof PolymerBond + ) { + isValueChanged = this.checkBondSelectionForSequenceMode( + drawingEntity, + isValueChanged, + ); + } else { + isValueChanged = drawingEntity.selectIfLocatedInRectangle( + rectangleTopLeftPoint, + rectangleBottomRightPoint, + !!isPreviousSelected, + shiftKey, + ); + } if (isValueChanged) { const selectionCommand = this.createDrawingEntitySelectionCommand(drawingEntity); @@ -416,6 +451,20 @@ export class DrawingEntitiesManager { return command; } + private checkBondSelectionForSequenceMode( + bond: PolymerBond, + isValueChanged: boolean, + ) { + const prevSelectedValue = bond.selected; + if (bond.firstMonomer.selected && bond.secondMonomer?.selected) { + bond.turnOnSelection(); + } else { + bond.turnOffSelection(); + } + isValueChanged = prevSelectedValue !== bond.selected; + return isValueChanged; + } + public startPolymerBondCreationChangeModel( firstMonomer, startPosition, @@ -459,6 +508,10 @@ export class DrawingEntitiesManager { } public deletePolymerBondChangeModel(polymerBond: PolymerBond) { + if (this.polymerBonds.get(polymerBond.id) !== polymerBond) { + return; + } + this.polymerBonds.delete(polymerBond.id); const firstMonomerAttachmentPoint = @@ -1477,16 +1530,24 @@ export class DrawingEntitiesManager { public mergeInto(targetDrawingEntitiesManager: DrawingEntitiesManager) { const command = new Command(); const monomerToNewMonomer = new Map(); + const mergedDrawingEntities = new DrawingEntitiesManager(); this.monomers.forEach((monomer) => { const monomerAddCommand = targetDrawingEntitiesManager.addMonomer( monomer.monomerItem, monomer.position, ); + + command.merge(monomerAddCommand); + + const addedMonomer = monomerAddCommand.operations[0] + .monomer as BaseMonomer; + + mergedDrawingEntities.monomers.set(addedMonomer.id, addedMonomer); + monomerToNewMonomer.set( monomer, monomerAddCommand.operations[0].monomer as BaseMonomer, ); - command.merge(monomerAddCommand); }); this.polymerBonds.forEach((polymerBond) => { assert(polymerBond.secondMonomer); @@ -1502,17 +1563,25 @@ export class DrawingEntitiesManager { ) as AttachmentPointName, ); command.merge(polymerBondCreateCommand); + + const addedPolymerBond = polymerBondCreateCommand.operations[0] + .polymerBond as PolymerBond; + + mergedDrawingEntities.polymerBonds.set( + addedPolymerBond.id, + addedPolymerBond, + ); }); this.micromoleculesHiddenEntities.mergeInto( targetDrawingEntitiesManager.micromoleculesHiddenEntities, ); - return command; + return { command, mergedDrawingEntities }; } public centerMacroStructure() { const centerPointOfModel = Coordinates.canvasToModel( - getCurrentCenterPointOfCanvas(), + this.getCurrentCenterPointOfCanvas(), ); const structCenter = this.getMacroStructureCenter(); const offset = Vec2.diff(centerPointOfModel, structCenter); @@ -1527,6 +1596,15 @@ export class DrawingEntitiesManager { }); } + public getCurrentCenterPointOfCanvas() { + const editor = CoreEditor.provideEditorInstance(); + const originalCenterPointOfCanvas = new Vec2( + editor.canvasOffset.width / 2, + editor.canvasOffset.height / 2, + ); + return Coordinates.viewToCanvas(originalCenterPointOfCanvas); + } + public getMacroStructureCenter() { let xmin = 1e50; let ymin = xmin; @@ -1596,53 +1674,81 @@ export class DrawingEntitiesManager { return command; } - public getAllSelectedEntities( + public getAllSelectedEntitiesForEntities(drawingEntities: DrawingEntity[]) { + const command = new Command(); + const editor = CoreEditor.provideEditorInstance(); + drawingEntities.forEach((monomer) => monomer.turnOnSelection()); + const newDrawingEntities = drawingEntities.reduce( + ( + selectedDrawingEntities: DrawingEntity[], + drawingEntity: DrawingEntity, + ) => { + const res = + editor.drawingEntitiesManager.getAllSelectedEntitiesForSingleEntity( + drawingEntity, + true, + selectedDrawingEntities, + ); + res.drawingEntities.forEach((entity) => + command.addOperation(new DrawingEntitySelectOperation(entity)), + ); + return selectedDrawingEntities.concat(res.drawingEntities); + }, + [], + ); + return { command, drawingEntities: newDrawingEntities }; + } + + public getAllSelectedEntitiesForSingleEntity( drawingEntity: DrawingEntity, needToSelectConnectedBonds = true, + selectedDrawingEntities?: DrawingEntity[], ) { + const command = new Command(); + command.addOperation(new DrawingEntitySelectOperation(drawingEntity)); + drawingEntity.turnOnSelection(); + let drawingEntities: DrawingEntity[] = [drawingEntity]; + const editor = CoreEditor.provideEditorInstance(); if ( !(editor.mode instanceof SequenceMode) || drawingEntity instanceof PolymerBond ) { - return [drawingEntity]; + return { command, drawingEntities }; } - const drawingEntities: DrawingEntity[] = [drawingEntity]; if (drawingEntity.isPartOfRna && drawingEntity instanceof Sugar) { const sugar = drawingEntity; if (isValidNucleoside(sugar)) { const nucleoside = Nucleoside.fromSugar(sugar); - drawingEntities.push(nucleoside.rnaBase); - if (needToSelectConnectedBonds && nucleoside.rnaBase.hasBonds) { - nucleoside.rnaBase.forEachBond((polymerBond) => { - drawingEntities.push(polymerBond); - }); - } + drawingEntities = nucleoside.monomers; } else if (isValidNucleotide(sugar)) { const nucleotide = Nucleotide.fromSugar(sugar); - drawingEntities.push(nucleotide.rnaBase); - drawingEntities.push(nucleotide.phosphate); - if (needToSelectConnectedBonds && nucleotide.rnaBase.hasBonds) { - nucleotide.rnaBase.forEachBond((polymerBond) => { - drawingEntities.push(polymerBond); - }); - } - if (needToSelectConnectedBonds && nucleotide.phosphate.hasBonds) { - nucleotide.phosphate.forEachBond((polymerBond) => { - drawingEntities.push(polymerBond); - }); - } + drawingEntities = nucleotide.monomers; } - } - const monomer = drawingEntity as BaseMonomer; - if (needToSelectConnectedBonds && monomer.hasBonds) { - monomer.forEachBond((polymerBond) => { - if (!drawingEntities.includes(polymerBond)) { - drawingEntities.push(polymerBond); + drawingEntities.forEach((entity) => { + if (!(entity instanceof Sugar)) { + entity.turnOnSelection(); + command.addOperation(new DrawingEntitySelectOperation(entity)); } }); } - return drawingEntities; + drawingEntities.forEach((entity) => { + const monomer = entity as BaseMonomer; + if (needToSelectConnectedBonds && monomer.hasBonds) { + monomer.forEachBond((polymerBond) => { + if ( + !selectedDrawingEntities?.includes(polymerBond) && + !drawingEntities.includes(polymerBond) && + polymerBond.getAnotherMonomer(monomer)?.selected + ) { + drawingEntities.push(polymerBond); + polymerBond.turnOnSelection(); + command.addOperation(new DrawingEntitySelectOperation(polymerBond)); + } + }); + } + }); + return { command, drawingEntities }; } public validateIfApplicableForFasta() { diff --git a/packages/ketcher-core/src/domain/entities/monomer-chains/Chain.ts b/packages/ketcher-core/src/domain/entities/monomer-chains/Chain.ts index af6f4c5770..6218f72459 100644 --- a/packages/ketcher-core/src/domain/entities/monomer-chains/Chain.ts +++ b/packages/ketcher-core/src/domain/entities/monomer-chains/Chain.ts @@ -1,5 +1,11 @@ import { BaseSubChain } from 'domain/entities/monomer-chains/BaseSubChain'; -import { BaseMonomer, Peptide, Phosphate, Sugar } from 'domain/entities'; +import { + BaseMonomer, + Peptide, + Phosphate, + SubChainNode, + Sugar, +} from 'domain/entities'; import { getNextMonomerInChain, isValidNucleoside, @@ -20,7 +26,7 @@ export class Chain { } } - public add(monomer: BaseMonomer) { + private createSubChainIfNeed(monomer) { const needCreateNewSubchain = !this.lastNode?.monomer || monomer.isMonomerTypeDifferentForChaining(this.lastNode.monomer); @@ -28,6 +34,10 @@ export class Chain { if (needCreateNewSubchain) { this.subChains.push(new monomer.SubChainConstructor()); } + } + + public add(monomer: BaseMonomer) { + this.createSubChainIfNeed(monomer); const nextMonomer = getNextMonomerInChain(monomer); @@ -49,6 +59,14 @@ export class Chain { } } + public addNode(node: SubChainNode) { + this.createSubChainIfNeed(node.monomer); + + this.lastSubChain.add(node); + + return this; + } + private fillSubChains(monomer?: BaseMonomer) { if (!monomer) return; diff --git a/packages/ketcher-core/src/domain/entities/monomer-chains/ChainsCollection.ts b/packages/ketcher-core/src/domain/entities/monomer-chains/ChainsCollection.ts index 49617664b6..9a6dd2fc92 100644 --- a/packages/ketcher-core/src/domain/entities/monomer-chains/ChainsCollection.ts +++ b/packages/ketcher-core/src/domain/entities/monomer-chains/ChainsCollection.ts @@ -29,6 +29,8 @@ export class ChainsCollection { public add(chain: Chain) { this.chains.push(chain); + + return this; } public static fromMonomers(monomers: BaseMonomer[]) { @@ -86,4 +88,12 @@ export class ChainsCollection { public get firstNode() { return this.chains[0]?.subChains[0]?.nodes[0]; } + + public get lastNode() { + return this.chains[0].lastSubChain.lastNode; + } + + public get length() { + return this.chains.reduce((length, chain) => length + chain.length, 0); + } } diff --git a/packages/ketcher-core/src/domain/services/struct/structService.types.ts b/packages/ketcher-core/src/domain/services/struct/structService.types.ts index bcf58a8f14..4e652d2da5 100644 --- a/packages/ketcher-core/src/domain/services/struct/structService.types.ts +++ b/packages/ketcher-core/src/domain/services/struct/structService.types.ts @@ -31,6 +31,9 @@ export enum ChemicalMimeType { SDF = 'chemical/x-sdf', FASTA = 'chemical/x-fasta', SEQUENCE = 'chemical/x-sequence', + RNA = 'chemical/x-rna-sequence', + DNA = 'chemical/x-dna-sequence', + PEPTIDE = 'chemical/x-peptide-sequence', } export interface WithStruct { diff --git a/packages/ketcher-core/src/utilities/clipboardUtils.ts b/packages/ketcher-core/src/utilities/clipboardUtils.ts new file mode 100644 index 0000000000..f8f6f76df6 --- /dev/null +++ b/packages/ketcher-core/src/utilities/clipboardUtils.ts @@ -0,0 +1,77 @@ +import { ChemicalMimeType } from 'domain/services'; + +/** + * + * Legacy browser API doesn't support async operations, so it is not possible + * to call indigo, when copy/cut/paste + */ +export function isClipboardAPIAvailable() { + return ( + typeof navigator?.clipboard?.writeText === 'function' && + typeof navigator?.clipboard?.read === 'function' + ); +} + +export function legacyCopy(clipboardData, data) { + let curFmt; + clipboardData.setData('text/plain', data['text/plain']); + try { + Object.keys(data).forEach((fmt) => { + curFmt = fmt; + clipboardData.setData(fmt, data[fmt]); + }); + } catch (e) { + console.error('clipboardUtils.ts::legacyCopy', e); + console.info(`Could not write exact type ${curFmt}`); + } +} + +export function legacyPaste(cb, formats) { + let data = {}; + data['text/plain'] = cb.getData('text/plain'); + data = formats.reduce((res, fmt) => { + const d = cb.getData(fmt); + if (d) res[fmt] = d; + return res; + }, data); + return data; +} + +export function notifyCopyCut() { + const event = new Event('copyOrCutComplete'); + window.dispatchEvent(event); +} + +export async function getStructStringFromClipboardData( + data: ClipboardItem[], +): Promise { + const clipboardItem = data[0]; + + if (clipboardItem && clipboardItem instanceof ClipboardItem) { + const structStr = + (await safelyGetMimeType(clipboardItem, `web ${ChemicalMimeType.KET}`)) || + (await safelyGetMimeType(clipboardItem, `web ${ChemicalMimeType.Mol}`)) || + (await safelyGetMimeType(clipboardItem, `web ${ChemicalMimeType.Rxn}`)) || + (await safelyGetMimeType(clipboardItem, 'text/plain')); + return structStr === '' ? '' : structStr.text(); + } else { + return ( + data[ChemicalMimeType.KET] || + data[ChemicalMimeType.Mol] || + data[ChemicalMimeType.Rxn] || + data['text/plain'] + ); + } +} + +export async function safelyGetMimeType( + clipboardItem: ClipboardItem, + mimeType: string, +) { + try { + const result = await clipboardItem.getType(mimeType); + return result; + } catch { + return ''; + } +} diff --git a/packages/ketcher-core/src/utilities/index.ts b/packages/ketcher-core/src/utilities/index.ts index 8d25afcead..48d8de383d 100644 --- a/packages/ketcher-core/src/utilities/index.ts +++ b/packages/ketcher-core/src/utilities/index.ts @@ -23,3 +23,4 @@ export * from './KetcherLogger'; export * from './SettingsManager'; export * from './keynorm'; export * from './shortcutsUtil'; +export * from './clipboardUtils'; diff --git a/packages/ketcher-macromolecules/src/Editor.tsx b/packages/ketcher-macromolecules/src/Editor.tsx index c6a0ad83ef..17c13408ce 100644 --- a/packages/ketcher-macromolecules/src/Editor.tsx +++ b/packages/ketcher-macromolecules/src/Editor.tsx @@ -62,6 +62,7 @@ import { } from 'hooks'; import { closeErrorTooltip, + openErrorModal, openErrorTooltip, openModal, selectErrorTooltipText, @@ -221,6 +222,12 @@ function Editor({ theme, togglerComponent }: EditorProps) { editor.events.error.add((errorText) => { dispatch(openErrorTooltip(errorText)); }); + editor.events.openErrorModal.add( + (errorData: string | { errorMessage: string; errorTitle: string }) => { + dispatch(openErrorModal(errorData)); + }, + ); + dispatch(selectTool('select-rectangle')); editor.events.selectTool.dispatch('select-rectangle'); editor.events.openMonomerConnectionModal.add( diff --git a/packages/ketcher-macromolecules/src/components/SequenceTypeButton/SequenceTypeDropdown.tsx b/packages/ketcher-macromolecules/src/components/SequenceTypeButton/SequenceTypeDropdown.tsx index 9412c1466a..03fed42161 100644 --- a/packages/ketcher-macromolecules/src/components/SequenceTypeButton/SequenceTypeDropdown.tsx +++ b/packages/ketcher-macromolecules/src/components/SequenceTypeButton/SequenceTypeDropdown.tsx @@ -15,7 +15,7 @@ ***************************************************************************/ import { useEffect, useState } from 'react'; -import { useAppSelector } from 'hooks'; +import { useAppSelector, useLayoutMode } from 'hooks'; import { selectEditor } from 'state/common'; import { SequenceType } from 'ketcher-core'; import { StyledDropdown } from 'components/modal/save/Save.styles'; @@ -36,8 +36,9 @@ export const SequenceTypeDropdown = () => { const [activeSequenceType, setActiveSequenceType] = useState( SequenceType.RNA, ); - const [isSequenceEditMode, setIsSequenceEditMode] = useState(false); + const [isSequenceMode, setIsSequenceMode] = useState(false); const editor = useAppSelector(selectEditor); + const layoutMode = useLayoutMode(); const dropdownOptions = [ { id: SequenceType.RNA, label: 'RNA' }, @@ -45,26 +46,31 @@ export const SequenceTypeDropdown = () => { { id: SequenceType.PEPTIDE, label: 'Peptide' }, ]; - const onToggleSequenceEditMode = (_isSequenceEditMode) => { - setIsSequenceEditMode(_isSequenceEditMode); + const onToggleSequenceMode = (data) => { + const mode = typeof data === 'object' ? data.mode : data; + setIsSequenceMode(mode === 'sequence-layout-mode'); }; useEffect(() => { - editor?.events.toggleSequenceEditMode.add(onToggleSequenceEditMode); + editor?.events.selectMode.add(onToggleSequenceMode); return () => { - editor?.events.toggleSequenceEditMode.remove(onToggleSequenceEditMode); + editor?.events.selectMode.remove(onToggleSequenceMode); }; }, [editor]); + useEffect(() => { + onToggleSequenceMode(layoutMode); + }, [layoutMode]); + const onSelectSequenceType = (sequenceType: string) => { setActiveSequenceType(sequenceType as SequenceType); editor.events.changeSequenceTypeEnterMode.dispatch(sequenceType); }; - return isSequenceEditMode ? ( + return isSequenceMode ? ( <> - Edit mode + Type = [ { id: 'peptide', label: 'Peptide' }, ]; -const inputFormats = { - ket: 'chemical/x-indigo-ket', - mol: 'chemical/x-mdl-molfile', - seq: { - rna: 'chemical/x-rna-sequence', - dna: 'chemical/x-dna-sequence', - peptide: 'chemical/x-peptide-sequence', - }, - fasta: { - rna: 'chemical/x-rna-fasta', - dna: 'chemical/x-dna-fasta', - peptide: 'chemical/x-peptide-fasta', - }, -}; +const inputFormats = macromoleculesFilesInputFormats; export const MODAL_STATES = { openOptions: 'openOptions', @@ -141,9 +129,10 @@ const addToCanvas = ({ } deserialisedKet.drawingEntitiesManager.centerMacroStructure(); - const modelChanges = deserialisedKet.drawingEntitiesManager.mergeInto( - editor.drawingEntitiesManager, - ); + const { command: modelChanges } = + deserialisedKet.drawingEntitiesManager.mergeInto( + editor.drawingEntitiesManager, + ); const editorHistory = new EditorHistory(editor); const isSequenceMode = editor.mode instanceof SequenceMode; diff --git a/packages/ketcher-react/src/Editor.tsx b/packages/ketcher-react/src/Editor.tsx index 1691781abd..67d574f05f 100644 --- a/packages/ketcher-react/src/Editor.tsx +++ b/packages/ketcher-react/src/Editor.tsx @@ -57,8 +57,8 @@ function Editor(props: EditorProps) { ...props, element: rootElRef.current, appRoot, - }).then((ketcher: Ketcher) => { - if (typeof onInit === 'function') { + }).then((ketcher: Ketcher | undefined) => { + if (typeof onInit === 'function' && ketcher) { onInit(ketcher); window.dispatchEvent(ketcherInitEvent); } diff --git a/packages/ketcher-react/src/script/builders/ketcher/KetcherBuilder.ts b/packages/ketcher-react/src/script/builders/ketcher/KetcherBuilder.ts index 6399f41b47..20c730a703 100644 --- a/packages/ketcher-react/src/script/builders/ketcher/KetcherBuilder.ts +++ b/packages/ketcher-react/src/script/builders/ketcher/KetcherBuilder.ts @@ -114,6 +114,14 @@ class KetcherBuilder { ); ketcher[this.serviceMode] = true; + const userInput = document.location.search; + if ( + userInput === '__proto__' || + userInput === 'constructor' || + userInput === 'prototype' + ) { + return; + } const params = new URLSearchParams(document.location.search); const initialMol = params.get('moll'); if (initialMol) { diff --git a/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx b/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx index 14426caaac..8d27b5ab5a 100644 --- a/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx +++ b/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx @@ -21,8 +21,9 @@ import { KetcherLogger, notifyRequestCompleted, isControlKey, + isClipboardAPIAvailable, + notifyCopyCut, } from 'ketcher-core'; -import { isClipboardAPIAvailable, notifyCopyCut } from './clipboardUtils'; const ieCb = window.clipboardData; diff --git a/packages/ketcher-react/src/script/ui/component/cliparea/clipboardUtils.js b/packages/ketcher-react/src/script/ui/component/cliparea/clipboardUtils.js deleted file mode 100644 index 63d6e7449d..0000000000 --- a/packages/ketcher-react/src/script/ui/component/cliparea/clipboardUtils.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * - * Legacy browser API doesn't support async operations, so it is not possible - * to call indigo, when copy/cut/paste - */ -export function isClipboardAPIAvailable() { - return ( - typeof navigator?.clipboard?.writeText === 'function' && - typeof navigator?.clipboard?.read === 'function' - ); -} - -export function notifyCopyCut() { - const event = new Event('copyOrCutComplete'); - window.dispatchEvent(event); -} diff --git a/packages/ketcher-react/src/script/ui/state/hotkeys.ts b/packages/ketcher-react/src/script/ui/state/hotkeys.ts index 1ee6e1cd71..0422d9c7a8 100644 --- a/packages/ketcher-react/src/script/ui/state/hotkeys.ts +++ b/packages/ketcher-react/src/script/ui/state/hotkeys.ts @@ -30,6 +30,7 @@ import { SettingsManager, keyNorm, initHotKeys, + getStructStringFromClipboardData, } from 'ketcher-core'; import { debounce, isEqual } from 'lodash/fp'; import { load, onAction, removeStructAction } from './shared'; @@ -339,40 +340,6 @@ export function initClipboard(dispatch) { }; } -async function safelyGetMimeType( - clipboardItem: ClipboardItem, - mimeType: string, -) { - try { - const result = await clipboardItem.getType(mimeType); - return result; - } catch { - return ''; - } -} - -async function getStructStringFromClipboardData( - data: ClipboardItem[], -): Promise { - const clipboardItem = data[0]; - - if (clipboardItem instanceof ClipboardItem) { - const structStr = - (await safelyGetMimeType(clipboardItem, `web ${ChemicalMimeType.KET}`)) || - (await safelyGetMimeType(clipboardItem, `web ${ChemicalMimeType.Mol}`)) || - (await safelyGetMimeType(clipboardItem, `web ${ChemicalMimeType.Rxn}`)) || - (await safelyGetMimeType(clipboardItem, 'text/plain')); - return structStr === '' ? '' : structStr.text(); - } else { - return ( - data[ChemicalMimeType.KET] || - data[ChemicalMimeType.Mol] || - data[ChemicalMimeType.Rxn] || - data['text/plain'] - ); - } -} - function isAbleToCopy(editor: Editor): boolean { const struct = editor.structSelected(); const errorHandler = editor.errorHandler;