diff --git a/ketcher-autotests/tests/specs/Macromolecule-editor/Macro-Micro-Switcher/macro-micro-switcher3.spec.ts b/ketcher-autotests/tests/specs/Macromolecule-editor/Macro-Micro-Switcher/macro-micro-switcher3.spec.ts index 0daa169c5a7..c94940816f1 100644 --- a/ketcher-autotests/tests/specs/Macromolecule-editor/Macro-Micro-Switcher/macro-micro-switcher3.spec.ts +++ b/ketcher-autotests/tests/specs/Macromolecule-editor/Macro-Micro-Switcher/macro-micro-switcher3.spec.ts @@ -11,10 +11,12 @@ import { cutToClipboardByKeyboard, pasteFromClipboardByKeyboard, clickOnCanvas, + clickInTheMiddleOfTheCanvas, waitForRender, resetZoomLevelToDefault, getCachedBodyCenter, zoomOutByKeyboard, + getKet, } from '@utils'; import { selectAllStructuresOnCanvas } from '@utils/canvas/selectSelection'; import { CommonLeftToolbar } from '@tests/pages/common/CommonLeftToolbar'; @@ -33,6 +35,7 @@ import { verticalFlipByKeyboard } from '@tests/specs/Structure-Creating-&-Editin import { MacromoleculesTopToolbar } from '@tests/pages/macromolecules/MacromoleculesTopToolbar'; import { LayoutMode } from '@tests/pages/constants/macromoleculesTopToolbar/Constants'; import { EditAbbreviationDialog } from '@tests/pages/molecules/canvas/EditAbbreviation'; +import { LeftToolbar } from '@tests/pages/molecules/LeftToolbar'; import { getBondLocator } from '@utils/macromolecules/polymerBond'; import { getAtomLocator } from '@utils/canvas/atoms/getAtomLocator/getAtomLocator'; import { getAbbreviationLocator } from '@utils/canvas/s-group-signes/getAbbreviationLocator'; @@ -421,6 +424,86 @@ const expandableMonomer: IMonomer = { monomerAlias: 'D', }; +const customRnaMoePreset: IMonomer = { + monomerDescription: 'Custom RNA 5meC-MOE-P preset', + KETFile: 'KET/Micro-Macro-Switcher/Expand-monomers/5meC-MOE-P-custom-rna.ket', + monomerAlias: 'MOE', +}; + +function expectCustomRnaMoePresetToStayIntact(rawKet: string) { + const ket = JSON.parse(rawKet) as { + root: { + nodes: Array<{ $ref: string }>; + connections: unknown[]; + }; + monomer0: { alias: string }; + monomer1: { alias: string }; + monomer2: { alias: string }; + }; + const nodeRefs = ket.root.nodes.map((node) => node.$ref); + + expect(nodeRefs).toHaveLength(3); + expect(nodeRefs).toEqual( + expect.arrayContaining(['monomer0', 'monomer1', 'monomer2']), + ); + expect(ket.root.connections).toHaveLength(2); + expect(ket.monomer0.alias).toBe('5meC'); + expect(ket.monomer1.alias).toBe('MOE'); + expect(ket.monomer2.alias).toBe('P'); +} + +function expectKetToContainMicroFragment(rawKet: string) { + const ket = JSON.parse(rawKet) as { + root: { + nodes: Array<{ $ref: string }>; + }; + [key: string]: unknown; + }; + + const hasMicroFragment = ket.root.nodes.some((node) => { + const referencedNode = ket[node.$ref] as { atoms?: unknown[] } | undefined; + + return Boolean(referencedNode?.atoms?.length); + }); + + expect(hasMicroFragment).toBeTruthy(); +} + +async function getStereoValuesBetweenCollapsedMonomers( + page: Page, + firstMonomerName: string, + secondMonomerName: string, +) { + return page.evaluate( + ({ firstMonomerName, secondMonomerName }) => { + const struct = window.ketcher.editor.struct(); + const sgroups = Array.from(struct.sgroups.values()); + const firstSgroup = sgroups.find( + (sgroup) => sgroup.data.name === firstMonomerName, + ); + const secondSgroup = sgroups.find( + (sgroup) => sgroup.data.name === secondMonomerName, + ); + + if (!firstSgroup || !secondSgroup) { + throw new Error('Unable to find collapsed monomer S-groups'); + } + + const firstAtoms = new Set(firstSgroup.atoms); + const secondAtoms = new Set(secondSgroup.atoms); + + return Array.from(struct.bonds.values()) + .filter( + (bond) => + (firstAtoms.has(bond.begin) && secondAtoms.has(bond.end)) || + (firstAtoms.has(bond.end) && secondAtoms.has(bond.begin)), + ) + .map((bond) => bond.stereo); + }, + { firstMonomerName, secondMonomerName }, + ); +} + test(`Verify that the system supports undo/redo functionality for expanding and collapsing monomers in micro mode`, async ({ MoleculesCanvas: _, }) => { @@ -479,6 +562,95 @@ test(`Verify switching back from micro mode to macro mode with expanded and coll ); }); +test(`Verify that custom RNA 5meC-MOE-P preset remains intact after expanding MOE and switching to macro mode`, async ({ + MoleculesCanvas: _, +}) => { + /* + * Test task: https://github.com/epam/ketcher/issues/9889 + * Description: Verify that a custom RNA preset with a multi-attachment MOE sugar + * is not lost when an expanded monomer is converted from Molecules + * mode back to Macromolecules mode. + * + * Case: 1. Load custom 5meC-MOE-P KET on Molecules canvas + * 2. Expand MOE monomer + * 3. Switch to Macromolecules mode + * 4. Verify monomer nodes and chain connections remain in exported KET + */ + await openFileAndAddToCanvasAsNewProject(page, customRnaMoePreset.KETFile); + await ContextMenu( + page, + getAbbreviationLocator(page, { + name: customRnaMoePreset.monomerAlias, + }), + ).click(MonomerOnMicroOption.ExpandMonomer); + + await CommonTopRightToolbar(page).turnOnMacromoleculesEditor(); + + expectCustomRnaMoePresetToStayIntact(await getKet(page)); +}); + +test(`Verify that custom RNA 5meC-MOE-P preset remains visible after removing MOE abbreviation and switching to macro mode`, async ({ + MoleculesCanvas: _, +}) => { + /* + * Test task: https://github.com/epam/ketcher/issues/9889 + * Description: Verify that removing a custom RNA monomer abbreviation in + * Molecules mode does not make the resulting atom/bond fragment + * disappear when switching back to Macromolecules mode. + * + * Case: 1. Load custom 5meC-MOE-P KET on Molecules canvas + * 2. Expand MOE monomer + * 3. Remove MOE abbreviation + * 4. Switch to Macromolecules mode + * 5. Verify an atom/bond fragment remains in exported KET + */ + await openFileAndAddToCanvasAsNewProject(page, customRnaMoePreset.KETFile); + await ContextMenu( + page, + getAbbreviationLocator(page, { + name: customRnaMoePreset.monomerAlias, + }), + ).click(MonomerOnMicroOption.ExpandMonomer); + + await clickInTheMiddleOfTheCanvas(page); + await LeftToolbar(page).sGroup(); + await EditAbbreviationDialog(page).removeAbbreviation(); + await CommonTopRightToolbar(page).turnOnMacromoleculesEditor(); + + expectKetToContainMicroFragment(await getKet(page)); +}); + +test(`Verify that custom RNA 5meC-MOE-P preset keeps a plain base-sugar bond after expanding and collapsing MOE`, async ({ + MoleculesCanvas: _, +}) => { + /* + * Test task: https://github.com/epam/ketcher/issues/9889 + * Description: Verify that the MOE attachment-point stereo bond is not copied + * permanently to the base-sugar monomer connection after an + * expand-collapse roundtrip. + * + * Case: 1. Load custom 5meC-MOE-P KET on Molecules canvas + * 2. Expand MOE monomer + * 3. Collapse MOE monomer + * 4. Verify the collapsed MOE-5meC connection has no stereo + */ + await openFileAndAddToCanvasAsNewProject(page, customRnaMoePreset.KETFile); + await ContextMenu( + page, + getAbbreviationLocator(page, { + name: customRnaMoePreset.monomerAlias, + }), + ).click(MonomerOnMicroOption.ExpandMonomer); + + await ContextMenu(page, getAtomLocator(page, { atomId: 10 })).click( + MonomerOnMicroOption.CollapseMonomer, + ); + + expect( + await getStereoValuesBetweenCollapsedMonomers(page, 'MOE', '5meC'), + ).toEqual([0]); +}); + const copyableMonomer: IMonomer = { monomerDescription: '1. Petide D (from library)', KETFile: diff --git a/ketcher-autotests/tests/test-data/KET/Micro-Macro-Switcher/Expand-monomers/5meC-MOE-P-custom-rna.ket b/ketcher-autotests/tests/test-data/KET/Micro-Macro-Switcher/Expand-monomers/5meC-MOE-P-custom-rna.ket new file mode 100644 index 00000000000..a6028475faf --- /dev/null +++ b/ketcher-autotests/tests/test-data/KET/Micro-Macro-Switcher/Expand-monomers/5meC-MOE-P-custom-rna.ket @@ -0,0 +1,657 @@ +{ + "ket_version": "2.0.0", + "root": { + "nodes": [ + { + "$ref": "monomer0" + }, + { + "$ref": "monomer1" + }, + { + "$ref": "monomer2" + } + ], + "connections": [ + { + "connectionType": "single", + "endpoint1": { + "monomerId": "monomer0", + "attachmentPointId": "R1" + }, + "endpoint2": { + "monomerId": "monomer1", + "attachmentPointId": "R3" + } + }, + { + "connectionType": "single", + "endpoint1": { + "monomerId": "monomer1", + "attachmentPointId": "R2" + }, + "endpoint2": { + "monomerId": "monomer2", + "attachmentPointId": "R1" + } + } + ], + "templates": [ + { + "$ref": "monomerTemplate-5meC___5-methylcytosine" + }, + { + "$ref": "monomerTemplate-MOE___2'-O-Methoxyethyl ribose" + }, + { + "$ref": "monomerTemplate-P___Phosphate" + } + ] + }, + "monomer0": { + "type": "monomer", + "id": "0", + "position": { + "x": 1.25, + "y": -2.75 + }, + "alias": "5meC", + "templateId": "5meC___5-methylcytosine" + }, + "monomerTemplate-5meC___5-methylcytosine": { + "type": "monomerTemplate", + "atoms": [ + { + "label": "N", + "location": [ + -1, + 0, + 0 + ] + }, + { + "label": "C", + "location": [ + -0.5, + -0.866, + 0 + ] + }, + { + "label": "N", + "location": [ + 0.5, + -0.866, + 0 + ] + }, + { + "label": "C", + "location": [ + 1, + 0, + 0 + ] + }, + { + "label": "C", + "location": [ + 0.5, + 0.866, + 0 + ] + }, + { + "label": "C", + "location": [ + -0.5, + 0.866, + 0 + ] + }, + { + "label": "H", + "location": [ + -2, + 0, + 0 + ] + }, + { + "label": "O", + "location": [ + -1, + -1.732, + 0 + ] + }, + { + "label": "N", + "location": [ + 2, + 0, + 0 + ] + }, + { + "label": "C", + "location": [ + 1, + 1.732, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 5, + 0 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 2 + ] + }, + { + "type": 2, + "atoms": [ + 2, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 3, + 4 + ] + }, + { + "type": 2, + "atoms": [ + 4, + 5 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 6 + ] + }, + { + "type": 2, + "atoms": [ + 1, + 7 + ] + }, + { + "type": 1, + "atoms": [ + 3, + 8 + ] + }, + { + "type": 1, + "atoms": [ + 4, + 9 + ] + } + ], + "class": "Base", + "classHELM": "RNA", + "id": "5meC___5-methylcytosine", + "fullName": "5-methylcytosine", + "alias": "5meC", + "aliasHELM": "m5C", + "attachmentPoints": [ + { + "attachmentAtom": 0, + "leavingGroup": { + "atoms": [ + 6 + ] + }, + "type": "left" + } + ], + "naturalAnalogShort": "C" + }, + "monomer1": { + "type": "monomer", + "id": "1", + "position": { + "x": 1.25, + "y": -1.25 + }, + "alias": "MOE", + "templateId": "MOE___2'-O-Methoxyethyl ribose" + }, + "monomerTemplate-MOE___2'-O-Methoxyethyl ribose": { + "type": "monomerTemplate", + "atoms": [ + { + "label": "C", + "location": [ + -0.704, + -0.378, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + 0.105, + 0.209, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + -0.204, + 1.16, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + -1.204, + 1.16, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "O", + "location": [ + -1.513, + 0.209, + 0 + ] + }, + { + "label": "C", + "location": [ + -1.792, + 1.969, + 0 + ] + }, + { + "label": "O", + "location": [ + -2.786, + 1.865, + 0 + ] + }, + { + "label": "H", + "location": [ + -3.374, + 2.674, + 0 + ] + }, + { + "label": "O", + "location": [ + 0.384, + 1.969, + 0 + ] + }, + { + "label": "H", + "location": [ + 1.378, + 1.865, + 0 + ] + }, + { + "label": "O", + "location": [ + -0.704, + -1.378, + 0 + ] + }, + { + "label": "O", + "location": [ + 1.056, + -0.1, + 0 + ] + }, + { + "label": "C", + "location": [ + 1.264, + -1.078, + 0 + ] + }, + { + "label": "C", + "location": [ + 2.215, + -1.387, + 0 + ] + }, + { + "label": "O", + "location": [ + 2.423, + -2.365, + 0 + ] + }, + { + "label": "C", + "location": [ + 3.374, + -2.674, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 4, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 3, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 0 + ] + }, + { + "type": 1, + "atoms": [ + 3, + 5 + ], + "stereo": 6 + }, + { + "type": 1, + "atoms": [ + 5, + 6 + ] + }, + { + "type": 1, + "atoms": [ + 6, + 7 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 8 + ], + "stereo": 1 + }, + { + "type": 1, + "atoms": [ + 8, + 9 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 10 + ], + "stereo": 6 + }, + { + "type": 1, + "atoms": [ + 1, + 11 + ], + "stereo": 1 + }, + { + "type": 1, + "atoms": [ + 11, + 12 + ] + }, + { + "type": 1, + "atoms": [ + 12, + 13 + ] + }, + { + "type": 1, + "atoms": [ + 13, + 14 + ] + }, + { + "type": 1, + "atoms": [ + 14, + 15 + ] + } + ], + "class": "Sugar", + "classHELM": "RNA", + "id": "MOE___2'-O-Methoxyethyl ribose", + "fullName": "2'-O-Methoxyethyl ribose", + "alias": "MOE", + "aliasHELM": "moe", + "attachmentPoints": [ + { + "attachmentAtom": 6, + "leavingGroup": { + "atoms": [ + 7 + ] + }, + "type": "left" + }, + { + "attachmentAtom": 8, + "leavingGroup": { + "atoms": [ + 9 + ] + }, + "type": "right" + }, + { + "attachmentAtom": 0, + "leavingGroup": { + "atoms": [ + 10 + ] + }, + "type": "side" + } + ], + "naturalAnalogShort": "R" + }, + "monomer2": { + "type": "monomer", + "id": "2", + "position": { + "x": 2.75, + "y": -1.25 + }, + "alias": "P", + "templateId": "P___Phosphate" + }, + "monomerTemplate-P___Phosphate": { + "type": "monomerTemplate", + "atoms": [ + { + "label": "P", + "location": [ + 0, + 0, + 0 + ] + }, + { + "label": "O", + "location": [ + 0.5, + -0.866, + 0 + ] + }, + { + "label": "O", + "location": [ + 0.5, + 0.866, + 0 + ] + }, + { + "label": "O", + "location": [ + -1, + 0, + 0 + ] + }, + { + "label": "O", + "location": [ + 1, + 0, + 0 + ] + } + ], + "bonds": [ + { + "type": 2, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 4 + ] + } + ], + "class": "Phosphate", + "classHELM": "RNA", + "id": "P___Phosphate", + "fullName": "Phosphate", + "alias": "P", + "aliasHELM": "p", + "aliasAxoLabs": "p", + "attachmentPoints": [ + { + "attachmentAtom": 0, + "leavingGroup": { + "atoms": [ + 3 + ] + }, + "type": "left" + }, + { + "attachmentAtom": 0, + "leavingGroup": { + "atoms": [ + 4 + ] + }, + "type": "right" + } + ], + "idtAliases": { + "base": "Phos", + "modifications": { + "endpoint3": "/3Phos/", + "endpoint5": "/5Phos/" + } + }, + "naturalAnalogShort": "P" + } +} \ No newline at end of file diff --git a/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts b/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts index 9019c2f133f..67a3c8315b8 100644 --- a/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts +++ b/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts @@ -40,6 +40,12 @@ import { HydrogenBond } from 'domain/entities/HydrogenBond'; import { MONOMER_CONST } from 'domain/constants/monomers'; import { MACROMOLECULES_BOND_TYPES } from 'application/editor/tools/types'; +type StructBondEndpoint = { + atom: MicromoleculesAtom; + sgroup?: SGroup; + attachmentPointNumber?: number; +}; + export class MacromoleculesConverter { public static convertMonomerToMonomerMicromolecule( monomer: BaseMonomer, @@ -483,6 +489,161 @@ export class MacromoleculesConverter { ); } + private static convertStructBondsToPolymerBonds( + struct: Struct, + drawingEntitiesManager: DrawingEntitiesManager, + command: Command, + sgroupToMonomer: Map, + fragmentIdToMonomer: Map, + superatomAttachmentPointToBond: Map, + ) { + struct.bonds.forEach((bond) => { + this.convertStructBondToPolymerBond( + struct, + bond, + drawingEntitiesManager, + command, + sgroupToMonomer, + fragmentIdToMonomer, + superatomAttachmentPointToBond, + ); + }); + } + + private static convertStructBondToPolymerBond( + struct: Struct, + bond: Bond, + drawingEntitiesManager: DrawingEntitiesManager, + command: Command, + sgroupToMonomer: Map, + fragmentIdToMonomer: Map, + superatomAttachmentPointToBond: Map, + ) { + const beginEndpoint = this.getStructBondEndpoint( + struct, + bond, + bond.begin, + bond.beginSuperatomAttachmentPointNumber, + superatomAttachmentPointToBond, + ); + const endEndpoint = this.getStructBondEndpoint( + struct, + bond, + bond.end, + bond.endSuperatomAttachmentPointNumber, + superatomAttachmentPointToBond, + ); + + if ( + !beginEndpoint || + !endEndpoint || + !this.canCreatePolymerBond(beginEndpoint, endEndpoint) + ) { + return; + } + + // Here we take monomers from sgroupToMonomer in case of macromolecules structure and + // from fragmentIdToMonomer in case of micromolecules structure. + const firstMonomer = this.getMonomerForStructBondEndpoint( + beginEndpoint, + sgroupToMonomer, + fragmentIdToMonomer, + ); + const secondMonomer = this.getMonomerForStructBondEndpoint( + endEndpoint, + sgroupToMonomer, + fragmentIdToMonomer, + ); + + assert(firstMonomer); + assert(secondMonomer); + assert(isNumber(beginEndpoint.attachmentPointNumber)); + assert(isNumber(endEndpoint.attachmentPointNumber)); + + command.merge( + drawingEntitiesManager.createPolymerBond( + firstMonomer, + secondMonomer, + getAttachmentPointLabel(beginEndpoint.attachmentPointNumber), + getAttachmentPointLabel(endEndpoint.attachmentPointNumber), + bond.type === Bond.PATTERN.TYPE.HYDROGEN + ? MACROMOLECULES_BOND_TYPES.HYDROGEN + : MACROMOLECULES_BOND_TYPES.SINGLE, + ), + ); + } + + private static getStructBondEndpoint( + struct: Struct, + bond: Bond, + atomId: number, + superatomAttachmentPointNumber: number | undefined, + superatomAttachmentPointToBond: Map, + ): StructBondEndpoint | undefined { + const atom = struct.atoms.get(atomId); + + if (!atom) { + return undefined; + } + + const sgroup = struct.getGroupFromAtomId(atomId); + const attachmentPoints = sgroup?.getAttachmentPoints(); + const attachmentPointNumber = isNumber(superatomAttachmentPointNumber) + ? superatomAttachmentPointNumber + : attachmentPoints?.findIndex( + (sgroupAttachmentPoint) => + sgroupAttachmentPoint.atomId === atomId && + !superatomAttachmentPointToBond.has(sgroupAttachmentPoint), + ); + const attachmentPoint = + isNumber(attachmentPointNumber) && + attachmentPoints?.find( + (candidate) => + candidate.attachmentPointNumber === attachmentPointNumber, + ); + + if (attachmentPoint) { + superatomAttachmentPointToBond.set(attachmentPoint, bond); + } + + return { + atom, + sgroup, + attachmentPointNumber, + }; + } + + private static canCreatePolymerBond( + beginEndpoint: StructBondEndpoint, + endEndpoint: StructBondEndpoint, + ) { + return ( + beginEndpoint.sgroup !== endEndpoint.sgroup && + isNumber(beginEndpoint.attachmentPointNumber) && + isNumber(endEndpoint.attachmentPointNumber) && + Boolean(beginEndpoint.sgroup) && + Boolean(endEndpoint.sgroup) && + this.isMonomerLikeSgroup(beginEndpoint.sgroup as SGroup) && + this.isMonomerLikeSgroup(endEndpoint.sgroup as SGroup) + ); + } + + private static isMonomerLikeSgroup(sgroup: SGroup) { + return ( + sgroup instanceof MonomerMicromolecule || sgroup.isSuperatomWithoutLabel + ); + } + + private static getMonomerForStructBondEndpoint( + endpoint: StructBondEndpoint, + sgroupToMonomer: Map, + fragmentIdToMonomer: Map, + ) { + return endpoint.sgroup instanceof MonomerMicromolecule + ? sgroupToMonomer.get(endpoint.sgroup) + : fragmentIdToMonomer.get(endpoint.atom.fragment); + } + public static convertStructToDrawingEntities( struct: Struct, drawingEntitiesManager: DrawingEntitiesManager, @@ -646,97 +807,14 @@ export class MacromoleculesConverter { ); }); - struct.bonds.forEach((bond) => { - const beginAtom = struct.atoms.get(bond.begin); - const endAtom = struct.atoms.get(bond.end); - - if (!beginAtom || !endAtom) { - return; - } - - const beginAtomSgroup = struct.getGroupFromAtomId(bond.begin); - const beginAtomSgroupAttachmentPoints = - beginAtomSgroup?.getAttachmentPoints(); - const endAtomSgroup = struct.getGroupFromAtomId(bond.end); - const endAtomSgroupAttachmentPoints = - endAtomSgroup?.getAttachmentPoints(); - const beginAtomAttachmentPointNumber = isNumber( - bond.beginSuperatomAttachmentPointNumber, - ) - ? bond.beginSuperatomAttachmentPointNumber - : beginAtomSgroupAttachmentPoints?.findIndex( - (sgroupAttachmentPoint) => - sgroupAttachmentPoint.atomId === bond.begin && - !superatomAttachmentPointToBond.has(sgroupAttachmentPoint), - ); - const beginAtomAttachmentPoint = - isNumber(beginAtomAttachmentPointNumber) && - beginAtomSgroupAttachmentPoints?.find( - (attachmentPoint) => - attachmentPoint.attachmentPointNumber === - beginAtomAttachmentPointNumber, - ); - const endAtomAttachmentPointNumber = isNumber( - bond.endSuperatomAttachmentPointNumber, - ) - ? bond.endSuperatomAttachmentPointNumber - : endAtomSgroupAttachmentPoints?.findIndex( - (sgroupAttachmentPoint) => - sgroupAttachmentPoint.atomId === bond.end && - !superatomAttachmentPointToBond.has(sgroupAttachmentPoint), - ); - const endAtomAttachmentPoint = - isNumber(endAtomAttachmentPointNumber) && - endAtomSgroupAttachmentPoints?.find( - (attachmentPoint) => - attachmentPoint.attachmentPointNumber === - endAtomAttachmentPointNumber, - ); - - if (beginAtomAttachmentPoint) { - superatomAttachmentPointToBond.set(beginAtomAttachmentPoint, bond); - } - if (endAtomAttachmentPoint) { - superatomAttachmentPointToBond.set(endAtomAttachmentPoint, bond); - } - if ( - endAtomSgroup !== beginAtomSgroup && - isNumber(beginAtomAttachmentPointNumber) && - isNumber(endAtomAttachmentPointNumber) && - beginAtomSgroup && - endAtomSgroup && - (beginAtomSgroup instanceof MonomerMicromolecule || - beginAtomSgroup.isSuperatomWithoutLabel) && - (endAtomSgroup instanceof MonomerMicromolecule || - endAtomSgroup.isSuperatomWithoutLabel) - ) { - // Here we take monomers from sgroupToMonomer in case of macromolecules structure and - // from fragmentIdToMonomer in case of micromolecules structure. - const firstMonomer = - beginAtomSgroup instanceof MonomerMicromolecule - ? sgroupToMonomer.get(beginAtomSgroup) - : fragmentIdToMonomer.get(beginAtom.fragment); - const secondMonomer = - endAtomSgroup instanceof MonomerMicromolecule - ? sgroupToMonomer.get(endAtomSgroup) - : fragmentIdToMonomer.get(endAtom.fragment); - - assert(firstMonomer); - assert(secondMonomer); - - command.merge( - drawingEntitiesManager.createPolymerBond( - firstMonomer, - secondMonomer, - getAttachmentPointLabel(beginAtomAttachmentPointNumber), - getAttachmentPointLabel(endAtomAttachmentPointNumber), - bond.type === Bond.PATTERN.TYPE.HYDROGEN - ? MACROMOLECULES_BOND_TYPES.HYDROGEN - : MACROMOLECULES_BOND_TYPES.SINGLE, - ), - ); - } - }); + this.convertStructBondsToPolymerBonds( + struct, + drawingEntitiesManager, + command, + sgroupToMonomer, + fragmentIdToMonomer, + superatomAttachmentPointToBond, + ); // Arrows and pluses struct.rxnArrows.forEach((rxnArrow) => { diff --git a/packages/ketcher-core/src/application/editor/actions/sgroup.ts b/packages/ketcher-core/src/application/editor/actions/sgroup.ts index e9ab14750f2..68d12d28c3b 100644 --- a/packages/ketcher-core/src/application/editor/actions/sgroup.ts +++ b/packages/ketcher-core/src/application/editor/actions/sgroup.ts @@ -261,6 +261,17 @@ export function setExpandMonomerSGroup( } } + if (!attrs.expanded && !otherMonomerIsExpanded) { + // Both monomers will be collapsed: clear transient attachment-point + // stereo carried over from the expanded state. + if (bondToOutside.stereo !== Bond.PATTERN.STEREO.NONE) { + action.addOp( + new BondAttr(bondId, 'stereo', Bond.PATTERN.STEREO.NONE), + ); + } + continue; + } + if (hasEffectiveCurrentStereo && !hasEffectiveOtherStereo) { if (bondToOutside.begin !== atomInsideCurrentMonomer) { action.mergeWith( diff --git a/packages/ketcher-core/src/domain/entities/BaseMicromoleculeEntity.ts b/packages/ketcher-core/src/domain/entities/BaseMicromoleculeEntity.ts index bf48e44e729..70ad33f48ac 100644 --- a/packages/ketcher-core/src/domain/entities/BaseMicromoleculeEntity.ts +++ b/packages/ketcher-core/src/domain/entities/BaseMicromoleculeEntity.ts @@ -16,6 +16,7 @@ export const INVALID = 'invalid'; export type initiallySelectedType = boolean | 'invalid'; + export abstract class BaseMicromoleculeEntity { initiallySelected?: initiallySelectedType; @@ -32,6 +33,12 @@ export abstract class BaseMicromoleculeEntity { return this.initiallySelected; } + getInitiallySelectedForSerialization(): boolean | undefined { + return this.initiallySelected === INVALID + ? undefined + : this.initiallySelected; + } + setInitiallySelected(value?: boolean): void { if (this.initiallySelected === INVALID) { throw new Error( diff --git a/packages/ketcher-core/src/domain/entities/image.ts b/packages/ketcher-core/src/domain/entities/image.ts index 62b4a8405c2..32a5e8d35f3 100644 --- a/packages/ketcher-core/src/domain/entities/image.ts +++ b/packages/ketcher-core/src/domain/entities/image.ts @@ -153,7 +153,7 @@ export class Image extends BaseMicromoleculeEntity { height: this.halfSize.y * 2, }, data: base64Data, - selected: this.getInitiallySelected(), + selected: this.getInitiallySelectedForSerialization(), }; } diff --git a/packages/ketcher-core/src/domain/entities/multitailArrow.ts b/packages/ketcher-core/src/domain/entities/multitailArrow.ts index 959bcba0994..ae037c0daa1 100644 --- a/packages/ketcher-core/src/domain/entities/multitailArrow.ts +++ b/packages/ketcher-core/src/domain/entities/multitailArrow.ts @@ -750,7 +750,7 @@ export class MultitailArrow extends BaseMicromoleculeEntity { this.tailsYOffset, this.height, this.center(), - this.getInitiallySelected(), + this.getInitiallySelectedForSerialization(), ); } } diff --git a/packages/ketcher-core/src/domain/serializers/ket/toKet/moleculeToKet.ts b/packages/ketcher-core/src/domain/serializers/ket/toKet/moleculeToKet.ts index 677695c2cd9..910f4ea4da6 100644 --- a/packages/ketcher-core/src/domain/serializers/ket/toKet/moleculeToKet.ts +++ b/packages/ketcher-core/src/domain/serializers/ket/toKet/moleculeToKet.ts @@ -109,7 +109,7 @@ function atomToKet(source, monomer?: BaseMonomer) { ifDef(result, 'radical', source.radical, 0); ifDef(result, 'attachmentPoints', source.attachmentPoints, 0); ifDef(result, 'cip', source.cip, ''); - ifDef(result, 'selected', source.getInitiallySelected()); + ifDef(result, 'selected', source.getInitiallySelectedForSerialization()); // stereo ifDef(result, 'stereoLabel', source.stereoLabel, null); ifDef(result, 'stereoParity', source.stereoCare, 0); @@ -148,7 +148,7 @@ function rglabelToKet(source) { (rgnumber) => `rg-${rgnumber}`, ); ifDef(result, '$refs', refsToRGroups); - ifDef(result, 'selected', source.getInitiallySelected()); + ifDef(result, 'selected', source.getInitiallySelectedForSerialization()); return result; } @@ -166,7 +166,7 @@ function bondToKet(source) { ifDef(result, 'center', source.reactingCenterStatus, 0); ifDef(result, 'cip', source.cip, ''); } - ifDef(result, 'selected', source.getInitiallySelected()); + ifDef(result, 'selected', source.getInitiallySelectedForSerialization()); return result; } diff --git a/packages/ketcher-core/src/domain/serializers/ket/toKet/prepare.ts b/packages/ketcher-core/src/domain/serializers/ket/toKet/prepare.ts index 4f9632aedf5..eba7312662c 100644 --- a/packages/ketcher-core/src/domain/serializers/ket/toKet/prepare.ts +++ b/packages/ketcher-core/src/domain/serializers/ket/toKet/prepare.ts @@ -55,7 +55,7 @@ export function prepareStructForKet(struct: Struct) { pos: item.pos, height: item.height, }, - selected: item.getInitiallySelected(), + selected: item.getInitiallySelectedForSerialization(), }); }); @@ -64,7 +64,7 @@ export function prepareStructForKet(struct: Struct) { type: 'plus', center: item.pp, data: {}, - selected: item.getInitiallySelected(), + selected: item.getInitiallySelectedForSerialization(), }); }); @@ -76,7 +76,7 @@ export function prepareStructForKet(struct: Struct) { mode: item.mode, pos: item.pos, }, - selected: item.getInitiallySelected(), + selected: item.getInitiallySelectedForSerialization(), }); }); @@ -89,7 +89,7 @@ export function prepareStructForKet(struct: Struct) { position: item.position, pos: item.pos, }, - selected: item.getInitiallySelected(), + selected: item.getInitiallySelectedForSerialization(), }); });