diff --git a/ketcher-autotests/tests/Macromolecule-editor/Ket/ket-deserialize.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/Ket/ket-deserialize.spec.ts new file mode 100644 index 0000000000..b7f77ee6a2 --- /dev/null +++ b/ketcher-autotests/tests/Macromolecule-editor/Ket/ket-deserialize.spec.ts @@ -0,0 +1,18 @@ +import { test } from '@playwright/test'; +import { openFileAndAddToCanvas, takeEditorScreenshot } from '@utils'; +import { turnOnMacromoleculesEditor } from '@utils/macromolecules'; + +test.describe('Ket Deserialize', () => { + test.beforeEach(async ({ page }) => { + await page.goto(''); + await turnOnMacromoleculesEditor(page); + }); + test('Open ket file with monomers and bonds', async ({ page }) => { + /* + Test case: #3230 - Support parsing KET file for macromolecules on ketcher side + Description: Ket Deserialize + */ + await openFileAndAddToCanvas('KET/monomers-with-bonds.ket', page); + await takeEditorScreenshot(page); + }); +}); diff --git a/ketcher-autotests/tests/Macromolecule-editor/Ket/ket-deserialize.spec.ts-snapshots/Ket-Deserialize-Open-ket-file-with-monomers-and-bonds-1-chromium-darwin.png b/ketcher-autotests/tests/Macromolecule-editor/Ket/ket-deserialize.spec.ts-snapshots/Ket-Deserialize-Open-ket-file-with-monomers-and-bonds-1-chromium-darwin.png new file mode 100644 index 0000000000..f221d8adbf Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/Ket/ket-deserialize.spec.ts-snapshots/Ket-Deserialize-Open-ket-file-with-monomers-and-bonds-1-chromium-darwin.png differ diff --git a/ketcher-autotests/tests/test-data/KET/monomers-with-bonds.ket b/ketcher-autotests/tests/test-data/KET/monomers-with-bonds.ket new file mode 100644 index 0000000000..2e525687b3 --- /dev/null +++ b/ketcher-autotests/tests/test-data/KET/monomers-with-bonds.ket @@ -0,0 +1,406 @@ +{ + "root": { + "nodes": [ + { + "type": "monomer", + "id": "1", + "position": { + "x": 2.5, + "y": 2.5 + }, + "templateId": "A" + }, + { + "type": "monomer", + "id": "2", + "position": { + "x": 6.25, + "y": 2.5 + }, + "templateId": "A" + }, + { + "type": "monomer", + "id": "3", + "position": { + "x": 10, + "y": 2.5 + }, + "templateId": "A" + }, + { + "type": "monomer", + "id": "4", + "position": { + "x": 13.75, + "y": 2.5 + }, + "templateId": "B" + }, + { + "type": "monomer", + "id": "5", + "position": { + "x": 13.75, + "y": 6.25 + }, + "templateId": "B" + }, + { + "type": "monomer", + "id": "6", + "position": { + "x": 17.5, + "y": 6.25 + }, + "templateId": "B" + } + ], + "connections": [ + { + "connectionType": "single", + "endPoint1": { + "monomerId": "2", + "attachmentPointId": "R1" + }, + "endPoint2": { + "monomerId": "1", + "attachmentPointId": "R2" + } + }, + { + "connectionType": "single", + "endPoint1": { + "monomerId": "3", + "attachmentPointId": "R1" + }, + "endPoint2": { + "monomerId": "2", + "attachmentPointId": "R2" + } + }, + { + "connectionType": "single", + "endPoint1": { + "monomerId": "4", + "attachmentPointId": "R1" + }, + "endPoint2": { + "monomerId": "3", + "attachmentPointId": "R2" + } + }, + { + "connectionType": "single", + "endPoint1": { + "monomerId": "5", + "attachmentPointId": "R1" + }, + "endPoint2": { + "monomerId": "4", + "attachmentPointId": "R2" + } + }, + { + "connectionType": "single", + "endPoint1": { + "monomerId": "6", + "attachmentPointId": "R1" + }, + "endPoint2": { + "monomerId": "5", + "attachmentPointId": "R2" + } + } + ], + "templates": [ + { + "type": "monomerTemplate", + "id": "A", + "alias": "Ala", + "naturalAnalog": "Ala", + "naturalAnalogShort": "A", + "monomerClass": "PEPTIDE", + "monomerSubClass": "AminoAcid", +"root": { + "nodes": [ + { + "$ref": "mol0" + } + ] + }, + "mol0": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 10.809849152128567, + -7.775074417174608, + 0 + ] + }, + { + "label": "C", + "location": [ + 12.540150847871434, + -7.774589229177203, + 0 + ] + }, + { + "label": "N", + "location": [ + 11.67663750949124, + -7.274966888850187, + 0 + ] + }, + { + "label": "C", + "location": [ + 12.540150847871434, + -8.775532067822148, + 0 + ] + }, + { + "label": "C", + "location": [ + 10.809849152128567, + -8.780020056798138, + 0 + ] + }, + { + "label": "C", + "location": [ + 11.67882085547956, + -9.275033111149813, + 0 + ] + } + ], + "bonds": [ + { + "type": 2, + "atoms": [ + 2, + 0 + ] + }, + { + "type": 2, + "atoms": [ + 3, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 2 + ] + }, + { + "type": 2, + "atoms": [ + 4, + 5 + ] + }, + { + "type": 1, + "atoms": [ + 5, + 3 + ] + } + ], + "sgroups": [ + { + "type": "DAT", + "atoms": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "context": "Fragment", + "fieldName": "A", + "fieldData": "Test", + "bonds": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + } + ] + } + + }, + { + "type": "monomerTemplate", + "id": "B", + "alias": "Pho", + "naturalAnalog": "Pho", + "naturalAnalogShort": "P", + "monomerClass": "RNA", + "monomerSubClass": "Phosphate", +"root": { + "nodes": [ + { + "$ref": "mol0" + } + ] + }, + "mol0": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 10.809849152128567, + -7.775074417174608, + 0 + ] + }, + { + "label": "C", + "location": [ + 12.540150847871434, + -7.774589229177203, + 0 + ] + }, + { + "label": "N", + "location": [ + 11.67663750949124, + -7.274966888850187, + 0 + ] + }, + { + "label": "C", + "location": [ + 12.540150847871434, + -8.775532067822148, + 0 + ] + }, + { + "label": "C", + "location": [ + 10.809849152128567, + -8.780020056798138, + 0 + ] + }, + { + "label": "C", + "location": [ + 11.67882085547956, + -9.275033111149813, + 0 + ] + } + ], + "bonds": [ + { + "type": 2, + "atoms": [ + 2, + 0 + ] + }, + { + "type": 2, + "atoms": [ + 3, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 2 + ] + }, + { + "type": 2, + "atoms": [ + 4, + 5 + ] + }, + { + "type": 1, + "atoms": [ + 5, + 3 + ] + } + ], + "sgroups": [ + { + "type": "DAT", + "atoms": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "context": "Fragment", + "fieldName": "A", + "fieldData": "Test", + "bonds": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + } + ] + } + } + ] + } + +} + + + + + + + + + + + diff --git a/packages/ketcher-core/src/application/editor/Editor.ts b/packages/ketcher-core/src/application/editor/Editor.ts index eadb58a25e..bb7aa0cf02 100644 --- a/packages/ketcher-core/src/application/editor/Editor.ts +++ b/packages/ketcher-core/src/application/editor/Editor.ts @@ -23,6 +23,8 @@ function isMouseMainButtonPressed(event: MouseEvent) { return event.button === 0; } +let editor; + export class CoreEditor { public events = editorEvents; @@ -43,6 +45,12 @@ export class CoreEditor { this.drawingEntitiesManager = new DrawingEntitiesManager(); this.domEventSetup(); this.canvasOffset = this.canvas.getBoundingClientRect(); + // eslint-disable-next-line @typescript-eslint/no-this-alias + editor = this; + } + + static provideEditorInstance(): CoreEditor { + return editor; } private subscribeEvents() { diff --git a/packages/ketcher-core/src/application/editor/editorSettings.ts b/packages/ketcher-core/src/application/editor/editorSettings.ts new file mode 100644 index 0000000000..f1b2ec60c3 --- /dev/null +++ b/packages/ketcher-core/src/application/editor/editorSettings.ts @@ -0,0 +1,7 @@ +const editorSettings = { + scale: 40, // Angstroms To Pixels Factor +}; + +export function provideEditorSettings() { + return editorSettings; +} diff --git a/packages/ketcher-core/src/application/editor/tools/Monomer.ts b/packages/ketcher-core/src/application/editor/tools/Monomer.ts index b8a23288b4..a50718068f 100644 --- a/packages/ketcher-core/src/application/editor/tools/Monomer.ts +++ b/packages/ketcher-core/src/application/editor/tools/Monomer.ts @@ -25,6 +25,8 @@ import { BaseMonomerRenderer } from 'application/render/renderers'; import { MonomerItemType } from 'domain/types'; import { monomerFactory } from '../operations/monomer/monomerFactory'; import assert from 'assert'; +import { provideEditorSettings } from 'application/editor/editorSettings'; +import { Scale } from 'domain/helpers'; class MonomerTool implements Tool { private monomerPreview: @@ -46,14 +48,17 @@ class MonomerTool implements Tool { mousedown() { assert(this.monomerPreviewRenderer); - + const editorSettings = provideEditorSettings(); const modelChanges = this.editor.drawingEntitiesManager.addMonomer( this.monomer, - new Vec2( - this.editor.lastCursorPosition.x - - this.monomerPreviewRenderer.width / 2, - this.editor.lastCursorPosition.y - - this.monomerPreviewRenderer.height / 2, + Scale.scaled2obj( + new Vec2( + this.editor.lastCursorPosition.x - + this.monomerPreviewRenderer.width / 2, + this.editor.lastCursorPosition.y - + this.monomerPreviewRenderer.height / 2, + ), + editorSettings, ), ); @@ -61,10 +66,14 @@ class MonomerTool implements Tool { } mousemove() { + const editorSettings = provideEditorSettings(); this.monomerPreview?.moveAbsolute( - new Vec2( - this.editor.lastCursorPosition.x + this.MONOMER_PREVIEW_OFFSET_X, - this.editor.lastCursorPosition.y + this.MONOMER_PREVIEW_OFFSET_Y, + Scale.scaled2obj( + new Vec2( + this.editor.lastCursorPosition.x + this.MONOMER_PREVIEW_OFFSET_X, + this.editor.lastCursorPosition.y + this.MONOMER_PREVIEW_OFFSET_Y, + ), + editorSettings, ), ); this.monomerPreviewRenderer?.move(); diff --git a/packages/ketcher-core/src/application/editor/tools/RnaPreset.ts b/packages/ketcher-core/src/application/editor/tools/RnaPreset.ts index f60b66f92d..b216c9b294 100644 --- a/packages/ketcher-core/src/application/editor/tools/RnaPreset.ts +++ b/packages/ketcher-core/src/application/editor/tools/RnaPreset.ts @@ -24,6 +24,8 @@ import { monomerFactory } from '../operations/monomer/monomerFactory'; import { RNABase } from 'domain/entities/RNABase'; import { Phosphate } from 'domain/entities/Phosphate'; import assert from 'assert'; +import { provideEditorSettings } from 'application/editor/editorSettings'; +import { Scale } from 'domain/helpers'; class RnaPresetTool implements Tool { rnaBase: MonomerItemType | undefined; @@ -53,38 +55,50 @@ class RnaPresetTool implements Tool { } mousedown() { + const editorSettings = provideEditorSettings(); + assert( this.sugarPreviewRenderer, 'monomerPreviewRenderer is not initialized', ); assert(this.sugar, 'no sugar in preset'); - const modelChanges = this.editor.drawingEntitiesManager.addRnaPreset({ sugar: this.sugar, - sugarPosition: new Vec2( - this.editor.lastCursorPosition.x - this.sugarPreviewRenderer.width / 2, - this.editor.lastCursorPosition.y - this.sugarPreviewRenderer.height / 2, + sugarPosition: Scale.scaled2obj( + new Vec2( + this.editor.lastCursorPosition.x - + this.sugarPreviewRenderer.width / 2, + this.editor.lastCursorPosition.y - + this.sugarPreviewRenderer.height / 2, + ), + editorSettings, ), phosphate: this.phosphate, phosphatePosition: this.phosphatePreviewRenderer - ? new Vec2( - this.editor.lastCursorPosition.x - - this.phosphatePreviewRenderer.width / 2 + - this.sugarPreviewRenderer?.width + - 30, - this.editor.lastCursorPosition.y - - this.phosphatePreviewRenderer.height / 2, + ? Scale.scaled2obj( + new Vec2( + this.editor.lastCursorPosition.x - + this.phosphatePreviewRenderer.width / 2 + + this.sugarPreviewRenderer?.width + + 30, + this.editor.lastCursorPosition.y - + this.phosphatePreviewRenderer.height / 2, + ), + editorSettings, ) : undefined, rnaBase: this.rnaBase, rnaBasePosition: this.rnaBasePreviewRenderer - ? new Vec2( - this.editor.lastCursorPosition.x - - this.rnaBasePreviewRenderer.width / 2, - this.editor.lastCursorPosition.y - - this.rnaBasePreviewRenderer.height / 2 + - this.sugarPreviewRenderer.height + - 30, + ? Scale.scaled2obj( + new Vec2( + this.editor.lastCursorPosition.x - + this.rnaBasePreviewRenderer.width / 2, + this.editor.lastCursorPosition.y - + this.rnaBasePreviewRenderer.height / 2 + + this.sugarPreviewRenderer.height + + 30, + ), + editorSettings, ) : undefined, }); @@ -93,24 +107,35 @@ class RnaPresetTool implements Tool { } mousemove() { + const editorSettings = provideEditorSettings(); + this.sugarPreview?.moveAbsolute( - new Vec2( - this.editor.lastCursorPosition.x + this.MONOMER_PREVIEW_OFFSET_X, - this.editor.lastCursorPosition.y + this.MONOMER_PREVIEW_OFFSET_Y, + Scale.scaled2obj( + new Vec2( + this.editor.lastCursorPosition.x + this.MONOMER_PREVIEW_OFFSET_X, + this.editor.lastCursorPosition.y + this.MONOMER_PREVIEW_OFFSET_Y, + ), + editorSettings, ), ); this.rnaBasePreview?.moveAbsolute( - new Vec2( - this.editor.lastCursorPosition.x + this.MONOMER_PREVIEW_OFFSET_X, - this.editor.lastCursorPosition.y + this.MONOMER_PREVIEW_OFFSET_Y + 18, + Scale.scaled2obj( + new Vec2( + this.editor.lastCursorPosition.x + this.MONOMER_PREVIEW_OFFSET_X, + this.editor.lastCursorPosition.y + this.MONOMER_PREVIEW_OFFSET_Y + 18, + ), + editorSettings, ), ); this.phosphatePreview?.moveAbsolute( - new Vec2( - this.editor.lastCursorPosition.x + this.MONOMER_PREVIEW_OFFSET_X + 18, - this.editor.lastCursorPosition.y + this.MONOMER_PREVIEW_OFFSET_Y, + Scale.scaled2obj( + new Vec2( + this.editor.lastCursorPosition.x + this.MONOMER_PREVIEW_OFFSET_X + 18, + this.editor.lastCursorPosition.y + this.MONOMER_PREVIEW_OFFSET_Y, + ), + editorSettings, ), ); diff --git a/packages/ketcher-core/src/application/formatters/ketFormatter.ts b/packages/ketcher-core/src/application/formatters/ketFormatter.ts index 6da162f207..e8fb273c03 100644 --- a/packages/ketcher-core/src/application/formatters/ketFormatter.ts +++ b/packages/ketcher-core/src/application/formatters/ketFormatter.ts @@ -33,4 +33,8 @@ export class KetFormatter implements StructFormatter { async getStructureFromStringAsync(content: string): Promise { return this.#ketSerializer.deserialize(content); } + + parseMacromoleculeString(content: string): void { + this.#ketSerializer.deserializeMacromolecule(content); + } } diff --git a/packages/ketcher-core/src/application/formatters/structFormatter.types.ts b/packages/ketcher-core/src/application/formatters/structFormatter.types.ts index ab3c54e92a..23a6a48337 100644 --- a/packages/ketcher-core/src/application/formatters/structFormatter.types.ts +++ b/packages/ketcher-core/src/application/formatters/structFormatter.types.ts @@ -21,6 +21,7 @@ import { StructServiceOptions } from 'domain/services'; export interface StructFormatter { getStructureFromStructAsync: (struct: Struct) => Promise; getStructureFromStringAsync: (stringifiedStruct: string) => Promise; + parseMacromoleculeString?: (stringifiedStruct: string) => void; } export enum SupportedFormat { diff --git a/packages/ketcher-core/src/application/formatters/types/ket.ts b/packages/ketcher-core/src/application/formatters/types/ket.ts new file mode 100644 index 0000000000..1f7d88132c --- /dev/null +++ b/packages/ketcher-core/src/application/formatters/types/ket.ts @@ -0,0 +1,58 @@ +export interface IKetMonomerNode { + type: 'monomer'; + id: string; + seqid?: number; + position: { + x: number; + y: number; + }; + alias?: string; + templateId: string; +} + +export interface IKetGroupNode { + type: 'group'; +} + +export type KetNode = IKetMonomerNode | IKetGroupNode; + +export interface IKetConnectionEndPoint { + monomerId: string; + attachmentPointId: string; + groupId: string; +} + +export interface IKetConnection { + connectionType: 'single' | 'hydrogen'; + label?: string; + endPoint1: IKetConnectionEndPoint; + endPoint2: IKetConnectionEndPoint; +} + +export interface IKetMonomerTemplate { + type: 'monomerTemplate'; + monomerClass?: 'RNA' | 'PEPTIDE' | 'CHEM' | 'UNKNOWN'; + monomerSubClass?: + | 'AminoAcid' + | 'Sugar' + | 'Phosphate' + | 'Base' + | 'Terminator' + | 'Linker' + | 'Unknown' + | 'CHEM'; + naturalAnalogShort: string; + id: string; + fullName?: string; + alias?: string; + naturalAnalog?: string; + attachmentPoints?; +} + +export interface IKetMacromoleculesContent { + root: { + nodes: KetNode[]; + connections: IKetConnection[]; + templates: IKetMonomerTemplate[]; + }; +} diff --git a/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts b/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts index 0ee647165b..77d140014d 100644 --- a/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts +++ b/packages/ketcher-core/src/application/render/renderers/BaseMonomerRenderer.ts @@ -4,6 +4,7 @@ import { BaseMonomer } from 'domain/entities/BaseMonomer'; import { D3SvgElementSelection } from 'application/render/types'; import { DrawingEntity } from 'domain/entities/DrawingEntity'; import { editorEvents } from 'application/editor/editorEvents'; +import { Scale } from 'domain/helpers'; export abstract class BaseMonomerRenderer extends BaseRenderer { private editorEvents: typeof editorEvents; @@ -28,8 +29,8 @@ export abstract class BaseMonomerRenderer extends BaseRenderer { public get center() { return { - x: this.monomer.position.x + this.bodyWidth / 2, - y: this.monomer.position.y + this.bodyHeight / 2, + x: this.scaledMonomerPosition.x + this.bodyWidth / 2, + y: this.scaledMonomerPosition.y + this.bodyHeight / 2, }; } @@ -168,8 +169,8 @@ export abstract class BaseMonomerRenderer extends BaseRenderer { .attr('transition', 'transform 0.2s') .attr( 'transform', - `translate(${this.monomer.position.x}, ${ - this.monomer.position.y + `translate(${this.scaledMonomerPosition.x}, ${ + this.scaledMonomerPosition.y }) scale(${this.scale || 1})`, ) as never as D3SvgElementSelection; } @@ -208,6 +209,10 @@ export abstract class BaseMonomerRenderer extends BaseRenderer { this.hoverElement.remove(); } + private get scaledMonomerPosition() { + return Scale.obj2scaled(this.monomer.position, this.editorSettings); + } + public appendSelection() { this.removeSelection(); @@ -220,8 +225,8 @@ export abstract class BaseMonomerRenderer extends BaseRenderer { this.selectionCircle = this.canvas ?.insert('circle', ':first-child') .attr('r', '42px') - .attr('cx', this.monomer.position.x + this.bodyWidth / 2) - .attr('cy', this.monomer.position.y + this.bodyHeight / 2) + .attr('cx', this.scaledMonomerPosition.x + this.bodyWidth / 2) + .attr('cy', this.scaledMonomerPosition.y + this.bodyHeight / 2) .attr('fill', '#57FF8F'); } @@ -281,8 +286,8 @@ export abstract class BaseMonomerRenderer extends BaseRenderer { public move() { this.rootElement?.attr( 'transform', - `translate(${this.monomer.position.x}, ${ - this.monomer.position.y + `translate(${this.scaledMonomerPosition.x}, ${ + this.scaledMonomerPosition.y }) scale(${this.scale || 1})`, ); } diff --git a/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts b/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts index 4fc92a8f4c..a1dfa28b73 100644 --- a/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts +++ b/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts @@ -2,6 +2,7 @@ import { select } from 'd3'; import { DrawingEntity } from 'domain/entities/DrawingEntity'; import assert from 'assert'; import { D3SvgElementSelection } from 'application/render/types'; +import { provideEditorSettings } from 'application/editor/editorSettings'; export interface IBaseRenderer { show(theme): void; @@ -28,6 +29,10 @@ export abstract class BaseRenderer implements IBaseRenderer { this.canvas = select('#polymer-editor-canvas'); } + protected get editorSettings() { + return provideEditorSettings(); + } + public get rootBBox() { const rootNode = this.rootElement?.node(); if (!rootNode) return; diff --git a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts index 58de7a69eb..b57440715f 100644 --- a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts +++ b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts @@ -60,11 +60,11 @@ export class DrawingEntitiesManager { return mergedCommand; } - public addMonomer(monomerItem: MonomerItemType, position: Vec2) { + public addMonomer(monomerItem: MonomerItemType, position: Vec2, id?: number) { const [Monomer] = monomerFactory(monomerItem); const monomer = new Monomer(monomerItem, position); monomer.moveAbsolute(position); - this.monomers.set(monomer.id, monomer); + this.monomers.set(id || monomer.id, monomer); const command = new Command(); const operation = new MonomerAddOperation(monomer); diff --git a/packages/ketcher-core/src/domain/serializers/ket/fromKet/monomerToDrawingEntity.ts b/packages/ketcher-core/src/domain/serializers/ket/fromKet/monomerToDrawingEntity.ts new file mode 100644 index 0000000000..362a9de8e5 --- /dev/null +++ b/packages/ketcher-core/src/domain/serializers/ket/fromKet/monomerToDrawingEntity.ts @@ -0,0 +1,31 @@ +import { + IKetMonomerNode, + IKetMonomerTemplate, +} from 'application/formatters/types/ket'; +import { Struct, Vec2 } from 'domain/entities'; +import { CoreEditor } from 'application/editor'; + +export function monomerToDrawingEntity( + node: IKetMonomerNode, + template: IKetMonomerTemplate, + struct: Struct, +) { + const editor = CoreEditor.provideEditorInstance(); + + return editor.drawingEntitiesManager.addMonomer( + { + struct, + label: template.alias || template.id, + colorScheme: undefined, + favorite: false, + props: { + Name: template.fullName || template.alias || template.id, + MonomerNaturalAnalogCode: template.naturalAnalogShort, + MonomerName: template.fullName || template.alias || template.id, + MonomerType: template.monomerClass, + }, + }, + new Vec2(node.position.x, node.position.y), + Number(node.id), + ); +} diff --git a/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts b/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts new file mode 100644 index 0000000000..eb936e3d8a --- /dev/null +++ b/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts @@ -0,0 +1,34 @@ +import { IKetConnection } from 'application/formatters/types/ket'; +import { CoreEditor } from 'application/editor'; +import { Command } from 'domain/entities/Command'; +import assert from 'assert'; + +export function polymerBondToDrawingEntity(connection: IKetConnection) { + const editor = CoreEditor.provideEditorInstance(); + const command = new Command(); + const firstMonomer = editor.drawingEntitiesManager.monomers.get( + Number(connection.endPoint1.monomerId), + ); + const secondMonomer = editor.drawingEntitiesManager.monomers.get( + Number(connection.endPoint2.monomerId), + ); + + assert(firstMonomer?.renderer); + assert(secondMonomer?.renderer); + const { command: bondAdditionCommand, polymerBond } = + editor.drawingEntitiesManager.addPolymerBond( + firstMonomer, + firstMonomer.renderer.center, + secondMonomer.renderer.center, + ); + command.merge(bondAdditionCommand); + command.merge( + editor.drawingEntitiesManager.finishPolymerBondCreation( + polymerBond, + secondMonomer, + connection.endPoint1.attachmentPointId, + connection.endPoint2.attachmentPointId, + ), + ); + return command; +} diff --git a/packages/ketcher-core/src/domain/serializers/ket/ketSerializer.ts b/packages/ketcher-core/src/domain/serializers/ket/ketSerializer.ts index 77c6a82fa2..fcbe970eb6 100644 --- a/packages/ketcher-core/src/domain/serializers/ket/ketSerializer.ts +++ b/packages/ketcher-core/src/domain/serializers/ket/ketSerializer.ts @@ -30,6 +30,16 @@ import { simpleObjectToStruct } from './fromKet/simpleObjectToStruct'; import { textToKet } from './toKet/textToKet'; import { textToStruct } from './fromKet/textToStruct'; import { validate } from './validate'; +import { + IKetConnection, + IKetMacromoleculesContent, + IKetMonomerNode, +} from 'application/formatters/types/ket'; +import { Command } from 'domain/entities/Command'; +import { CoreEditor } from 'application/editor'; +import { monomerToDrawingEntity } from 'domain/serializers/ket/fromKet/monomerToDrawingEntity'; +import assert from 'assert'; +import { polymerBondToDrawingEntity } from 'domain/serializers/ket/fromKet/polymerBondToDrawingEntity'; function parseNode(node: any, struct: any) { const type = node.type; @@ -70,12 +80,18 @@ function parseNode(node: any, struct: any) { } export class KetSerializer implements Serializer { deserialize(content: string): Struct { - const resultingStruct = new Struct(); const ket = JSON.parse(content); if (!validate(ket)) { throw new Error('Cannot deserialize input JSON.'); } + + return this.fillStruct(ket); + } + + fillStruct(ket) { + const resultingStruct = new Struct(); const nodes = ket.root.nodes; + Object.keys(nodes).forEach((i) => { if (nodes[i].type) parseNode(nodes[i], resultingStruct); else if (nodes[i].$ref) parseNode(ket[nodes[i].$ref], resultingStruct); @@ -136,4 +152,105 @@ export class KetSerializer implements Serializer { return JSON.stringify(result, null, 4); } + + private validateMonomerNodeTemplate( + node: IKetMonomerNode, + parsedFileContent: IKetMacromoleculesContent, + editor: CoreEditor, + ) { + const template = parsedFileContent.root.templates.find( + (template) => template.id === node.templateId, + ); + if (!template) { + editor.events.error.dispatch('Error during file parsing'); + return true; + } + + return false; + } + + private validateConnectionTypeAndEndpoints( + connection: IKetConnection, + editor: CoreEditor, + ) { + if ( + connection.connectionType !== 'single' || + !connection.endPoint1.monomerId || + !connection.endPoint2.monomerId || + !connection.endPoint1.attachmentPointId || + !connection.endPoint2.attachmentPointId + ) { + editor.events.error.dispatch('Error during file parsing'); + return true; + } + return false; + } + + parseAndValidateMacromolecules(fileContent: string) { + const editor = CoreEditor.provideEditorInstance(); + let parsedFileContent: IKetMacromoleculesContent; + try { + parsedFileContent = JSON.parse(fileContent); + } catch (error) { + editor.events.error.dispatch('Error during file parsing'); + return { error: true }; + } + let error = false; + parsedFileContent.root.nodes.forEach((node) => { + if (node.type === 'monomer') { + error = this.validateMonomerNodeTemplate( + node, + parsedFileContent, + editor, + ); + } else { + editor.events.error.dispatch('Error during file parsing'); + error = true; + } + }); + if (error) { + return { error: true }; + } + parsedFileContent.root.connections.forEach((connection: IKetConnection) => { + this.validateConnectionTypeAndEndpoints(connection, editor); + }); + return { error, parsedFileContent }; + } + + deserializeMacromolecule(fileContent: string) { + const { error: hasValidationErrors, parsedFileContent } = + this.parseAndValidateMacromolecules(fileContent); + if (hasValidationErrors || !parsedFileContent) return; + let command = new Command(); + const editor = CoreEditor.provideEditorInstance(); + parsedFileContent.root.nodes.forEach((node) => { + switch (node.type) { + case 'monomer': { + const template = parsedFileContent.root.templates.find( + (template) => template.id === node.templateId, + ); + const struct = this.fillStruct(template); + assert(template); + command.merge(monomerToDrawingEntity(node, template, struct)); + break; + } + default: + break; + } + }); + editor.renderersContainer.update(command); + command = new Command(); + parsedFileContent.root.connections.forEach((connection) => { + switch (connection.connectionType) { + case 'single': { + const bondAdditionCommand = polymerBondToDrawingEntity(connection); + command.merge(bondAdditionCommand); + break; + } + default: + break; + } + }); + editor.renderersContainer.update(command); + } } diff --git a/packages/ketcher-core/src/domain/serializers/serializers.types.ts b/packages/ketcher-core/src/domain/serializers/serializers.types.ts index aa419afa40..5a23a3d134 100644 --- a/packages/ketcher-core/src/domain/serializers/serializers.types.ts +++ b/packages/ketcher-core/src/domain/serializers/serializers.types.ts @@ -17,4 +17,5 @@ export interface Serializer { deserialize: (content: string) => T; serialize: (struct: T) => string; + deserializeMacromolecule?: (content: string) => void; } diff --git a/packages/ketcher-polymer-editor-react/src/Editor.tsx b/packages/ketcher-polymer-editor-react/src/Editor.tsx index 46eca548fd..403469161d 100644 --- a/packages/ketcher-polymer-editor-react/src/Editor.tsx +++ b/packages/ketcher-polymer-editor-react/src/Editor.tsx @@ -146,6 +146,7 @@ function Editor({ theme }: EditorProps) { @@ -229,7 +230,7 @@ function MenuComponent() { - + diff --git a/packages/ketcher-polymer-editor-react/src/components/menu/subMenu/SubMenu.tsx b/packages/ketcher-polymer-editor-react/src/components/menu/subMenu/SubMenu.tsx index bc0ad544ad..e5c9fe45a0 100644 --- a/packages/ketcher-polymer-editor-react/src/components/menu/subMenu/SubMenu.tsx +++ b/packages/ketcher-polymer-editor-react/src/components/menu/subMenu/SubMenu.tsx @@ -56,11 +56,14 @@ const SubMenu = ({ .filter((item) => item); const activeOption = options.filter((itemKey) => isActive(itemKey)); const visibleItemId = activeOption.length ? activeOption[0] : options[0]; + const visibleItemTitle = subComponents.find( + (option) => option.props.itemId === visibleItemId, + )?.props.title; return ( - + {open || ( void; @@ -34,7 +35,8 @@ const onOk = ({ struct, fragment }) => { if (fragment) { console.log('add fragment'); } - console.log(struct); + const ketSerializer = new KetSerializer(); + ketSerializer.deserializeMacromolecule(struct); }; const isAnalyzingFile = false; const errorHandler = (error) => console.log(error); diff --git a/packages/ketcher-polymer-editor-react/src/components/modal/Open/OpenOptions/FileDrop/FileDrop.tsx b/packages/ketcher-polymer-editor-react/src/components/modal/Open/OpenOptions/FileDrop/FileDrop.tsx index 3fd721626d..8d9ffe476f 100644 --- a/packages/ketcher-polymer-editor-react/src/components/modal/Open/OpenOptions/FileDrop/FileDrop.tsx +++ b/packages/ketcher-polymer-editor-react/src/components/modal/Open/OpenOptions/FileDrop/FileDrop.tsx @@ -82,12 +82,12 @@ const FileDrop = ({
+ {textLabel && {textLabel}} - {textLabel && {textLabel}} {disabled ?

{disabledText}

: }