Skip to content

Commit

Permalink
#6252 - allow creation of antisense chains in sequence mode (#6516)
Browse files Browse the repository at this point in the history
* feat: Add antisense logic in menu dropdown

* feat: Reuse antisense logic in menu dropdown from other view

* feat: Update rerender for sequence mode

* feat: Update Playwright tests

* feat: Update Playwright test

* - updated screenshots

---------

Co-authored-by: Roman Rodionov <[email protected]>
  • Loading branch information
vitaepam and rrodionov91 authored Feb 19, 2025
1 parent 604f87f commit 63a3386
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 92 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ test.describe('Sequence mode selection for view mode', () => {
await takeEditorScreenshot(page);
await page.getByText('G').first().click({ button: 'right' });
await page.getByTestId('edit_sequence').click();
await moveMouseAway(page);
await takeEditorScreenshot(page);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import {
StandardAmbiguousRnaBase,
} from 'domain/constants/monomers';
import { Chain } from 'domain/entities/monomer-chains/Chain';
import { ReinitializeModeOperation } from 'application/editor/operations/modes';
import {
SnakeLayoutModel,
SnakeLayoutNode,
Expand Down Expand Up @@ -2897,6 +2898,10 @@ export class DrawingEntitiesManager {
this.applySnakeLayout(editor.canvas.width.baseVal.value, true, true),
);

if (editor.mode instanceof SequenceMode) {
command.addOperation(new ReinitializeModeOperation());
}

command.setUndoOperationsByPriority();

return command;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,113 +5,23 @@ import { KETCHER_MACROMOLECULES_ROOT_NODE_SELECTOR } from 'ketcher-react';
import { useAppSelector } from 'hooks';
import { selectEditor } from 'state/common';
import {
AmbiguousMonomer,
BaseMonomer,
getRnaBaseFromSugar,
getSugarFromRnaBase,
isRnaBaseOrAmbiguousRnaBase,
KetAmbiguousMonomerTemplateSubType,
RNABase,
Sugar,
} from 'ketcher-core';
import { ContextMenu } from 'components/contextMenu/ContextMenu';
import { isAntisenseCreationDisabled } from './helpers';

type SelectedMonomersContextMenuType = {
selectedMonomers: BaseMonomer[];
};

const getMonomersCode = (monomers: BaseMonomer[]) => {
return monomers
.map((monomer) => monomer.monomerItem.props.MonomerNaturalAnalogCode)
.sort()
.join('');
};
const isSenseBase = (monomer: BaseMonomer | AmbiguousMonomer) => {
const { monomerItem } = monomer;
const isNaturalAnalogue =
monomerItem.props.MonomerNaturalAnalogCode === 'A' ||
monomerItem.props.MonomerNaturalAnalogCode === 'C' ||
monomerItem.props.MonomerNaturalAnalogCode === 'G' ||
monomerItem.props.MonomerNaturalAnalogCode === 'T' ||
monomerItem.props.MonomerNaturalAnalogCode === 'U';
if (isNaturalAnalogue) {
return true;
}
if (!monomer.monomerItem.isAmbiguous) {
return false;
}

if (
(monomer as AmbiguousMonomer).subtype ===
KetAmbiguousMonomerTemplateSubType.MIXTURE
) {
return false;
}

const N1 = 'ACGT';
const N2 = 'ACGU';
const B1 = 'CGT';
const B2 = 'CGU';
const D1 = 'AGT';
const D2 = 'AGU';
const H1 = 'ACT';
const H2 = 'ACU';
const K1 = 'GT';
const K2 = 'GU';
const W1 = 'AT';
const W2 = 'AU';
const Y1 = 'CT';
const Y2 = 'CU';
const M = 'AC';
const R = 'AG';
const S = 'CG';
const V = 'ACG';
const ambigues = [
N1,
N2,
B1,
B2,
D1,
D2,
H1,
H2,
K1,
K2,
W1,
W2,
Y1,
Y2,
M,
R,
S,
V,
];
const code = getMonomersCode((monomer as AmbiguousMonomer).monomers);
return ambigues.some((v) => v === code);
};

export const SelectedMonomersContextMenu = ({
selectedMonomers,
}: SelectedMonomersContextMenuType) => {
const editor = useAppSelector(selectEditor);
const isAntisenseCreationDisabled = selectedMonomers?.some(
(selectedMonomer: BaseMonomer) => {
const rnaBaseForSugar =
selectedMonomer instanceof Sugar &&
getRnaBaseFromSugar(selectedMonomer);

return (
(selectedMonomer instanceof RNABase &&
(selectedMonomer.hydrogenBonds.length > 0 ||
selectedMonomer.covalentBonds.length > 1)) ||
(isRnaBaseOrAmbiguousRnaBase(selectedMonomer) &&
!isSenseBase(selectedMonomer)) ||
(rnaBaseForSugar &&
(rnaBaseForSugar.hydrogenBonds.length > 0 ||
!isSenseBase(rnaBaseForSugar)))
);
},
);

const menuItems = [
{
Expand All @@ -122,7 +32,7 @@ export const SelectedMonomersContextMenu = ({
name: 'create_antisense_chain',
title: 'Create Antisense Strand',
separator: true,
disabled: isAntisenseCreationDisabled,
disabled: isAntisenseCreationDisabled(selectedMonomers),
hidden: ({ props }: { props?: { selectedMonomers?: BaseMonomer[] } }) => {
return !props?.selectedMonomers?.some((selectedMonomer) => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
AmbiguousMonomer,
BaseMonomer,
getRnaBaseFromSugar,
isRnaBaseOrAmbiguousRnaBase,
KetAmbiguousMonomerTemplateSubType,
RNABase,
Sugar,
} from 'ketcher-core';

const getMonomersCode = (monomers: BaseMonomer[]) => {
return monomers
.map((monomer) => monomer.monomerItem.props.MonomerNaturalAnalogCode)
.sort()
.join('');
};

export const isSenseBase = (monomer: BaseMonomer | AmbiguousMonomer) => {
const { monomerItem } = monomer;
const isNaturalAnalogue =
monomerItem.props.MonomerNaturalAnalogCode === 'A' ||
monomerItem.props.MonomerNaturalAnalogCode === 'C' ||
monomerItem.props.MonomerNaturalAnalogCode === 'G' ||
monomerItem.props.MonomerNaturalAnalogCode === 'T' ||
monomerItem.props.MonomerNaturalAnalogCode === 'U';
if (isNaturalAnalogue) {
return true;
}
if (!monomer.monomerItem.isAmbiguous) {
return false;
}

if (
(monomer as AmbiguousMonomer).subtype ===
KetAmbiguousMonomerTemplateSubType.MIXTURE
) {
return false;
}

const N1 = 'ACGT';
const N2 = 'ACGU';
const B1 = 'CGT';
const B2 = 'CGU';
const D1 = 'AGT';
const D2 = 'AGU';
const H1 = 'ACT';
const H2 = 'ACU';
const K1 = 'GT';
const K2 = 'GU';
const W1 = 'AT';
const W2 = 'AU';
const Y1 = 'CT';
const Y2 = 'CU';
const M = 'AC';
const R = 'AG';
const S = 'CG';
const V = 'ACG';
const ambigues = [
N1,
N2,
B1,
B2,
D1,
D2,
H1,
H2,
K1,
K2,
W1,
W2,
Y1,
Y2,
M,
R,
S,
V,
];
const code = getMonomersCode((monomer as AmbiguousMonomer).monomers);
return ambigues.some((v) => v === code);
};

export const isAntisenseCreationDisabled = (
selectedMonomers: BaseMonomer[],
) => {
return selectedMonomers?.some((selectedMonomer: BaseMonomer) => {
const rnaBaseForSugar =
selectedMonomer instanceof Sugar && getRnaBaseFromSugar(selectedMonomer);

return (
(selectedMonomer instanceof RNABase &&
(selectedMonomer.hydrogenBonds.length > 0 ||
selectedMonomer.covalentBonds.length > 1)) ||
(isRnaBaseOrAmbiguousRnaBase(selectedMonomer) &&
!isSenseBase(selectedMonomer)) ||
(rnaBaseForSugar &&
(rnaBaseForSugar.hydrogenBonds.length > 0 ||
!isSenseBase(rnaBaseForSugar)))
);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import {
BaseSequenceItemRenderer,
ModeTypes,
NodesSelection,
getRnaBaseFromSugar,
getSugarFromRnaBase,
BaseMonomer,
RNABase,
Sugar,
} from 'ketcher-core';
import { setSelectedTabIndex } from 'state/library';
import {
Expand All @@ -23,6 +28,7 @@ import {
} from 'state/rna-builder';
import { generateSequenceContextMenuProps } from 'components/contextMenu/SequenceItemContextMenu/helpers';
import { ContextMenu } from 'components/contextMenu/ContextMenu';
import { isAntisenseCreationDisabled } from 'components/contextMenu/SelectedMonomersContextMenu/helpers';
import { LIBRARY_TAB_INDEX } from 'src/constants';

type SequenceItemContextMenuType = {
Expand All @@ -31,6 +37,8 @@ type SequenceItemContextMenuType = {

export enum SequenceItemContextMenuNames {
title = 'sequence_menu_title',
createRnaAntisenseStrand = 'create_rna_antisense_strand',
createDnaAntisenseStrand = 'create_dna_antisense_strand',
modifyInRnaBuilder = 'modify_in_rna_builder',
editSequence = 'edit_sequence',
startNewSequence = 'start_new_sequence',
Expand All @@ -42,11 +50,46 @@ export const SequenceItemContextMenu = ({
const editor = useAppSelector(selectEditor);
const dispatch = useAppDispatch();
const menuProps = generateSequenceContextMenuProps(selections);
const extractedBaseMonomers: BaseMonomer[] =
selections?.[0]
?.flatMap((item) => {
const node = item.node as {
sugar?: BaseMonomer;
rnaBase?: BaseMonomer;
phosphate?: BaseMonomer;
monomer?: BaseMonomer;
};
return node
? [node.sugar, node.rnaBase, node.phosphate, node.monomer]
: [];
})
.filter(
(baseMonomer): baseMonomer is BaseMonomer => baseMonomer !== undefined,
) || [];

const isSequenceEditInRNABuilderMode = useAppSelector(
selectIsSequenceEditInRNABuilderMode,
);
const isSequenceMode = useLayoutMode() === ModeTypes.sequence;

const isAntisenseOptionHidden = ({
props,
}: {
props?: { sequenceItemRenderer?: BaseSequenceItemRenderer };
}) => {
return (
!props?.sequenceItemRenderer ||
!extractedBaseMonomers?.some((selectedMonomer) => {
return (
(selectedMonomer instanceof RNABase &&
getSugarFromRnaBase(selectedMonomer)) ||
(selectedMonomer instanceof Sugar &&
getRnaBaseFromSugar(selectedMonomer))
);
})
);
};

const menuItems = [
{
name: SequenceItemContextMenuNames.title,
Expand All @@ -64,6 +107,18 @@ export const SequenceItemContextMenu = ({
);
},
},
{
name: SequenceItemContextMenuNames.createRnaAntisenseStrand,
title: 'Create RNA antisense strand',
disabled: isAntisenseCreationDisabled(extractedBaseMonomers),
hidden: isAntisenseOptionHidden,
},
{
name: SequenceItemContextMenuNames.createDnaAntisenseStrand,
title: 'Create DNA antisense strand',
disabled: true,
hidden: isAntisenseOptionHidden,
},
{
name: SequenceItemContextMenuNames.modifyInRnaBuilder,
title: 'Modify in RNA Builder...',
Expand Down Expand Up @@ -128,6 +183,12 @@ export const SequenceItemContextMenu = ({
case SequenceItemContextMenuNames.editSequence:
editor.events.editSequence.dispatch(props.sequenceItemRenderer);
break;
case SequenceItemContextMenuNames.createRnaAntisenseStrand:
editor.events.createAntisenseChain.dispatch();
break;
case SequenceItemContextMenuNames.createDnaAntisenseStrand:
// TODO: implement createDnaAntisenseStrand
break;
default:
break;
}
Expand Down

0 comments on commit 63a3386

Please sign in to comment.