diff --git a/ketcher-autotests/tests/Macromolecule-editor/Chem-monomers/chem-library.spec.ts-snapshots/Open-Ketcher-Open-Chem-tab-in-library-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Chem-monomers/chem-library.spec.ts-snapshots/Open-Ketcher-Open-Chem-tab-in-library-1-chromium-linux.png index 87150fb137..703adec710 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/Chem-monomers/chem-library.spec.ts-snapshots/Open-Ketcher-Open-Chem-tab-in-library-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/Chem-monomers/chem-library.spec.ts-snapshots/Open-Ketcher-Open-Chem-tab-in-library-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/add-default-presets.spec.ts-snapshots/Macromolecules-default-presets-Check-Guanine-in-default-presets-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/add-default-presets.spec.ts-snapshots/Macromolecules-default-presets-Check-Guanine-in-default-presets-1-chromium-linux.png index 50264a4d28..3eac70bcaf 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/add-default-presets.spec.ts-snapshots/Macromolecules-default-presets-Check-Guanine-in-default-presets-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/add-default-presets.spec.ts-snapshots/Macromolecules-default-presets-Check-Guanine-in-default-presets-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/delete-preset.spec.ts-snapshots/Macromolecules-delete-RNA-presets-Should-not-delete-default-RNA-preset-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/delete-preset.spec.ts-snapshots/Macromolecules-delete-RNA-presets-Should-not-delete-default-RNA-preset-1-chromium-linux.png index aa429a5fef..ae5be33027 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/delete-preset.spec.ts-snapshots/Macromolecules-delete-RNA-presets-Should-not-delete-default-RNA-preset-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/delete-preset.spec.ts-snapshots/Macromolecules-delete-RNA-presets-Should-not-delete-default-RNA-preset-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/rna-layout.spec.ts-snapshots/RNA-layout-Each-panel-is-collapsed-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/rna-layout.spec.ts-snapshots/RNA-layout-Each-panel-is-collapsed-1-chromium-linux.png index b740382339..9d70f73bf5 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/rna-layout.spec.ts-snapshots/RNA-layout-Each-panel-is-collapsed-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/rna-layout.spec.ts-snapshots/RNA-layout-Each-panel-is-collapsed-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/rna-layout.spec.ts-snapshots/RNA-layout-RNA-Builder-panel-is-collapsed-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/rna-layout.spec.ts-snapshots/RNA-layout-RNA-Builder-panel-is-collapsed-1-chromium-linux.png index ddb5ba8714..9f10bc053e 100644 Binary files a/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/rna-layout.spec.ts-snapshots/RNA-layout-RNA-Builder-panel-is-collapsed-1-chromium-linux.png and b/ketcher-autotests/tests/Macromolecule-editor/RNAEditor/rna-layout.spec.ts-snapshots/RNA-layout-RNA-Builder-panel-is-collapsed-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts index f62905e339..dd5a152235 100644 --- a/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts +++ b/ketcher-autotests/tests/Macromolecule-editor/Rectangle-Selection-Tool/rectangle-selection-tool.spec.ts @@ -1,7 +1,6 @@ import { test } from '@playwright/test'; import { addMonomerToCanvas, - dragMouseTo, selectEraseTool, selectRectangleArea, selectRectangleSelectionTool, @@ -11,6 +10,7 @@ import { } from '@utils'; import { turnOnMacromoleculesEditor } from '@utils/macromolecules'; import { bondTwoMonomers } from '@utils/macromolecules/polymerBond'; +import { moveMonomer } from '@utils/macromolecules/monomer'; /* eslint-disable no-magic-numbers */ test.describe('Rectangle Selection Tool', () => { @@ -129,10 +129,7 @@ test.describe('Rectangle Selection Tool', () => { await takeEditorScreenshot(page); - // Move selected monomer - await selectRectangleSelectionTool(page); - await page.mouse.click(400, 400); - await dragMouseTo(200, 400, page); + await moveMonomer(page, peptide2, 200, 400); await takeEditorScreenshot(page); }); diff --git a/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts new file mode 100644 index 0000000000..607b62b50d --- /dev/null +++ b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts @@ -0,0 +1,113 @@ +import { Locator, test } from '@playwright/test'; +import { + addMonomerToCanvas, + clickRedo, + clickUndo, + selectSingleBondTool, + selectSnakeBondTool, + takeEditorScreenshot, + waitForPageInit, +} from '@utils'; +import { + hideMonomerPreview, + turnOnMacromoleculesEditor, +} from '@utils/macromolecules'; +import { bondTwoMonomers } from '@utils/macromolecules/polymerBond'; +import { moveMonomer } from '@utils/macromolecules/monomer'; +/* eslint-disable no-magic-numbers */ + +test.describe('Undo Redo', () => { + let peptide1: Locator; + let peptide2: Locator; + test.beforeEach(async ({ page }) => { + await waitForPageInit(page); + await turnOnMacromoleculesEditor(page); + const MONOMER_NAME = 'Tza___3-thiazolylalanine'; + const MONOMER_ALIAS = 'Tza'; + + peptide1 = await addMonomerToCanvas( + page, + MONOMER_NAME, + MONOMER_ALIAS, + 300, + 300, + 0, + ); + peptide2 = await addMonomerToCanvas( + page, + MONOMER_NAME, + MONOMER_ALIAS, + 400, + 300, + 1, + ); + const peptide3 = await addMonomerToCanvas( + page, + MONOMER_NAME, + MONOMER_ALIAS, + 500, + 300, + 2, + ); + + // Select bond tool + await selectSingleBondTool(page); + + // Create bonds between peptides + await bondTwoMonomers(page, peptide1, peptide2); + await bondTwoMonomers(page, peptide3, peptide2); + + await hideMonomerPreview(page); + }); + + test('Undo redo for monomers and bonds addition', async ({ page }) => { + /* + Description: Add monomers and bonds and do undo redo + */ + + // check that history pointer stops on last operation + await clickRedo(page); + await clickRedo(page); + + // check undo + await clickUndo(page); + await clickUndo(page); + await takeEditorScreenshot(page); + + // check that history pointer stops on first operation + await clickUndo(page); + await clickUndo(page); + await clickUndo(page); + await clickUndo(page); + await clickUndo(page); + + // check redo + await clickRedo(page); + await takeEditorScreenshot(page); + }); + + test('Undo redo for snake mode layout', async ({ page }) => { + /* + Description: Add monomers and bonds, activate snake mode and do undo redo + */ + + await selectSnakeBondTool(page); + await clickUndo(page); + await takeEditorScreenshot(page); + }); + + test('Undo redo for monomers movement', async ({ page }) => { + /* + Description: Move monomers and do undo redo + */ + + await moveMonomer(page, peptide1, 500, 500); + await moveMonomer(page, peptide2, 600, 600); + await moveMonomer(page, peptide2, 400, 400); + await clickUndo(page); + await clickUndo(page); + await takeEditorScreenshot(page); + await clickRedo(page); + await takeEditorScreenshot(page); + }); +}); diff --git a/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-and-bonds-addition-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-and-bonds-addition-1-chromium-linux.png new file mode 100644 index 0000000000..7b2432cc45 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-and-bonds-addition-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-and-bonds-addition-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-and-bonds-addition-2-chromium-linux.png new file mode 100644 index 0000000000..83f57d0c96 Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-and-bonds-addition-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-movement-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-movement-1-chromium-linux.png new file mode 100644 index 0000000000..d7ae5b4e7b Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-movement-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-movement-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-movement-2-chromium-linux.png new file mode 100644 index 0000000000..f6356d009a Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-monomers-movement-2-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-snake-mode-layout-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-snake-mode-layout-1-chromium-linux.png new file mode 100644 index 0000000000..7c15ada6af Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Undo-Redo/undo-redo.spec.ts-snapshots/Undo-Redo-Undo-redo-for-snake-mode-layout-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/utils/canvas/tools/helpers.ts b/ketcher-autotests/tests/utils/canvas/tools/helpers.ts index 545a22aeac..bba68c9ef6 100644 --- a/ketcher-autotests/tests/utils/canvas/tools/helpers.ts +++ b/ketcher-autotests/tests/utils/canvas/tools/helpers.ts @@ -68,6 +68,17 @@ export async function selectRectangleSelectionTool(page: Page) { await bondToolButton.click(); } +// undo/redo heplers currently used for macromolecules editor because buttons are in different panel +export async function clickUndo(page: Page) { + const undoButton = page.getByTestId('undo-button'); + await undoButton.click(); +} + +export async function clickRedo(page: Page) { + const redoButton = page.getByTestId('redo-button'); + await redoButton.click(); +} + export async function selectRectangleArea( page: Page, startX: number, diff --git a/ketcher-autotests/tests/utils/macromolecules/monomer.ts b/ketcher-autotests/tests/utils/macromolecules/monomer.ts new file mode 100644 index 0000000000..d59219cb16 --- /dev/null +++ b/ketcher-autotests/tests/utils/macromolecules/monomer.ts @@ -0,0 +1,13 @@ +import { Locator, Page } from '@playwright/test'; +import { dragMouseTo, selectRectangleSelectionTool } from '@utils'; + +export async function moveMonomer( + page: Page, + monomer: Locator, + x: number, + y: number, +) { + await selectRectangleSelectionTool(page); + await monomer.click(); + await dragMouseTo(x, y, page); +} diff --git a/packages/ketcher-core/__tests__/application/editor/EditorHistory.test.ts b/packages/ketcher-core/__tests__/application/editor/EditorHistory.test.ts new file mode 100644 index 0000000000..11946700ab --- /dev/null +++ b/packages/ketcher-core/__tests__/application/editor/EditorHistory.test.ts @@ -0,0 +1,59 @@ +import { CoreEditor, EditorHistory } from 'application/editor'; +import { createPolymerEditorCanvas } from '../../helpers/dom'; +import { Command } from 'domain/entities/Command'; + +describe('EditorHistory', () => { + let canvas; + let editor: CoreEditor; + let history: EditorHistory; + beforeEach(() => { + canvas = createPolymerEditorCanvas(); + editor = new CoreEditor({ theme: {}, canvas }); + history = new EditorHistory(editor); + }); + + afterEach(() => { + history.destroy(); + }); + + it('should be a singletone', () => { + const historyInstance2 = new EditorHistory(editor); + expect(history).toBe(historyInstance2); + }); + + it('should create another instance after destroy', () => { + history.destroy(); + const historyInstance2 = new EditorHistory(editor); + expect(history).not.toBe(historyInstance2); + }); + + it('should add commands into history stack', () => { + history.update(new Command()); + history.update(new Command()); + expect(history.historyStack.length).toEqual(2); + }); + + it('should move pointer when undo/redo methods called', () => { + history.update(new Command()); + history.update(new Command()); + expect(history.historyPointer).toEqual(2); + history.redo(); + expect(history.historyPointer).toEqual(2); + history.undo(); + expect(history.historyPointer).toEqual(1); + history.undo(); + expect(history.historyPointer).toEqual(0); + history.undo(); + expect(history.historyPointer).toEqual(0); + history.redo(); + expect(history.historyPointer).toEqual(1); + }); + + it('should have stack maximum size equal 32 commands', () => { + for (let i = 0; i < 40; i++) { + history.update(new Command()); + } + expect(history.historyStack.length).toEqual(32); + expect(history.historyPointer).toEqual(32); + }); +}); diff --git a/packages/ketcher-core/__tests__/application/editor/tools/SelectRectangleTool.test.ts b/packages/ketcher-core/__tests__/application/editor/tools/SelectRectangleTool.test.ts index 91462ceaec..0b655ad41b 100644 --- a/packages/ketcher-core/__tests__/application/editor/tools/SelectRectangleTool.test.ts +++ b/packages/ketcher-core/__tests__/application/editor/tools/SelectRectangleTool.test.ts @@ -34,7 +34,28 @@ jest.mock('d3', () => { style() { return this; }, - on() {}, + on() { + return this; + }, + append() { + return this; + }, + data() { + return this; + }, + text() { + return this; + }, + node() { + return { + getBBox() { + return {}; + }, + getBoundingClientRect() { + return {}; + }, + }; + }, }; }, ZoomTransform: jest.fn().mockImplementation(() => { @@ -67,6 +88,10 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({ })); describe('Select Rectangle Tool', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should select drawing entity on mousedown', () => { const polymerBond = getFinishedPolymerBond(0, 0, 10, 10); const event = { @@ -111,11 +136,15 @@ describe('Select Rectangle Tool', () => { new Vec2(0, 0), ); editor.renderersContainer.update(modelChanges); + const peptide = Array.from(editor.drawingEntitiesManager.monomers)[0][1]; const onMove = jest.fn(); jest .spyOn(BaseMonomerRenderer.prototype, 'moveSelection') .mockImplementation(onMove); + jest + .spyOn(PeptideRenderer.prototype, 'drawSelection') + .mockImplementation(() => {}); const selectRectangleTool = new SelectRectangle(editor); diff --git a/packages/ketcher-core/__tests__/domain/entities/drawingEntitiesManager.test.ts b/packages/ketcher-core/__tests__/domain/entities/drawingEntitiesManager.test.ts index 90f3c55138..b3a161e6c3 100644 --- a/packages/ketcher-core/__tests__/domain/entities/drawingEntitiesManager.test.ts +++ b/packages/ketcher-core/__tests__/domain/entities/drawingEntitiesManager.test.ts @@ -10,14 +10,15 @@ import { PolymerBond } from 'domain/entities/PolymerBond'; import { DrawingEntity } from 'domain/entities/DrawingEntity'; import { DrawingEntityHoverOperation, + DrawingEntityMoveOperation, DrawingEntitySelectOperation, } from 'application/editor/operations/drawingEntity'; import { MonomerAddOperation, MonomerDeleteOperation, MonomerHoverOperation, - MonomerMoveOperation, } from 'application/editor/operations/monomer'; +import { RenderersManager } from 'application/render/renderers/RenderersManager'; describe('Drawing Entities Manager', () => { it('should create monomer', () => { @@ -33,11 +34,12 @@ describe('Drawing Entities Manager', () => { it('should create polymer bond', () => { const drawingEntitiesManager = new DrawingEntitiesManager(); - const { command, polymerBond } = drawingEntitiesManager.addPolymerBond( - new Peptide(peptideMonomerItem), - new Vec2(0, 0), - new Vec2(10, 10), - ); + const { command, polymerBond } = + drawingEntitiesManager.startPolymerBondCreation( + new Peptide(peptideMonomerItem), + new Vec2(0, 0), + new Vec2(10, 10), + ); expect(command.operations.length).toEqual(1); expect(command.operations[0]).toBeInstanceOf(PolymerBondAddOperation); expect(polymerBond).toBeInstanceOf(PolymerBond); @@ -55,7 +57,7 @@ describe('Drawing Entities Manager', () => { secondPeptide.attachmentPointsToBonds = { R2: null }; secondPeptide.potentialAttachmentPointsToBonds = { R2: null }; - const { polymerBond } = drawingEntitiesManager.addPolymerBond( + const { polymerBond } = drawingEntitiesManager.startPolymerBondCreation( firstPeptide, new Vec2(0, 0), new Vec2(10, 10), @@ -85,10 +87,12 @@ describe('Drawing Entities Manager', () => { it('should delete peptide', () => { const drawingEntitiesManager = new DrawingEntitiesManager(); + const renderersManager = new RenderersManager({ theme: {} }); drawingEntitiesManager.addMonomer(peptideMonomerItem, new Vec2(0, 0)); const peptide = Array.from(drawingEntitiesManager.monomers)[0][1]; expect(peptide).toBeInstanceOf(Peptide); const command = drawingEntitiesManager.deleteMonomer(peptide); + renderersManager.update(command); expect(command.operations.length).toEqual(1); expect(command.operations[0]).toBeInstanceOf(MonomerDeleteOperation); expect(drawingEntitiesManager.monomers.size).toEqual(0); @@ -96,7 +100,8 @@ describe('Drawing Entities Manager', () => { it('should delete polymer bond', () => { const drawingEntitiesManager = new DrawingEntitiesManager(); - const { polymerBond } = drawingEntitiesManager.addPolymerBond( + const renderersManager = new RenderersManager({ theme: {} }); + const { polymerBond } = drawingEntitiesManager.startPolymerBondCreation( new Peptide(peptideMonomerItem), new Vec2(0, 0), new Vec2(10, 10), @@ -105,6 +110,7 @@ describe('Drawing Entities Manager', () => { Array.from(drawingEntitiesManager.polymerBonds)[0][1], ).toBeInstanceOf(PolymerBond); const command = drawingEntitiesManager.deletePolymerBond(polymerBond); + renderersManager.update(command); expect(command.operations.length).toEqual(1); expect(command.operations[0]).toBeInstanceOf(PolymerBondDeleteOperation); expect(drawingEntitiesManager.polymerBonds.size).toEqual(0); @@ -121,15 +127,19 @@ describe('Drawing Entities Manager', () => { it('should move peptide', () => { const drawingEntitiesManager = new DrawingEntitiesManager(); - const peptide = new Peptide(peptideMonomerItem); - const command = drawingEntitiesManager.moveMonomer( - peptide, + const renderersManager = new RenderersManager({ theme: {} }); + jest.spyOn(renderersManager, 'moveDrawingEntity').mockImplementation(); + drawingEntitiesManager.addMonomer(peptideMonomerItem, new Vec2(0, 0)); + const peptide = Array.from(drawingEntitiesManager.monomers)[0][1]; + peptide.turnOnSelection(); + const command = drawingEntitiesManager.moveSelectedDrawingEntities( new Vec2(100, 200), ); + renderersManager.update(command); expect(peptide.position.x).toEqual(100); expect(peptide.position.y).toEqual(200); expect(command.operations.length).toEqual(1); - expect(command.operations[0]).toBeInstanceOf(MonomerMoveOperation); + expect(command.operations[0]).toBeInstanceOf(DrawingEntityMoveOperation); }); it('should hover drawing entity', () => { diff --git a/packages/ketcher-core/src/application/editor/Editor.ts b/packages/ketcher-core/src/application/editor/Editor.ts index bb54da5ff4..61adba4c4a 100644 --- a/packages/ketcher-core/src/application/editor/Editor.ts +++ b/packages/ketcher-core/src/application/editor/Editor.ts @@ -21,6 +21,7 @@ import { resetEditorEvents, } from 'application/editor/editorEvents'; import { PolymerBondRenderer } from 'application/render/renderers'; +import { EditorHistory, HistoryOperationType } from './EditorHistory'; import { Editor } from 'application/editor/editor.types'; import { MacromoleculesConverter } from 'application/editor/MacromoleculesConverter'; import { BaseMonomer } from 'domain/entities/BaseMonomer'; @@ -82,6 +83,7 @@ export class CoreEditor { this.onCancelBondCreation(secondMonomer), ); this.events.selectMode.add((isSnakeMode) => this.onSelectMode(isSnakeMode)); + this.events.selectHistory.add((name) => this.onSelectHistory(name)); renderersEvents.forEach((eventName) => { this.events[eventName].add((event) => @@ -126,9 +128,20 @@ export class CoreEditor { this.canvas.width.baseVal.value, isSnakeMode, ); + const history = new EditorHistory(this); + history.update(modelChanges); this.renderersContainer.update(modelChanges); } + private onSelectHistory(name: HistoryOperationType) { + const history = new EditorHistory(this); + if (name === 'undo') { + history.undo(); + } else if (name === 'redo') { + history.redo(); + } + } + public selectTool(name: string, options?) { const ToolConstructor: ToolConstructorInterface = toolsMap[name]; const oldTool = this.tool; @@ -270,6 +283,8 @@ export class CoreEditor { public switchToMicromolecules() { this.unsubscribeEvents(); + const history = new EditorHistory(this); + history.destroy(); const struct = this.micromoleculesEditor.struct(); const reStruct = this.micromoleculesEditor.render.ctab; const { conversionErrorMessage } = diff --git a/packages/ketcher-core/src/application/editor/EditorHistory.ts b/packages/ketcher-core/src/application/editor/EditorHistory.ts new file mode 100644 index 0000000000..08e59cc870 --- /dev/null +++ b/packages/ketcher-core/src/application/editor/EditorHistory.ts @@ -0,0 +1,80 @@ +/**************************************************************************** + * Copyright 2021 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***************************************************************************/ + +import { Command } from 'domain/entities/Command'; +import { CoreEditor } from './Editor'; +import assert from 'assert'; +const HISTORY_SIZE = 32; // put me to options + +export type HistoryOperationType = 'undo' | 'redo'; + +export class EditorHistory { + historyStack: Command[] | [] = []; + historyPointer = 0; + editor: CoreEditor | undefined; + + private static _instance; + constructor(editor: CoreEditor) { + if (EditorHistory._instance) { + return EditorHistory._instance; + } + this.editor = editor; + this.historyPointer = 0; + + EditorHistory._instance = this; + + return this; + } + + update(command: Command) { + this.historyStack.splice(this.historyPointer, HISTORY_SIZE + 1, command); + if (this.historyStack.length > HISTORY_SIZE) { + this.historyStack.shift(); + } + this.historyPointer = this.historyStack.length; + } + + undo() { + if (this.historyPointer === 0) { + return; + } + + assert(this.editor); + + this.historyPointer--; + const lastCommand = this.historyStack[this.historyPointer]; + lastCommand.invert(this.editor.renderersContainer); + const turnOffSelectionCommand = + this.editor?.drawingEntitiesManager.unselectAllDrawingEntities(); + this.editor?.renderersContainer.update(turnOffSelectionCommand); + } + + redo() { + if (this.historyPointer === this.historyStack.length) { + return; + } + + assert(this.editor); + + const lastCommand = this.historyStack[this.historyPointer]; + lastCommand.execute(this.editor.renderersContainer); + this.historyPointer++; + } + + destroy() { + EditorHistory._instance = null; + } +} diff --git a/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts b/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts index 40b4d523c2..748e76b46d 100644 --- a/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts +++ b/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts @@ -277,20 +277,14 @@ export class MacromoleculesConverter { beginAtomSgroup instanceof MonomerMicromolecule && endAtomSgroup instanceof MonomerMicromolecule ) { - const { command: polymerBondAdditionCommand, polymerBond } = - drawingEntitiesManager.addPolymerBond( - sgroupToMonomer.get(beginAtomSgroup), - sgroupToMonomer.get(beginAtomSgroup)?.position, - sgroupToMonomer.get(endAtomSgroup)?.position, - ); - command.merge(polymerBondAdditionCommand); - + const firstMonomer = sgroupToMonomer.get(beginAtomSgroup); const secondMonomer = sgroupToMonomer.get(endAtomSgroup); + assert(firstMonomer); assert(secondMonomer); command.merge( - drawingEntitiesManager.finishPolymerBondCreation( - polymerBond, + drawingEntitiesManager.createPolymerBond( + firstMonomer, secondMonomer, beginAtomAttachmentPointNumber, endAtomAttachmentPointNumber, diff --git a/packages/ketcher-core/src/application/editor/editorEvents.ts b/packages/ketcher-core/src/application/editor/editorEvents.ts index dcd9893c25..aa7e1da6b8 100644 --- a/packages/ketcher-core/src/application/editor/editorEvents.ts +++ b/packages/ketcher-core/src/application/editor/editorEvents.ts @@ -11,6 +11,7 @@ export function resetEditorEvents() { createBondViaModal: new Subscription(), cancelBondCreationViaModal: new Subscription(), selectMode: new Subscription(), + selectHistory: new Subscription(), error: new Subscription(), openMonomerConnectionModal: new Subscription(), mouseOverPolymerBond: new Subscription(), diff --git a/packages/ketcher-core/src/application/editor/index.ts b/packages/ketcher-core/src/application/editor/index.ts index 424b9c782e..2716e109df 100644 --- a/packages/ketcher-core/src/application/editor/index.ts +++ b/packages/ketcher-core/src/application/editor/index.ts @@ -28,3 +28,4 @@ export * from './actions'; export * from './shared/constants'; export * from './editor.types'; export * from './Editor'; +export * from './EditorHistory'; diff --git a/packages/ketcher-core/src/application/editor/operations/drawingEntity/index.ts b/packages/ketcher-core/src/application/editor/operations/drawingEntity/index.ts index 133b731941..c8dc827f37 100644 --- a/packages/ketcher-core/src/application/editor/operations/drawingEntity/index.ts +++ b/packages/ketcher-core/src/application/editor/operations/drawingEntity/index.ts @@ -8,6 +8,8 @@ export class DrawingEntityHoverOperation implements Operation { public execute(renderersManager: RenderersManager) { renderersManager.hoverDrawingEntity(this.drawingEntity); } + + public invert() {} } export class DrawingEntitySelectOperation implements Operation { @@ -16,18 +18,44 @@ export class DrawingEntitySelectOperation implements Operation { public execute(renderersManager: RenderersManager) { renderersManager.selectDrawingEntity(this.drawingEntity); } + + public invert() {} } export class DrawingEntityMoveOperation implements Operation { - constructor(private drawingEntity: DrawingEntity) {} + private wasInverted = false; + constructor( + private moveDrawingEntityChangeModel: () => void, + private invertMoveDrawingEntityChangeModel: () => void, + private redoDrawingEntityChangeModel: () => void, + private drawingEntity: DrawingEntity, + ) {} public execute(renderersManager: RenderersManager) { + this.wasInverted + ? this.redoDrawingEntityChangeModel() + : this.moveDrawingEntityChangeModel(); renderersManager.moveDrawingEntity(this.drawingEntity); } + + public invert(renderersManager: RenderersManager) { + this.invertMoveDrawingEntityChangeModel(); + renderersManager.moveDrawingEntity(this.drawingEntity); + this.wasInverted = true; + } } export class DrawingEntityRedrawOperation implements Operation { - constructor(private drawingEntity: DrawingEntity) {} + constructor( + private drawingEntityRedrawModelChange: () => DrawingEntity, + private invertDrawingEntityRedrawModelChange: () => DrawingEntity, + ) {} public execute(renderersManager: RenderersManager) { - renderersManager.redrawDrawingEntity(this.drawingEntity); + const drawingEntity = this.drawingEntityRedrawModelChange(); + renderersManager.redrawDrawingEntity(drawingEntity); + } + + public invert(renderersManager: RenderersManager) { + const drawingEntity = this.invertDrawingEntityRedrawModelChange(); + renderersManager.redrawDrawingEntity(drawingEntity); } } diff --git a/packages/ketcher-core/src/application/editor/operations/monomer/index.ts b/packages/ketcher-core/src/application/editor/operations/monomer/index.ts index 48f31c2866..85c282231b 100644 --- a/packages/ketcher-core/src/application/editor/operations/monomer/index.ts +++ b/packages/ketcher-core/src/application/editor/operations/monomer/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ /**************************************************************************** * Copyright 2021 EPAM Systems * @@ -20,18 +21,45 @@ import { Operation } from 'domain/entities/Operation'; import { BaseMonomer } from 'domain/entities/BaseMonomer'; export class MonomerAddOperation implements Operation { - constructor(public monomer: BaseMonomer, private callback?: () => void) {} + public monomer: BaseMonomer; + constructor( + public addMonomerChangeModel: (monomer?: BaseMonomer) => BaseMonomer, + public deleteMonomerChangeModel: (monomer: BaseMonomer) => void, + private callback?: () => void, + ) { + this.monomer = this.addMonomerChangeModel(); + } public execute(renderersManager: RenderersManager) { + this.monomer = this.addMonomerChangeModel(this.monomer); renderersManager.addMonomer(this.monomer, this.callback); } + + public invert(renderersManager: RenderersManager) { + if (this.monomer) { + this.deleteMonomerChangeModel(this.monomer); + renderersManager.deleteMonomer(this.monomer); + } + } } export class MonomerMoveOperation implements Operation { - constructor(private peptide: BaseMonomer) {} + public monomer: BaseMonomer; + constructor( + private monomerMoveModelChange: () => BaseMonomer, + private invertMonomerMoveModelChange: () => BaseMonomer, + ) { + this.monomer = this.monomerMoveModelChange(); + } public execute(renderersManager: RenderersManager) { - renderersManager.moveMonomer(this.peptide); + this.monomer = this.monomerMoveModelChange(); + renderersManager.moveMonomer(this.monomer); + } + + public invert(renderersManager: RenderersManager) { + this.monomer = this.invertMonomerMoveModelChange(); + renderersManager.moveMonomer(this.monomer); } } @@ -47,6 +75,8 @@ export class MonomerHoverOperation implements Operation { this.needRedrawAttachmentPoints, ); } + + public invert() {} } export class AttachmentPointHoverOperation implements Operation { @@ -61,12 +91,28 @@ export class AttachmentPointHoverOperation implements Operation { this.attachmentPointName, ); } + + public invert() {} } export class MonomerDeleteOperation implements Operation { - constructor(private peptide: BaseMonomer) {} + monomer: BaseMonomer; + constructor( + monomer: BaseMonomer, + public addMonomerChangeModel: (monomer: BaseMonomer) => BaseMonomer, + public deleteMonomerChangeModel: (monomer: BaseMonomer) => void, + private callback?: () => void, + ) { + this.monomer = monomer; + } public execute(renderersManager: RenderersManager) { - renderersManager.deleteMonomer(this.peptide); + this.deleteMonomerChangeModel(this.monomer); + renderersManager.deleteMonomer(this.monomer); + } + + public invert(renderersManager: RenderersManager) { + this.monomer = this.addMonomerChangeModel(this.monomer); + renderersManager.addMonomer(this.monomer, this.callback); } } 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 3f4c7726a7..d709d6d980 100644 --- a/packages/ketcher-core/src/application/editor/operations/polymerBond/index.ts +++ b/packages/ketcher-core/src/application/editor/operations/polymerBond/index.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************/ -/* eslint-disable @typescript-eslint/no-use-before-define */ import { PolymerBond } from 'domain/entities/PolymerBond'; import { RenderersManager } from 'application/render/renderers/RenderersManager'; @@ -21,40 +20,72 @@ import { Operation } from 'domain/entities/Operation'; import { BaseMonomer } from 'domain/entities/BaseMonomer'; export class PolymerBondAddOperation implements Operation { - constructor(private polymerBond: PolymerBond) {} + public polymerBond; + constructor( + private addPolymerBondChangeModel: ( + polymerBond?: PolymerBond, + ) => PolymerBond, + private deletePolymerBondChangeModel: (polymerBond) => void, + ) { + this.polymerBond = this.addPolymerBondChangeModel(); + } public execute(renderersManager: RenderersManager) { + this.polymerBond = this.addPolymerBondChangeModel(this.polymerBond); renderersManager.addPolymerBond(this.polymerBond); } + + public invert(renderersManager: RenderersManager) { + this.deletePolymerBondChangeModel(this.polymerBond); + renderersManager.deletePolymerBond(this.polymerBond); + } } export class PolymerBondDeleteOperation implements Operation { - constructor(private polymerBond: PolymerBond) {} + constructor( + public polymerBond: PolymerBond, + private deletePolymerBondChangeModel: () => void, + private finishPolymerBondCreationModelChange: ( + polymerBond?: PolymerBond, + ) => PolymerBond, + ) {} public execute(renderersManager: RenderersManager) { + this.deletePolymerBondChangeModel(); renderersManager.deletePolymerBond(this.polymerBond); } + + public invert(renderersManager: RenderersManager) { + this.polymerBond = this.finishPolymerBondCreationModelChange( + this.polymerBond, + ); + renderersManager.addPolymerBond(this.polymerBond); + } } export class PolymerBondMoveOperation implements Operation { - constructor(private polymerBond: PolymerBond) {} + constructor(public polymerBond: PolymerBond) {} public execute(renderersManager: RenderersManager) { renderersManager.movePolymerBond(this.polymerBond); } + + public invert() {} } export class PolymerBondShowInfoOperation implements Operation { - constructor(private polymerBond: PolymerBond) {} + constructor(public polymerBond: PolymerBond) {} public execute(renderersManager: RenderersManager) { renderersManager.showPolymerBondInformation(this.polymerBond); } + + public invert() {} } export class PolymerBondCancelCreationOperation implements Operation { constructor( - private polymerBond: PolymerBond, + public polymerBond: PolymerBond, private secondMonomer?: BaseMonomer, ) {} @@ -64,12 +95,30 @@ export class PolymerBondCancelCreationOperation implements Operation { this.secondMonomer, ); } + + public invert() {} } export class PolymerBondFinishCreationOperation implements Operation { - constructor(private polymerBond: PolymerBond) {} + public polymerBond; + constructor( + private finishPolymerBondCreationModelChange: ( + polymerBond?: PolymerBond, + ) => PolymerBond, + private deletePolymerBondCreationModelChange: (polymerBond) => void, + ) { + this.polymerBond = this.finishPolymerBondCreationModelChange(); + } public execute(renderersManager: RenderersManager) { + this.polymerBond = this.finishPolymerBondCreationModelChange( + this.polymerBond, + ); renderersManager.finishPolymerBondCreation(this.polymerBond); } + + public invert(renderersManager: RenderersManager) { + this.deletePolymerBondCreationModelChange(this.polymerBond); + renderersManager.deletePolymerBond(this.polymerBond); + } } diff --git a/packages/ketcher-core/src/application/editor/tools/Bond.ts b/packages/ketcher-core/src/application/editor/tools/Bond.ts index 225cbd1284..90751afc2a 100644 --- a/packages/ketcher-core/src/application/editor/tools/Bond.ts +++ b/packages/ketcher-core/src/application/editor/tools/Bond.ts @@ -14,7 +14,7 @@ * limitations under the License. ***************************************************************************/ import { BaseMonomerRenderer } from 'application/render/renderers'; -import { CoreEditor } from 'application/editor'; +import { CoreEditor, EditorHistory } from 'application/editor'; import { PolymerBondRenderer } from 'application/render/renderers/PolymerBondRenderer'; import assert from 'assert'; import { BaseMonomer } from 'domain/entities/BaseMonomer'; @@ -29,9 +29,11 @@ import Coordinates from 'application/editor/shared/coordinates'; class PolymerBond implements BaseTool { private bondRenderer?: PolymerBondRenderer; private isBondConnectionModalOpen = false; + history: EditorHistory; constructor(private editor: CoreEditor) { this.editor = editor; + this.history = new EditorHistory(this.editor); } public mouseDownAttachmentPoint(event) { @@ -70,7 +72,7 @@ class PolymerBond implements BaseTool { return; } const { polymerBond, command: modelChanges } = - this.editor.drawingEntitiesManager.addPolymerBond( + this.editor.drawingEntitiesManager.startPolymerBondCreation( selectedRenderer.monomer, selectedRenderer.monomer.position, Coordinates.canvasToModel(this.editor.lastCursorPositionOfCanvas), @@ -247,9 +249,11 @@ class PolymerBond implements BaseTool { }); return; } - const modelChanges = this.finishBondCreation(renderer.monomer); this.editor.renderersContainer.update(modelChanges); + this.editor.renderersContainer.deletePolymerBond( + this.bondRenderer.polymerBond, + ); this.bondRenderer = undefined; event.stopPropagation(); } @@ -335,7 +339,11 @@ class PolymerBond implements BaseTool { // This logic so far is only for no-modal connections. Maybe then we can chain it after modal invoke const modelChanges = this.finishBondCreation(renderer.monomer); this.editor.renderersContainer.update(modelChanges); + this.editor.renderersContainer.deletePolymerBond( + this.bondRenderer.polymerBond, + ); this.bondRenderer = undefined; + this.history.update(modelChanges); event.stopPropagation(); } } diff --git a/packages/ketcher-core/src/application/editor/tools/Clear.ts b/packages/ketcher-core/src/application/editor/tools/Clear.ts index 27da31b2af..7784d46924 100644 --- a/packages/ketcher-core/src/application/editor/tools/Clear.ts +++ b/packages/ketcher-core/src/application/editor/tools/Clear.ts @@ -13,14 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************/ -import { CoreEditor } from 'application/editor'; +import { CoreEditor, EditorHistory } from 'application/editor'; import { BaseTool } from 'application/editor/tools/Tool'; class ClearTool implements BaseTool { + private history: EditorHistory; + constructor(private editor: CoreEditor) { this.editor = editor; + this.history = new EditorHistory(editor); + const modelChanges = this.editor.drawingEntitiesManager.deleteAllEntities(); this.editor.renderersContainer.update(modelChanges); + this.history.update(modelChanges); } destroy() {} diff --git a/packages/ketcher-core/src/application/editor/tools/Erase.ts b/packages/ketcher-core/src/application/editor/tools/Erase.ts index 7f976fc0c5..12bc0778ba 100644 --- a/packages/ketcher-core/src/application/editor/tools/Erase.ts +++ b/packages/ketcher-core/src/application/editor/tools/Erase.ts @@ -13,16 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************/ -import { CoreEditor } from 'application/editor'; +import { CoreEditor, EditorHistory } from 'application/editor'; import { BaseRenderer } from 'application/render/renderers/BaseRenderer'; import { BaseTool } from 'application/editor/tools/Tool'; class EraserTool implements BaseTool { + private history: EditorHistory; constructor(private editor: CoreEditor) { this.editor = editor; + this.history = new EditorHistory(editor); if (this.editor.drawingEntitiesManager.selectedEntities.length) { const modelChanges = this.editor.drawingEntitiesManager.deleteSelectedEntities(); + this.history.update(modelChanges); this.editor.renderersContainer.update(modelChanges); } } @@ -34,6 +37,7 @@ class EraserTool implements BaseTool { this.editor.drawingEntitiesManager.deleteDrawingEntity( selectedItemRenderer.drawingEntity, ); + this.history.update(modelChanges); this.editor.renderersContainer.update(modelChanges); } } diff --git a/packages/ketcher-core/src/application/editor/tools/Monomer.ts b/packages/ketcher-core/src/application/editor/tools/Monomer.ts index b243318ead..dbab6a8eba 100644 --- a/packages/ketcher-core/src/application/editor/tools/Monomer.ts +++ b/packages/ketcher-core/src/application/editor/tools/Monomer.ts @@ -20,7 +20,7 @@ import { Sugar } from 'domain/entities/Sugar'; import { Phosphate } from 'domain/entities/Phosphate'; import { RNABase } from 'domain/entities/RNABase'; import { Vec2 } from 'domain/entities'; -import { CoreEditor } from 'application/editor'; +import { CoreEditor, EditorHistory } from 'application/editor'; import { BaseMonomerRenderer } from 'application/render/renderers'; import { MonomerItemType } from 'domain/types'; import { monomerFactory } from '../operations/monomer/monomerFactory'; @@ -40,9 +40,11 @@ class MonomerTool implements BaseTool { readonly MONOMER_PREVIEW_SCALE_FACTOR = 0.4; readonly MONOMER_PREVIEW_OFFSET_X = 45; readonly MONOMER_PREVIEW_OFFSET_Y = 45; + history: EditorHistory; constructor(private editor: CoreEditor, private monomer: MonomerItemType) { this.editor = editor; this.monomer = monomer; + this.history = new EditorHistory(this.editor); } mousedown() { @@ -60,6 +62,7 @@ class MonomerTool implements BaseTool { position, ); + this.history.update(modelChanges); this.editor.renderersContainer.update(modelChanges); } diff --git a/packages/ketcher-core/src/application/editor/tools/RnaPreset.ts b/packages/ketcher-core/src/application/editor/tools/RnaPreset.ts index ba43ee9942..40e9739fc1 100644 --- a/packages/ketcher-core/src/application/editor/tools/RnaPreset.ts +++ b/packages/ketcher-core/src/application/editor/tools/RnaPreset.ts @@ -17,7 +17,7 @@ import { Tool, IRnaPreset } from 'application/editor/tools/Tool'; import { Sugar } from 'domain/entities/Sugar'; import { Vec2 } from 'domain/entities'; -import { CoreEditor } from 'application/editor'; +import { CoreEditor, EditorHistory } from 'application/editor'; import { BaseMonomerRenderer } from 'application/render/renderers'; import { MonomerItemType } from 'domain/types'; import { monomerFactory } from '../operations/monomer/monomerFactory'; @@ -42,6 +42,8 @@ class RnaPresetTool implements Tool { readonly RNA_BASE_PREVIEW_OFFSET_X = 2; readonly RNA_BASE_PREVIEW_OFFSET_Y = 20; readonly PHOSPHATE_PREVIEW_OFFSET_X = 18; + history: EditorHistory; + constructor(private editor: CoreEditor, preset: IRnaPreset) { this.editor = editor; if (preset?.base) { @@ -53,6 +55,7 @@ class RnaPresetTool implements Tool { if (preset?.sugar) { this.sugar = preset?.sugar; } + this.history = new EditorHistory(this.editor); } mousedown() { @@ -93,6 +96,7 @@ class RnaPresetTool implements Tool { : undefined, }); + this.history.update(modelChanges); this.editor.renderersContainer.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 3f9264c0c8..235ee76d9e 100644 --- a/packages/ketcher-core/src/application/editor/tools/SelectRectangle.ts +++ b/packages/ketcher-core/src/application/editor/tools/SelectRectangle.ts @@ -14,7 +14,7 @@ * limitations under the License. ***************************************************************************/ import { Vec2 } from 'domain/entities'; -import { CoreEditor } from 'application/editor'; +import { CoreEditor, EditorHistory } from 'application/editor'; import { brush as d3Brush, select } from 'd3'; import { BaseRenderer } from 'application/render/renderers/BaseRenderer'; import { Command } from 'domain/entities/Command'; @@ -26,10 +26,13 @@ class SelectRectangle implements BaseTool { private brushArea; private moveStarted; private mousePositionAfterMove = new Vec2(0, 0, 0); + private mousePositionBeforeMove = new Vec2(0, 0, 0); private canvasResizeObserver?: ResizeObserver; + private history: EditorHistory; constructor(private editor: CoreEditor) { this.editor = editor; + this.history = new EditorHistory(this.editor); this.destroy(); this.createBrush(); @@ -98,6 +101,7 @@ class SelectRectangle implements BaseTool { if (renderer instanceof BaseRenderer) { this.moveStarted = true; this.mousePositionAfterMove = this.editor.lastCursorPositionOfCanvas; + this.mousePositionBeforeMove = this.editor.lastCursorPositionOfCanvas; if (renderer.drawingEntity.selected) { return; } else { @@ -132,8 +136,29 @@ class SelectRectangle implements BaseTool { mouseup(event) { const renderer = event.target.__data__; + if (this.moveStarted && renderer.drawingEntity.selected) { this.moveStarted = false; + + if ( + Vec2.diff( + this.mousePositionAfterMove, + this.mousePositionBeforeMove, + ).length() === 0 + ) { + return; + } + const modelChanges = + this.editor.drawingEntitiesManager.moveSelectedDrawingEntities( + new Vec2(0, 0), + Coordinates.canvasToModel( + new Vec2( + this.mousePositionAfterMove.x - this.mousePositionBeforeMove.x, + this.mousePositionAfterMove.y - this.mousePositionBeforeMove.y, + ), + ), + ); + this.history.update(modelChanges); } } diff --git a/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts b/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts index 129c945cc4..97c0bcc805 100644 --- a/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts +++ b/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts @@ -338,7 +338,6 @@ export abstract class BaseMonomerRenderer extends BaseRenderer { this.rootElement = this.rootElement || this.appendRootElement(this.scale ? this.canvasWrapper : this.canvas); - this.rootElement = this.rootElement || this.appendRootElement(this.canvas); this.bodyElement = this.appendBody(this.rootElement, theme); this.appendEvents(); diff --git a/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer.ts b/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer.ts index 83784b0aca..190c7f5f41 100644 --- a/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer.ts +++ b/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer.ts @@ -23,7 +23,7 @@ export class PolymerBondRenderer extends BaseRenderer { private editorEvents: typeof editorEvents; private selectionElement; private path = ''; - private previousStateOfIsMonomersOnSameHorisontalLine: boolean | undefined; + private previousStateOfIsMonomersOnSameHorisontalLine = false; constructor(public polymerBond: PolymerBond) { super(polymerBond as DrawingEntity); this.polymerBond.setRenderer(this); @@ -154,14 +154,14 @@ export class PolymerBondRenderer extends BaseRenderer { } public isMonomersOnSameHorizontalLine() { - return ( + return Boolean( this.polymerBond.secondMonomer && - this.polymerBond.firstMonomer.position.y - - this.polymerBond.secondMonomer.position.y < - 0.5 && - this.polymerBond.firstMonomer.position.y - - this.polymerBond.secondMonomer.position.y > - -0.5 + this.polymerBond.firstMonomer.position.y - + this.polymerBond.secondMonomer.position.y < + 0.5 && + this.polymerBond.firstMonomer.position.y - + this.polymerBond.secondMonomer.position.y > + -0.5, ); } diff --git a/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts b/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts index e2cefa1260..e1e7162ab2 100644 --- a/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts +++ b/packages/ketcher-core/src/application/render/renderers/RenderersManager.ts @@ -29,6 +29,7 @@ export class RenderersManager { public moveDrawingEntity(drawingEntity: DrawingEntity) { assert(drawingEntity.baseRenderer); drawingEntity.baseRenderer.moveSelection(); + drawingEntity.baseRenderer.drawSelection(); } public addMonomer(monomer: BaseMonomer, callback?: () => void) { @@ -85,19 +86,22 @@ export class RenderersManager { polymerBond.renderer?.remove(); polymerBond?.firstMonomer?.renderer?.redrawAttachmentPoints(); polymerBond?.secondMonomer?.renderer?.redrawAttachmentPoints(); - this.monomers.delete(polymerBond.id); + this.polymerBonds.delete(polymerBond.id); } public finishPolymerBondCreation(polymerBond) { assert(polymerBond.secondMonomer); - polymerBond.renderer?.moveSelection(); - polymerBond.renderer?.redrawHover(); + + const polymerBondRenderer = new PolymerBondRenderer(polymerBond); + this.polymerBonds.set(polymerBond.id, polymerBondRenderer); polymerBond.firstMonomer.renderer?.redrawAttachmentPoints(); polymerBond.firstMonomer.renderer?.drawSelection(); polymerBond.firstMonomer.renderer?.redrawHover(); polymerBond.secondMonomer.renderer?.redrawAttachmentPoints(); polymerBond.secondMonomer.renderer?.drawSelection(); polymerBond.secondMonomer.renderer?.redrawHover(); + + polymerBond.renderer?.show(); } public cancelPolymerBondCreation(polymerBond, secondMonomer) { @@ -124,8 +128,6 @@ export class RenderersManager { } public update(modelChanges: Command) { - modelChanges.operations.forEach((modelChange) => { - modelChange.execute(this); - }); + modelChanges.execute(this); } } diff --git a/packages/ketcher-core/src/domain/entities/Command.ts b/packages/ketcher-core/src/domain/entities/Command.ts index 3e1e1d38f1..fd7b8064d1 100644 --- a/packages/ketcher-core/src/domain/entities/Command.ts +++ b/packages/ketcher-core/src/domain/entities/Command.ts @@ -10,4 +10,14 @@ export class Command { public merge(command: Command) { this.operations = [...this.operations, ...command.operations]; } + + public invert(renderersManagers) { + this.operations.forEach((operation) => operation.invert(renderersManagers)); + } + + public execute(renderersManagers) { + this.operations.forEach((operation) => + operation.execute(renderersManagers), + ); + } } diff --git a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts index d17f1448bc..1299358c45 100644 --- a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts +++ b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts @@ -82,29 +82,46 @@ export class DrawingEntitiesManager { public deleteAllEntities() { const mergedCommand = new Command(); this.allEntities.forEach(([, drawingEntity]) => { - const command = this.deleteDrawingEntity(drawingEntity); + const command = this.deleteDrawingEntity(drawingEntity, false); mergedCommand.merge(command); }); return mergedCommand; } - public addMonomer(monomerItem: MonomerItemType, position: Vec2) { + public addMonomerChangeModel( + monomerItem: MonomerItemType, + position: Vec2, + _monomer?: BaseMonomer, + ) { + if (_monomer) { + this.monomers.set(_monomer.id, _monomer); + return _monomer; + } const [Monomer] = monomerFactory(monomerItem); const monomer = new Monomer(monomerItem, position); monomer.moveAbsolute(position); this.monomers.set(monomer.id, monomer); + return monomer; + } + public addMonomer(monomerItem: MonomerItemType, position: Vec2) { const command = new Command(); - const operation = new MonomerAddOperation(monomer); + const operation = new MonomerAddOperation( + this.addMonomerChangeModel.bind(this, monomerItem, position), + this.deleteMonomerChangeModel.bind(this), + ); command.addOperation(operation); return command; } - public deleteDrawingEntity(drawingEntity: DrawingEntity) { + public deleteDrawingEntity( + drawingEntity: DrawingEntity, + needToDeleteConnectedEntities = true, + ) { if (drawingEntity instanceof BaseMonomer) { - return this.deleteMonomer(drawingEntity); + return this.deleteMonomer(drawingEntity, needToDeleteConnectedEntities); } else if (drawingEntity instanceof PolymerBond) { return this.deletePolymerBond(drawingEntity); } else { @@ -144,13 +161,35 @@ export class DrawingEntitiesManager { return command; } - public moveSelectedDrawingEntities(offset: Vec2) { + public moveDrawingEntityModelChange( + drawingEntity: DrawingEntity, + offset?: Vec2, + ) { + if (drawingEntity instanceof PolymerBond) { + drawingEntity.moveToLinkedMonomers(); + } else { + assert(offset); + drawingEntity.moveRelative(offset); + } + + return drawingEntity; + } + + public moveSelectedDrawingEntities( + partOfMovementOffset: Vec2, + fullMovementOffset?: Vec2, + ) { const command = new Command(); this.monomers.forEach((drawingEntity) => { if (drawingEntity.selected) { - drawingEntity.moveRelative(offset); - command.merge(this.createDrawingEntityMovingCommand(drawingEntity)); + command.merge( + this.createDrawingEntityMovingCommand( + drawingEntity, + partOfMovementOffset, + fullMovementOffset, + ), + ); } }); @@ -160,38 +199,86 @@ export class DrawingEntitiesManager { drawingEntity.firstMonomer.selected || drawingEntity.secondMonomer?.selected ) { - drawingEntity.moveToLinkedMonomers(); - command.merge(this.createDrawingEntityMovingCommand(drawingEntity)); + command.merge( + this.createDrawingEntityMovingCommand( + drawingEntity, + partOfMovementOffset, + fullMovementOffset, + ), + ); } }); return command; } - public createDrawingEntityMovingCommand(drawingEntity: DrawingEntity) { + public createDrawingEntityMovingCommand( + drawingEntity: DrawingEntity, + partOfMovementOffset: Vec2, + fullMovementOffset?: Vec2, + ) { const command = new Command(); - const movingCommand = new DrawingEntityMoveOperation(drawingEntity); + const movingCommand = new DrawingEntityMoveOperation( + this.moveDrawingEntityModelChange.bind( + this, + drawingEntity, + partOfMovementOffset, + ), + this.moveDrawingEntityModelChange.bind( + this, + drawingEntity, + fullMovementOffset + ? fullMovementOffset.negated() + : partOfMovementOffset.negated(), + ), + this.moveDrawingEntityModelChange.bind( + this, + drawingEntity, + fullMovementOffset || partOfMovementOffset, + ), + drawingEntity, + ); command.addOperation(movingCommand); return command; } - public createDrawingEntityRedrawCommand(drawingEntity: DrawingEntity) { + public createDrawingEntityRedrawCommand( + drawingEntityRedrawModelChange: () => DrawingEntity, + invertDrawingEntityRedrawModelChange: () => DrawingEntity, + ) { const command = new Command(); - const redrawCommand = new DrawingEntityRedrawOperation(drawingEntity); + const redrawCommand = new DrawingEntityRedrawOperation( + drawingEntityRedrawModelChange, + invertDrawingEntityRedrawModelChange, + ); command.addOperation(redrawCommand); return command; } - public deleteMonomer(monomer: BaseMonomer) { + private deleteMonomerChangeModel(monomer: BaseMonomer) { this.monomers.delete(monomer.id); + } + + public deleteMonomer( + monomer: BaseMonomer, + needToDeleteConnectedBonds = true, + ) { const command = new Command(); - const operation = new MonomerDeleteOperation(monomer); + const operation = new MonomerDeleteOperation( + monomer, + this.addMonomerChangeModel.bind( + this, + monomer.monomerItem, + monomer.position, + ), + this.deleteMonomerChangeModel.bind(this), + ); command.addOperation(operation); - if (monomer.hasBonds) { + if (needToDeleteConnectedBonds && monomer.hasBonds) { monomer.forEachBond((bond) => { command.merge(this.deletePolymerBond(bond)); }); @@ -200,17 +287,6 @@ export class DrawingEntitiesManager { return command; } - public moveMonomer(monomer: BaseMonomer, position: Vec2) { - const command = new Command(); - monomer.moveAbsolute(position); - - const operation = new MonomerMoveOperation(monomer); - - command.addOperation(operation); - - return command; - } - public selectIfLocatedInRectangle( rectangleTopLeftPoint: Vec2, rectangleBottomRightPoint: Vec2, @@ -232,7 +308,17 @@ export class DrawingEntitiesManager { return command; } - public addPolymerBond(firstMonomer, startPosition, endPosition) { + public startPolymerBondCreationChangeModel( + firstMonomer, + startPosition, + endPosition, + _polymerBond?: PolymerBond, + ) { + if (_polymerBond) { + this.polymerBonds.set(_polymerBond.id, _polymerBond); + return _polymerBond; + } + const polymerBond = new PolymerBond(firstMonomer); this.polymerBonds.set(polymerBond.id, polymerBond); // If we started from a specific AP, we need to 'attach' the bond to the first monomer @@ -243,24 +329,36 @@ export class DrawingEntitiesManager { } polymerBond.moveBondStartAbsolute(startPosition.x, startPosition.y); polymerBond.moveBondEndAbsolute(endPosition.x, endPosition.y); + return polymerBond; + } + public startPolymerBondCreation(firstMonomer, startPosition, endPosition) { const command = new Command(); - const operation = new PolymerBondAddOperation(polymerBond); + + const operation = new PolymerBondAddOperation( + this.startPolymerBondCreationChangeModel.bind( + this, + firstMonomer, + startPosition, + endPosition, + ), + this.deletePolymerBondChangeModel.bind(this), + ); command.addOperation(operation); - return { command, polymerBond }; + return { command, polymerBond: operation.polymerBond }; } - public deletePolymerBond(polymerBond: PolymerBond) { + public deletePolymerBondChangeModel(polymerBond: PolymerBond) { this.polymerBonds.delete(polymerBond.id); - const command = new Command(); + const firstMonomerAttachmentPoint = polymerBond.firstMonomer.getAttachmentPointByBond(polymerBond); const secondMonomerAttachmentPoint = polymerBond.secondMonomer?.getAttachmentPointByBond(polymerBond); - polymerBond.firstMonomer.removePotentialBonds(true); - polymerBond.secondMonomer?.removePotentialBonds(true); + polymerBond.firstMonomer.removePotentialBonds(); + polymerBond.secondMonomer?.removePotentialBonds(); polymerBond.firstMonomer.turnOffSelection(); polymerBond.secondMonomer?.turnOffSelection(); if (firstMonomerAttachmentPoint) { @@ -269,7 +367,26 @@ export class DrawingEntitiesManager { if (secondMonomerAttachmentPoint) { polymerBond.secondMonomer?.unsetBond(secondMonomerAttachmentPoint); } - const operation = new PolymerBondDeleteOperation(polymerBond); + } + + public deletePolymerBond(polymerBond: PolymerBond) { + const command = new Command(); + + const operation = new PolymerBondDeleteOperation( + polymerBond, + this.deletePolymerBondChangeModel.bind(this, polymerBond), + this.finishPolymerBondCreationModelChange.bind( + this, + polymerBond.firstMonomer, + polymerBond.secondMonomer as BaseMonomer, + polymerBond.firstMonomer.getAttachmentPointByBond( + polymerBond, + ) as string, + polymerBond.secondMonomer?.getAttachmentPointByBond( + polymerBond, + ) as string, + ), + ); command.addOperation(operation); return command; @@ -310,38 +427,88 @@ export class DrawingEntitiesManager { return command; } - public finishPolymerBondCreation( - polymerBond: PolymerBond, + public finishPolymerBondCreationModelChange( + firstMonomer: BaseMonomer, secondMonomer: BaseMonomer, firstMonomerAttachmentPoint: string, secondMonomerAttachmentPoint: string, + _polymerBond?: PolymerBond, ) { - const command = new Command(); + if (_polymerBond) { + this.polymerBonds.set(_polymerBond.id, _polymerBond); + firstMonomer.setBond(firstMonomerAttachmentPoint, _polymerBond); + secondMonomer.setBond(secondMonomerAttachmentPoint, _polymerBond); + return _polymerBond; + } + const polymerBond = new PolymerBond(firstMonomer); + this.polymerBonds.set(polymerBond.id, polymerBond); polymerBond.setSecondMonomer(secondMonomer); - polymerBond.firstMonomer.removeBond(polymerBond); polymerBond.firstMonomer.setBond(firstMonomerAttachmentPoint, polymerBond); assert(polymerBond.secondMonomer); polymerBond.secondMonomer.setBond( secondMonomerAttachmentPoint, polymerBond, ); - polymerBond.firstMonomer.removePotentialBonds(true); - polymerBond.secondMonomer.removePotentialBonds(true); + polymerBond.firstMonomer.removePotentialBonds(); + polymerBond.secondMonomer.removePotentialBonds(); polymerBond.moveToLinkedMonomers(); - polymerBond.firstMonomer.turnOffSelection(); polymerBond.firstMonomer.turnOffHover(); polymerBond.firstMonomer.turnOffAttachmentPointsVisibility(); - polymerBond.secondMonomer.turnOffSelection(); polymerBond.secondMonomer.turnOffHover(); polymerBond.secondMonomer.turnOffAttachmentPointsVisibility(); - polymerBond.turnOffHover(); - const operation = new PolymerBondFinishCreationOperation(polymerBond); + return polymerBond; + } + + public finishPolymerBondCreation( + polymerBond: PolymerBond, + secondMonomer: BaseMonomer, + firstMonomerAttachmentPoint: string, + secondMonomerAttachmentPoint: string, + ) { + const command = new Command(); + + const firstMonomer = polymerBond.firstMonomer; + this.polymerBonds.delete(polymerBond.id); + const operation = new PolymerBondFinishCreationOperation( + this.finishPolymerBondCreationModelChange.bind( + this, + firstMonomer, + secondMonomer, + firstMonomerAttachmentPoint, + secondMonomerAttachmentPoint, + ), + this.deletePolymerBondChangeModel.bind(this), + ); + + command.addOperation(operation); + + return command; + } + + public createPolymerBond( + firstMonomer: BaseMonomer, + secondMonomer: BaseMonomer, + firstMonomerAttachmentPoint: string, + secondMonomerAttachmentPoint: string, + ) { + const command = new Command(); + + const operation = new PolymerBondFinishCreationOperation( + this.finishPolymerBondCreationModelChange.bind( + this, + firstMonomer, + secondMonomer, + firstMonomerAttachmentPoint, + secondMonomerAttachmentPoint, + ), + this.deletePolymerBondChangeModel.bind(this), + ); command.addOperation(operation); @@ -568,19 +735,13 @@ export class DrawingEntitiesManager { let previousMonomer: BaseMonomer | undefined; monomersToAdd.forEach(([monomerItem, monomerPosition]) => { - const [Monomer] = monomerFactory(monomerItem); - const monomer = new Monomer(monomerItem, monomerPosition); - this.monomers.set(monomer.id, monomer); - let monomerAddOperation; + const monomerAddOperation = new MonomerAddOperation( + this.addMonomerChangeModel.bind(this, monomerItem, monomerPosition), + this.deleteMonomerChangeModel.bind(this), + ); + const monomer = monomerAddOperation.monomer; + command.addOperation(monomerAddOperation); if (previousMonomer) { - const polymerBond = new PolymerBond(previousMonomer); - this.polymerBonds.set(polymerBond.id, polymerBond); - monomerAddOperation = new MonomerAddOperation(monomer, () => { - polymerBond.moveToLinkedMonomers(); - }); - command.addOperation(monomerAddOperation); - polymerBond.setSecondMonomer(monomer); - // requirements are: Base(R1)-(R3)Sugar(R2)-(R1)Phosphate const attPointStart = previousMonomer.getValidSourcePoint(monomer); const attPointEnd = monomer.getValidSourcePoint(previousMonomer); @@ -588,15 +749,18 @@ export class DrawingEntitiesManager { assert(attPointStart); assert(attPointEnd); - previousMonomer.setBond(attPointStart, polymerBond); - monomer.setBond(attPointEnd, polymerBond); - const operation = new PolymerBondAddOperation(polymerBond); + const operation = new PolymerBondFinishCreationOperation( + this.finishPolymerBondCreationModelChange.bind( + this, + previousMonomer, + monomer, + attPointStart, + attPointEnd, + ), + this.deletePolymerBondChangeModel.bind(this), + ); command.addOperation(operation); - } else { - monomerAddOperation = new MonomerAddOperation(monomer); - command.addOperation(monomerAddOperation); } - previousMonomer = monomer; }); @@ -624,6 +788,12 @@ export class DrawingEntitiesManager { return monomerChain; } + private rearrangeChainModelChange(monomer: BaseMonomer, newPosition: Vec2) { + monomer.moveAbsolute(newPosition); + + return monomer; + } + private rearrangeChain( monomer: BaseMonomer, initialPosition: Vec2, @@ -644,8 +814,15 @@ export class DrawingEntitiesManager { initialPosition.y + heightMonomerWithBond, ) : initialPosition; - monomer.moveAbsolute(Scale.canvasToModel(newPosition, editorSettings)); - const operation = new MonomerMoveOperation(monomer); + + const operation = new MonomerMoveOperation( + this.rearrangeChainModelChange.bind( + this, + monomer, + Scale.canvasToModel(newPosition, editorSettings), + ), + this.rearrangeChainModelChange.bind(this, monomer, oldMonomerPosition), + ); command.addOperation(operation); let lastPosition = newPosition; @@ -725,12 +902,22 @@ export class DrawingEntitiesManager { return command; } + private redrawBondsModelChange(polymerBond: PolymerBond) { + polymerBond.moveToLinkedMonomers(); + + return polymerBond; + } + public redrawBonds() { const command = new Command(); - this.polymerBonds.forEach((drawingEntity) => { - drawingEntity.moveToLinkedMonomers(); - command.merge(this.createDrawingEntityRedrawCommand(drawingEntity)); + this.polymerBonds.forEach((polymerBond) => { + command.merge( + this.createDrawingEntityRedrawCommand( + this.redrawBondsModelChange.bind(this, polymerBond), + this.redrawBondsModelChange.bind(this, polymerBond), + ), + ); }); return command; } diff --git a/packages/ketcher-core/src/domain/entities/Operation.ts b/packages/ketcher-core/src/domain/entities/Operation.ts index 7ce32fffaa..97c37e2a42 100644 --- a/packages/ketcher-core/src/domain/entities/Operation.ts +++ b/packages/ketcher-core/src/domain/entities/Operation.ts @@ -1,7 +1,10 @@ import { RenderersManager } from 'application/render/renderers/RenderersManager'; import { BaseMonomer } from 'domain/entities/BaseMonomer'; +import { PolymerBond } from 'domain/entities/PolymerBond'; export interface Operation { monomer?: BaseMonomer; + polymerBond?: PolymerBond; execute(renderersManager: RenderersManager): void; + invert(renderersManager: RenderersManager): void; } diff --git a/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts b/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts index 5dce39c450..f684b399ff 100644 --- a/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts +++ b/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts @@ -19,19 +19,13 @@ export function polymerBondToDrawingEntity( Number(monomerIdsMap[connection.endpoint2.monomerId]), ); + assert(firstMonomer); assert(secondMonomer); assert(connection.endpoint1.attachmentPointId); assert(connection.endpoint2.attachmentPointId); - const { command: bondAdditionCommand, polymerBond } = - drawingEntitiesManager.addPolymerBond( - firstMonomer, - firstMonomer?.position, - secondMonomer?.position, - ); - command.merge(bondAdditionCommand); command.merge( - drawingEntitiesManager.finishPolymerBondCreation( - polymerBond, + drawingEntitiesManager.createPolymerBond( + firstMonomer, secondMonomer, connection.endpoint1.attachmentPointId, connection.endpoint2.attachmentPointId, diff --git a/packages/ketcher-polymer-editor-react/src/Editor.tsx b/packages/ketcher-polymer-editor-react/src/Editor.tsx index 03879315bd..6d47026022 100644 --- a/packages/ketcher-polymer-editor-react/src/Editor.tsx +++ b/packages/ketcher-polymer-editor-react/src/Editor.tsx @@ -303,6 +303,8 @@ function MenuComponent() { } else if (name === 'snake-mode') { dispatch(selectMode(!isSnakeMode)); editor.events.selectMode.dispatch(!isSnakeMode); + } else if (name === 'undo' || name === 'redo') { + editor.events.selectHistory.dispatch(name); } else if (!['zoom-in', 'zoom-out', 'zoom-reset'].includes(name)) { editor.events.selectTool.dispatch(name); if (name === 'clear') { @@ -323,6 +325,10 @@ function MenuComponent() { testId="clear-canvas-button" /> + + + + diff --git a/packages/ketcher-polymer-editor-react/src/components/modal/Open/Open.tsx b/packages/ketcher-polymer-editor-react/src/components/modal/Open/Open.tsx index 594c02afae..a942704563 100644 --- a/packages/ketcher-polymer-editor-react/src/components/modal/Open/Open.tsx +++ b/packages/ketcher-polymer-editor-react/src/components/modal/Open/Open.tsx @@ -26,6 +26,7 @@ import { identifyStructFormat, CoreEditor, KetcherLogger, + EditorHistory, } from 'ketcher-core'; import { IndigoProvider } from 'ketcher-react'; import assert from 'assert'; @@ -64,6 +65,8 @@ const addToCanvas = ({ deserialisedKet.drawingEntitiesManager.mergeInto( editor.drawingEntitiesManager, ); + const editorHistory = new EditorHistory(editor); + editorHistory.update(deserialisedKet.modelChanges); editor.renderersContainer.update(deserialisedKet.modelChanges); };