From 9a42ce126a264e4bd9db953e3d638558633d734e Mon Sep 17 00:00:00 2001 From: Roman Rodionov Date: Mon, 9 Dec 2024 15:45:38 +0100 Subject: [PATCH 1/2] #6068 - Same chain configuration imported by different HELM layouted differently (anyway - both are wrong) (#6099) #6074 - System doesn't flip chain if connected to monomer but not to base (2) #6068 - Same chain configuration imported by different HELM layouted differently (anyway - both are wrong) #6074 - System doesn't flip chain if connected to monomer but not to base (2) #6080 - System doesn't flip chain if connected to monomer but not to base (3) #6081 - Smaller chain should be at the bottom #6087 - Antisense layout is wrong for any ambiguouse base from the library #6077 - H-bond is not alligned to Snake mode view in some cases #6076 - Two-to-one base H-bond connection layouted wrong #6075 - In case of multipal H-bonds system should arrange antisence chain to first base of bottom chain #6070 - System doesn't flip chain if connected to monomer but not to base #6067 - Two chains connected by H-bond arranged wrong if third bond present on the canvas #6061 - RNA chain remain flipped after hydrogen bond removal - reworked antisense chains calculation --- .../src/application/editor/tools/Erase.ts | 6 + .../SnakeModePolymerBondRenderer.ts | 12 +- .../domain/entities/DrawingEntitiesManager.ts | 116 +++++++++++++----- .../src/domain/entities/Nucleoside.ts | 19 --- .../src/domain/entities/Nucleotide.ts | 5 - .../monomer-chains/ChainsCollection.ts | 55 ++++++--- 6 files changed, 144 insertions(+), 69 deletions(-) diff --git a/packages/ketcher-core/src/application/editor/tools/Erase.ts b/packages/ketcher-core/src/application/editor/tools/Erase.ts index 842c0b5b35..57673b82a4 100644 --- a/packages/ketcher-core/src/application/editor/tools/Erase.ts +++ b/packages/ketcher-core/src/application/editor/tools/Erase.ts @@ -30,6 +30,9 @@ class EraserTool implements BaseTool { ) { const modelChanges = this.editor.drawingEntitiesManager.deleteSelectedEntities(); + modelChanges.merge( + this.editor.drawingEntitiesManager.recalculateAntisenseChains(), + ); this.history.update(modelChanges); this.editor.renderersContainer.update(modelChanges); } @@ -47,6 +50,9 @@ class EraserTool implements BaseTool { this.editor.drawingEntitiesManager.deleteDrawingEntity( selectedItemRenderer.drawingEntity, ); + modelChanges.merge( + this.editor.drawingEntitiesManager.recalculateAntisenseChains(), + ); this.history.update(modelChanges); this.editor.renderersContainer.update(modelChanges); } diff --git a/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer/SnakeModePolymerBondRenderer.ts b/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer/SnakeModePolymerBondRenderer.ts index 8a0dcbbccb..078a8833da 100644 --- a/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer/SnakeModePolymerBondRenderer.ts +++ b/packages/ketcher-core/src/application/render/renderers/PolymerBondRenderer/SnakeModePolymerBondRenderer.ts @@ -225,7 +225,17 @@ export class SnakeModePolymerBondRenderer extends BaseRenderer { ) as Connection; const isVerticalConnection = firstCellConnection.isVertical; const isStraightVerticalConnection = - cells.length === 2 && isVerticalConnection; + (cells.length === 2 || + cells.reduce( + (isStraight: boolean, cell: Cell, index: number): boolean => { + if (!isStraight || index === 0 || index === cells.length - 1) { + return isStraight; + } + return cell.x === firstCell.x && !cell.monomer; + }, + true, + )) && + isVerticalConnection; const isFirstMonomerOfBondInFirstCell = firstCell.node?.monomers.includes( this.polymerBond.firstMonomer, ); diff --git a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts index 2e7dddc232..7094802f10 100644 --- a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts +++ b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts @@ -50,6 +50,7 @@ import { import { Coordinates, CoreEditor } from 'application/editor/internal'; import { getNextMonomerInChain, + getSugarFromRnaBase, isAmbiguousMonomerLibraryItem, isRnaBaseOrAmbiguousRnaBase, isValidNucleoside, @@ -864,6 +865,8 @@ export class DrawingEntitiesManager { command.merge(this.recalculateCanvasMatrix()); } + command.merge(this.recalculateAntisenseChains()); + return command; } @@ -1546,6 +1549,7 @@ export class DrawingEntitiesManager { private calculateSnakeLayoutMatrix(chainsCollection: ChainsCollection) { const snakeLayoutMatrix = new Matrix(); const monomersGroupedByY = new Map>(); + const monomerToNode = chainsCollection.monomerToNode; this.monomers.forEach((monomer) => { const x = Number(monomer.position.x.toFixed()); @@ -1596,7 +1600,7 @@ export class DrawingEntitiesManager { Number(indexY), Number(monomerXToIndexInMatrix[x]), new Cell( - chainsCollection.monomerToNode.get(monomer), + monomerToNode.get(monomer), [], Number(indexY), Number(monomerXToIndexInMatrix[x]), @@ -1642,26 +1646,27 @@ export class DrawingEntitiesManager { return; } - const antisenseChains = [ - ...chainsCollection.getComplementaryChains(chain), - ]; - const antisenseChainsStartIndexes = antisenseChains.map( - (antisenseChain) => { + const complimentaryChainsWithData = + chainsCollection.getComplimentaryChainsWithData(chain); + const antisenseChainsWithData = complimentaryChainsWithData.filter( + (complimentaryChainWithData) => + complimentaryChainWithData.complimentaryChain.firstMonomer + ?.monomerItem.isAntisense, + ); + const antisenseChainsStartIndexes = antisenseChainsWithData.map( + (antisenseChainWithData) => { const firstConnectedAntisenseNodeIndex = - antisenseChain.nodes.findIndex((node) => { - return ( - (node instanceof Nucleoside || node instanceof Nucleotide) && - node.getAntisenseRnaBase() - ); - }); - const firstConnectedAntisenseNode = antisenseChain.nodes[ - firstConnectedAntisenseNodeIndex - ] as Nucleoside | Nucleotide; - const senseRnaBase = - firstConnectedAntisenseNode.getAntisenseRnaBase(); - const senseNode = - senseRnaBase && chainsCollection.monomerToNode.get(senseRnaBase); - const senseNodeIndex = senseNode && chain.nodes.indexOf(senseNode); + antisenseChainWithData.complimentaryChain.nodes.findIndex( + (node) => { + return ( + node === + antisenseChainWithData.firstConnectedComplimentaryNode + ); + }, + ); + const senseNodeIndex = chain.nodes.indexOf( + antisenseChainWithData.firstConnectedNode, + ); if (!isNumber(senseNodeIndex)) { return -1; @@ -1674,7 +1679,7 @@ export class DrawingEntitiesManager { antisenseChainsStartIndexes.map( (antisenseChainsStartIndex, index) => [ antisenseChainsStartIndex, - antisenseChains[index], + antisenseChainsWithData[index], ], ), ); @@ -1682,17 +1687,24 @@ export class DrawingEntitiesManager { let restOfRowsWithAntisense = 0; let isPreviousChainWithAntisense = false; - chain.forEachNode(({ node, nodeIndex }) => { - if (rearrangedMonomersSet.has(node.monomer.id)) { + for ( + let nodeIndex = Math.min(0, ...antisenseChainsStartIndexes); + nodeIndex < chain.length; + nodeIndex++ + ) { + const node = chain.nodes[nodeIndex]; + + if (node && rearrangedMonomersSet.has(node.monomer.id)) { return; } - const antisenseChain = antisenseChainsStartIndexesMap.get(nodeIndex); + const antisenseChainWithData = + antisenseChainsStartIndexesMap.get(nodeIndex); - if (antisenseChain) { + if (antisenseChainWithData) { const { rowsUsedByAntisense, command: rearrangedAntisenseCommand } = this.rearrangeAntisenseChain( - antisenseChain, + antisenseChainWithData.complimentaryChain, lastPosition, canvasWidth, rearrangedMonomersSet, @@ -1705,6 +1717,10 @@ export class DrawingEntitiesManager { isPreviousChainWithAntisense = true; } + if (!node) { + continue; + } + const r2PolymerBond = node.lastMonomerInNode.attachmentPointsToBonds[ AttachmentPointName.R2 @@ -1757,7 +1773,8 @@ export class DrawingEntitiesManager { command.merge(rearrangeResult.command); }); } - }); + } + lastPosition = getFirstPosition(maxVerticalDistance, lastPosition); maxVerticalDistance = 0; @@ -2641,15 +2658,54 @@ export class DrawingEntitiesManager { const senseToAntisenseChains = new Map(); const handledChains = new Set(); + this.monomers.forEach((monomer) => { + command.merge( + this.modifyMonomerItem(monomer, { + ...monomer.monomerItem, + isAntisense: false, + isSense: false, + }), + ); + }); + chainsCollection.chains.forEach((chain) => { if (handledChains.has(chain)) { return; } let senseChain: Chain; - const complementaryChains = - chainsCollection.getComplementaryChains(chain); - const chainsToCheck = new Set(complementaryChains).add(chain); + const complimentaryChainsWithData = + chainsCollection.getComplimentaryChainsWithData(chain); + const chainsToCheck = new Set(); + + complimentaryChainsWithData.forEach((complimentaryChainWithData) => { + const hasHydrogenBondWithRnaBase = + complimentaryChainWithData.complimentaryChain.monomers.some( + (monomer) => { + return ( + (monomer instanceof RNABase && + Boolean(getSugarFromRnaBase(monomer)) && + monomer.hydrogenBonds.length > 0) || + monomer.hydrogenBonds.some((hydrogenBond) => { + const anotherMonomer = + hydrogenBond.getAnotherMonomer(monomer); + + return ( + anotherMonomer instanceof RNABase && + Boolean(getSugarFromRnaBase(anotherMonomer)) + ); + }) + ); + }, + ); + + if (hasHydrogenBondWithRnaBase) { + chainsToCheck.add(complimentaryChainWithData.complimentaryChain); + } + }); + + chainsToCheck.add(chain); + const chainToMonomers = new Map(); chainsToCheck.forEach((chainToCheck) => { diff --git a/packages/ketcher-core/src/domain/entities/Nucleoside.ts b/packages/ketcher-core/src/domain/entities/Nucleoside.ts index 076fdc84ae..7411edd0d9 100644 --- a/packages/ketcher-core/src/domain/entities/Nucleoside.ts +++ b/packages/ketcher-core/src/domain/entities/Nucleoside.ts @@ -4,7 +4,6 @@ import assert from 'assert'; import { getNextMonomerInChain, getRnaBaseFromSugar, - getSugarFromRnaBase, isValidNucleoside, isValidNucleotide, } from 'domain/helpers/monomers'; @@ -126,22 +125,4 @@ export class Nucleoside { this.rnaBase.isModification || this.sugar.isModification || isNotLastNode ); } - - public getAntisenseRnaBase() { - const hydrogenBondToAntisenseNode = this.rnaBase.hydrogenBonds.find( - (hydrogenBond) => { - const anotherMonomer = hydrogenBond.getAnotherMonomer(this.rnaBase); - - return ( - anotherMonomer instanceof RNABase && - getSugarFromRnaBase(anotherMonomer) - ); - }, - ); - const antisenseRnaBase = hydrogenBondToAntisenseNode?.getAnotherMonomer( - this.rnaBase, - ); - - return antisenseRnaBase; - } } diff --git a/packages/ketcher-core/src/domain/entities/Nucleotide.ts b/packages/ketcher-core/src/domain/entities/Nucleotide.ts index 584bebe47a..5689afdae2 100644 --- a/packages/ketcher-core/src/domain/entities/Nucleotide.ts +++ b/packages/ketcher-core/src/domain/entities/Nucleotide.ts @@ -18,7 +18,6 @@ import { AmbiguousMonomer } from 'domain/entities/AmbiguousMonomer'; import { RNA_MONOMER_DISTANCE } from 'application/editor/tools/RnaPreset'; import { SugarRenderer } from 'application/render'; import { SNAKE_LAYOUT_CELL_WIDTH } from 'domain/entities/DrawingEntitiesManager'; -import { Nucleoside } from 'domain/entities/Nucleoside'; export class Nucleotide { constructor( @@ -124,8 +123,4 @@ export class Nucleotide { this.phosphate.isModification ); } - - public getAntisenseRnaBase() { - return Nucleoside.prototype.getAntisenseRnaBase.call(this); - } } diff --git a/packages/ketcher-core/src/domain/entities/monomer-chains/ChainsCollection.ts b/packages/ketcher-core/src/domain/entities/monomer-chains/ChainsCollection.ts index 9b9a759cf8..f5e0e07317 100644 --- a/packages/ketcher-core/src/domain/entities/monomer-chains/ChainsCollection.ts +++ b/packages/ketcher-core/src/domain/entities/monomer-chains/ChainsCollection.ts @@ -4,8 +4,6 @@ import { BaseMonomer, Chem, IsChainCycled, - Nucleoside, - Nucleotide, Peptide, Phosphate, RNABase, @@ -24,6 +22,13 @@ import { BaseSubChain } from 'domain/entities/monomer-chains/BaseSubChain'; import { MonomerToAtomBond } from 'domain/entities/MonomerToAtomBond'; import { isMonomerSgroupWithAttachmentPoints } from '../../../utilities/monomers'; +export interface ComplimentaryChainsWithData { + complimentaryChain: Chain; + chain: Chain; + firstConnectedNode: SubChainNode; + firstConnectedComplimentaryNode: SubChainNode; +} + export class ChainsCollection { public chains: Chain[] = []; @@ -305,26 +310,48 @@ export class ChainsCollection { }); } - public getComplementaryChains(chain: Chain) { - const complementaryChains: Set = new Set(); - const monomerToChain = this.monomerToChain; + private getFirstAntisenseMonomerInNode(node: SubChainNode) { + for (let i = 0; i < node.monomers.length; i++) { + const monomer = node.monomers[i]; + const hydrogenBond = monomer.hydrogenBonds[0]; - chain.forEachNode(({ node }) => { - if (!(node instanceof Nucleotide || node instanceof Nucleoside)) { - return; + if (hydrogenBond) { + return hydrogenBond.getAnotherMonomer(monomer); } + } + + return undefined; + } + + public getComplimentaryChainsWithData(chain: Chain) { + const complimentaryChainsWithData: ComplimentaryChainsWithData[] = []; + const handledChains = new Set(); + const monomerToChain = this.monomerToChain; - const complementaryRnaBase = node.getAntisenseRnaBase(); - const complementaryChain = - complementaryRnaBase && monomerToChain.get(complementaryRnaBase); + chain.forEachNode(({ node }) => { + const complimentaryMonomer = this.getFirstAntisenseMonomerInNode(node); + const complimentaryNode = + complimentaryMonomer && this.monomerToNode.get(complimentaryMonomer); + const complimentaryChain = + complimentaryMonomer && monomerToChain.get(complimentaryMonomer); - if (!complementaryChain) { + if ( + !complimentaryNode || + !complimentaryChain || + handledChains.has(complimentaryChain) + ) { return; } - complementaryChains.add(complementaryChain); + handledChains.add(complimentaryChain); + complimentaryChainsWithData.push({ + complimentaryChain, + chain, + firstConnectedNode: node, + firstConnectedComplimentaryNode: complimentaryNode, + }); }); - return complementaryChains; + return complimentaryChainsWithData; } } From f26b2476a87ef5c3e747675407afd6bc25db1bae Mon Sep 17 00:00:00 2001 From: Roman Rodionov Date: Mon, 9 Dec 2024 19:57:12 +0100 Subject: [PATCH 2/2] - fixed flacky test --- ketcher-autotests/tests/Reagents/CML-format/CML-format.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ketcher-autotests/tests/Reagents/CML-format/CML-format.spec.ts b/ketcher-autotests/tests/Reagents/CML-format/CML-format.spec.ts index 90f8d8e6ec..57364626bd 100644 --- a/ketcher-autotests/tests/Reagents/CML-format/CML-format.spec.ts +++ b/ketcher-autotests/tests/Reagents/CML-format/CML-format.spec.ts @@ -10,6 +10,7 @@ import { pasteFromClipboardAndAddToCanvas, selectTopPanelButton, TopPanelButton, + moveMouseAway, } from '@utils'; import { clickOnFileFormatDropdown, getCml } from '@utils/formats'; @@ -76,6 +77,7 @@ test.describe('Reagents CML format', () => { page, ); await saveFileAsCmlFormat(page); + await moveMouseAway(page); await takeEditorScreenshot(page); });