From 963009b6cdfee94fb05f73a382de011ad5acf869 Mon Sep 17 00:00:00 2001 From: Roman Rodionov Date: Mon, 27 May 2024 18:59:05 +0200 Subject: [PATCH] #4530 - Add ability to define attachment points for molecules - added conversion of molecules to superatoms without label after adding superatom attachment points - added connections between molecules and monomers --- .../src/application/editor/actions/bond.ts | 12 ++++- .../src/application/editor/actions/erase.ts | 5 +- .../src/application/editor/actions/sgroup.ts | 47 +++++++++++++++--- .../editor/operations/sgroup/sgroupAtom.ts | 4 +- .../src/application/formatters/types/ket.ts | 12 ++++- .../src/domain/entities/BaseMonomer.ts | 6 +-- .../ketcher-core/src/domain/entities/atom.ts | 30 +++++++++++- .../src/domain/entities/functionalGroup.ts | 7 ++- .../src/domain/entities/struct.ts | 6 +++ .../ket/fromKet/polymerBondToDrawingEntity.ts | 12 ++--- .../domain/serializers/ket/ketSerializer.ts | 26 ++++++---- .../src/script/editor/tool/bond.ts | 13 +++-- .../src/script/editor/tool/chain.ts | 12 +++-- .../tool/helper/filterNotInCollapsedSGroup.ts | 24 ++-------- .../src/script/editor/tool/select.ts | 5 +- .../src/script/editor/tool/sgroup.ts | 48 +++++++++++++++++-- .../ContextMenu/ContextMenuTrigger.tsx | 7 ++- .../hooks/useRemoveAttachmentPoint.ts | 45 +++++++++++++++++ .../ContextMenu/menuItems/AtomMenuItems.tsx | 44 +++++++++++++---- 19 files changed, 279 insertions(+), 86 deletions(-) create mode 100644 packages/ketcher-react/src/script/ui/views/components/ContextMenu/hooks/useRemoveAttachmentPoint.ts diff --git a/packages/ketcher-core/src/application/editor/actions/bond.ts b/packages/ketcher-core/src/application/editor/actions/bond.ts index d17f469fb0..3b6c3bb9e4 100644 --- a/packages/ketcher-core/src/application/editor/actions/bond.ts +++ b/packages/ketcher-core/src/application/editor/actions/bond.ts @@ -49,6 +49,7 @@ import { Action } from './action'; import { ReSGroup, ReStruct } from '../../render'; import { StereoValidator } from 'domain/helpers'; import utils from '../shared/utils'; +import { fromSgroupAttachmentPointRemove } from 'application/editor'; export function fromBondAddition( reStruct: ReStruct, @@ -481,10 +482,19 @@ export function removeAttachmentPointFromSuperatom( sgroup: ReSGroup, beginAtomId: number | undefined, endAtomId: number | undefined, + action: Action, + restruct: ReStruct, ) { (sgroup.item?.atoms as number[]).forEach((atomId) => { if (beginAtomId === atomId || endAtomId === atomId) { - sgroup.item?.removeAttachmentPoint(atomId); + action.mergeWith( + fromSgroupAttachmentPointRemove( + restruct, + sgroup.item?.id as number, + atomId as number, + false, + ), + ); } }); } diff --git a/packages/ketcher-core/src/application/editor/actions/erase.ts b/packages/ketcher-core/src/application/editor/actions/erase.ts index 4ab638d302..e86f06651e 100644 --- a/packages/ketcher-core/src/application/editor/actions/erase.ts +++ b/packages/ketcher-core/src/application/editor/actions/erase.ts @@ -47,6 +47,8 @@ function fromBondDeletion( bid: number, skipAtoms: Array = [], ) { + let action = new Action(); + if (restruct.sgroups && restruct.sgroups.size > 0) { restruct.sgroups.forEach((sgroup) => { if (sgroup.item?.type && sgroup.item?.type === 'SUP') { @@ -57,12 +59,13 @@ function fromBondDeletion( sgroup, beginAtomConnectedToBond, endAtomConnectedToBond, + action, + restruct, ); } }); } - let action = new Action(); const bond: any = restruct.molecule.bonds.get(bid); const atomsToRemove: Array = []; diff --git a/packages/ketcher-core/src/application/editor/actions/sgroup.ts b/packages/ketcher-core/src/application/editor/actions/sgroup.ts index 3c9639ad76..aecef3c8fd 100644 --- a/packages/ketcher-core/src/application/editor/actions/sgroup.ts +++ b/packages/ketcher-core/src/application/editor/actions/sgroup.ts @@ -142,7 +142,7 @@ export function sGroupAttributeAction(id, attrs) { return action; } -export function fromSgroupDeletion(restruct, id) { +export function fromSgroupDeletion(restruct, id, needPerform = true) { let action = new Action(); const struct = restruct.molecule; @@ -174,7 +174,9 @@ export function fromSgroupDeletion(restruct, id) { action.addOp(new SGroupDelete(id)); - action = action.perform(restruct); + if (needPerform) { + action = action.perform(restruct); + } action.mergeWith(sGroupAttributeAction(id, attrs)); @@ -457,12 +459,12 @@ export function removeAtomFromSgroupIfNeeded(action, restruct, id) { } // Add action operations to remove whole s-group if needed -export function removeSgroupIfNeeded(action, restruct, atoms) { +export function removeSgroupIfNeeded(action, restruct: Restruct, atoms) { const struct = restruct.molecule; const sgCounts = new Map(); - atoms.forEach((id) => { - const sgroups = atomGetSGroups(restruct, id); + atoms.forEach((atomId) => { + const sgroups = atomGetSGroups(restruct, atomId); sgroups.forEach((sid) => { sgCounts.set(sid, sgCounts.has(sid) ? sgCounts.get(sid) + 1 : 1); @@ -470,8 +472,8 @@ export function removeSgroupIfNeeded(action, restruct, atoms) { }); sgCounts.forEach((count, sid) => { - const sG = restruct.sgroups.get(sid).item; - const sgAtoms = SGroup.getAtoms(restruct.molecule, sG); + const sGroup = restruct.sgroups.get(sid)?.item; + const sgAtoms = SGroup.getAtoms(restruct.molecule, sGroup); if (sgAtoms.length === count) { // delete whole s-group @@ -483,6 +485,13 @@ export function removeSgroupIfNeeded(action, restruct, atoms) { }); action.addOp(new SGroupDelete(sid)); } + + if ( + sGroup?.isSuperatomWithoutLabel && + sGroup.getAttachmentPoints().length === 0 + ) { + action.mergeWith(fromSgroupDeletion(restruct, sid, false)); + } }); } @@ -516,3 +525,27 @@ export function fromSgroupAttachmentPointAddition( return action; } + +export function fromSgroupAttachmentPointRemove( + restruct: Restruct, + sgroupId: number, + atomId: number, + needPerform = true, +) { + let action = new Action(); + const struct = restruct.molecule; + const sgroup = struct.sgroups.get(sgroupId); + const attachmentPoint = sgroup + ?.getAttachmentPoints() + .find((attachmentPoint) => attachmentPoint.atomId === atomId); + + if (sgroup && attachmentPoint) { + action.addOp(new SGroupAttachmentPointRemove(sgroupId, attachmentPoint)); + } + + if (needPerform) { + action = action.perform(restruct); + } + + return action; +} diff --git a/packages/ketcher-core/src/application/editor/operations/sgroup/sgroupAtom.ts b/packages/ketcher-core/src/application/editor/operations/sgroup/sgroupAtom.ts index da7f6600a6..e4833fee4e 100644 --- a/packages/ketcher-core/src/application/editor/operations/sgroup/sgroupAtom.ts +++ b/packages/ketcher-core/src/application/editor/operations/sgroup/sgroupAtom.ts @@ -43,9 +43,7 @@ class SGroupAtomAdd extends BaseOperation { const sgroup = struct.sgroups.get(sgid)!; if (sgroup.atoms.indexOf(aid) >= 0) { - throw new Error( - 'The same atom cannot be added to an S-group more than once', - ); + return; } if (!atom) { diff --git a/packages/ketcher-core/src/application/formatters/types/ket.ts b/packages/ketcher-core/src/application/formatters/types/ket.ts index 0c14b3b901..dafdd12dca 100644 --- a/packages/ketcher-core/src/application/formatters/types/ket.ts +++ b/packages/ketcher-core/src/application/formatters/types/ket.ts @@ -18,12 +18,20 @@ export interface IKetGroupNode { export type KetNode = IKetMonomerNode | IKetGroupNode; -export interface IKetConnectionEndPoint { - monomerId?: string; +export interface IKetConnectionMonomerEndPoint { + monomerId: string; attachmentPointId?: string; groupId?: string; } +export interface IKetConnectionMoleculeEndPoint { + moleculeId: string; + atomId: number; +} + +export type IKetConnectionEndPoint = IKetConnectionMonomerEndPoint & + IKetConnectionMoleculeEndPoint; + export enum KetConnectionType { SINGLE = 'single', HYDROGEN = 'hydrogen', diff --git a/packages/ketcher-core/src/domain/entities/BaseMonomer.ts b/packages/ketcher-core/src/domain/entities/BaseMonomer.ts index 77f8977266..06e6ad55ae 100644 --- a/packages/ketcher-core/src/domain/entities/BaseMonomer.ts +++ b/packages/ketcher-core/src/domain/entities/BaseMonomer.ts @@ -4,10 +4,7 @@ import { AttachmentPointName, MonomerItemType } from 'domain/types'; import { PolymerBond } from 'domain/entities/PolymerBond'; import { BaseMonomerRenderer } from 'application/render/renderers/BaseMonomerRenderer'; import { BaseRenderer } from 'application/render/renderers/BaseRenderer'; -import { - getAttachmentPointLabel, - getAttachmentPointLabelWithBinaryShift, -} from 'domain/helpers/attachmentPointCalculations'; +import { getAttachmentPointLabel } from 'domain/helpers/attachmentPointCalculations'; import assert from 'assert'; import { IKetAttachmentPoint, @@ -315,7 +312,6 @@ export abstract class BaseMonomer extends DrawingEntity { private getAttachmentPointDict(): Partial< Record > { - console.log(this.monomerItem.attachmentPoints); if (this.monomerItem.attachmentPoints) { const { attachmentPointDictionary } = BaseMonomer.getAttachmentPointDictFromMonomerDefinition( diff --git a/packages/ketcher-core/src/domain/entities/atom.ts b/packages/ketcher-core/src/domain/entities/atom.ts index 4d1b797dbb..8e5b37a2db 100644 --- a/packages/ketcher-core/src/domain/entities/atom.ts +++ b/packages/ketcher-core/src/domain/entities/atom.ts @@ -719,6 +719,16 @@ export class Atom extends BaseMicromoleculeEntity { return rad + conn + Math.abs(charge); } + public static getSuperAtomAttachmentPointByAttachmentAtom( + struct: Struct, + atomId: number, + ) { + const sgroup = struct.getGroupFromAtomId(atomId); + return sgroup + ?.getAttachmentPoints() + .find((attachmentPoint) => attachmentPoint.atomId === atomId); + } + public static getSuperAtomAttachmentPointByLeavingGroup( struct: Struct, atomId: number, @@ -729,8 +739,24 @@ export class Atom extends BaseMicromoleculeEntity { .find((attachmentPoint) => attachmentPoint.leaveAtomId === atomId); } - public static isSuperatomLeavingGroupAtom(struct: Struct, atomId: number) { - return Atom.getSuperAtomAttachmentPointByLeavingGroup(struct, atomId); + public static isSuperatomLeavingGroupAtom(struct: Struct, atomId?: number) { + if (atomId === undefined) { + return false; + } + + return Boolean( + Atom.getSuperAtomAttachmentPointByLeavingGroup(struct, atomId), + ); + } + + public static isSuperatomAttachmentAtom(struct: Struct, atomId?: number) { + if (atomId === undefined) { + return false; + } + + return Boolean( + Atom.getSuperAtomAttachmentPointByAttachmentAtom(struct, atomId), + ); } public static isAttachmentAtomHasExternalConnections( diff --git a/packages/ketcher-core/src/domain/entities/functionalGroup.ts b/packages/ketcher-core/src/domain/entities/functionalGroup.ts index 04e1526a52..8d0bc5fa00 100644 --- a/packages/ketcher-core/src/domain/entities/functionalGroup.ts +++ b/packages/ketcher-core/src/domain/entities/functionalGroup.ts @@ -118,7 +118,10 @@ export class FunctionalGroup { isFunctionalGroupReturned?: boolean, ): number | FunctionalGroup | null { for (const fg of functionalGroups.values()) { - if (fg.relatedSGroup.atoms.includes(atomId)) + if ( + !fg.relatedSGroup.isSuperatomWithoutLabel && + fg.relatedSGroup.atoms.includes(atomId) + ) return isFunctionalGroupReturned ? fg : fg.relatedSGroupId; } return null; @@ -145,7 +148,7 @@ export class FunctionalGroup { ): FunctionalGroup | number | null { for (const fg of functionalGroups.values()) { const bonds = SGroup.getBonds(molecule, fg.relatedSGroup); - if (bonds.includes(bondId)) { + if (!fg.relatedSGroup.isSuperatomWithoutLabel && bonds.includes(bondId)) { return isFunctionalGroupReturned ? fg : fg.relatedSGroupId; } } diff --git a/packages/ketcher-core/src/domain/entities/struct.ts b/packages/ketcher-core/src/domain/entities/struct.ts index 19f5eeb5b7..70d7282206 100644 --- a/packages/ketcher-core/src/domain/entities/struct.ts +++ b/packages/ketcher-core/src/domain/entities/struct.ts @@ -1166,6 +1166,12 @@ export class Struct { return null; } + getGroupFromBondId(atomId: number | undefined): SGroup | undefined { + const sgroupId = this.getGroupIdFromBondId(atomId as number); + + return this.sgroups?.get(sgroupId as number); + } + getGroupsIdsFromBondId(bondId: number): number[] { const bond = this.bonds.get(bondId); if (!bond) return []; 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 0fcc7515a6..179c9e4773 100644 --- a/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts +++ b/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts @@ -33,25 +33,25 @@ export function polymerBondToDrawingEntity( secondMonomer, connection.endpoint1.attachmentPointId || getAttachmentPointLabel( - firstMonomer.monomerItem.struct.sgroups + (firstMonomer.monomerItem.struct.sgroups .get(0) - .getAttachmentPoints() + ?.getAttachmentPoints() .findIndex( (attachmentPoint) => attachmentPoint.atomId === atomIdMap.get(connection.endpoint1.atomId), - ) + 1, + ) as number) + 1, ), connection.endpoint2.attachmentPointId || getAttachmentPointLabel( - secondMonomer.monomerItem.struct.sgroups + (secondMonomer.monomerItem.struct.sgroups .get(0) - .getAttachmentPoints() + ?.getAttachmentPoints() .findIndex( (attachmentPoint) => attachmentPoint.atomId === atomIdMap.get(connection.endpoint2.atomId), - ) + 1, + ) as number) + 1, ), ), ); diff --git a/packages/ketcher-core/src/domain/serializers/ket/ketSerializer.ts b/packages/ketcher-core/src/domain/serializers/ket/ketSerializer.ts index bcdd205a18..0fec599c00 100644 --- a/packages/ketcher-core/src/domain/serializers/ket/ketSerializer.ts +++ b/packages/ketcher-core/src/domain/serializers/ket/ketSerializer.ts @@ -30,6 +30,9 @@ import { textToKet } from './toKet/textToKet'; import { textToStruct } from './fromKet/textToStruct'; import { IKetConnection, + IKetConnectionEndPoint, + IKetConnectionMoleculeEndPoint, + IKetConnectionMonomerEndPoint, IKetMacromoleculesContent, IKetMacromoleculesContentRootProperty, IKetMonomerNode, @@ -469,7 +472,10 @@ export class KetSerializer implements Serializer { return this.deserializeToStruct(fileContent); } - getConnectionMonomerEndpoint(monomer: BaseMonomer, polymerBond: PolymerBond) { + getConnectionMonomerEndpoint( + monomer: BaseMonomer, + polymerBond: PolymerBond, + ): IKetConnectionMonomerEndPoint { return { monomerId: setMonomerPrefix(monomer.id), attachmentPointId: monomer.getAttachmentPointByBond(polymerBond), @@ -481,7 +487,7 @@ export class KetSerializer implements Serializer { polymerBond: PolymerBond, monomerToAtomIdMap: Map>, struct: Struct, - ) { + ): IKetConnectionMoleculeEndPoint { const atomId = MacromoleculesConverter.findAttachmentPointAtom( polymerBond, monomer, @@ -572,28 +578,28 @@ export class KetSerializer implements Serializer { connectionType: KetConnectionType.SINGLE, endpoint1: polymerBond.firstMonomer.monomerItem.props .isMicromoleculeFragment - ? this.getConnectionMoleculeEndpoint( + ? (this.getConnectionMoleculeEndpoint( polymerBond.firstMonomer, polymerBond, monomerToAtomIdMap, struct, - ) - : this.getConnectionMonomerEndpoint( + ) as IKetConnectionEndPoint) + : (this.getConnectionMonomerEndpoint( polymerBond.firstMonomer, polymerBond, - ), + ) as IKetConnectionEndPoint), endpoint2: polymerBond.secondMonomer.monomerItem.props .isMicromoleculeFragment - ? this.getConnectionMoleculeEndpoint( + ? (this.getConnectionMoleculeEndpoint( polymerBond.secondMonomer, polymerBond, monomerToAtomIdMap, struct, - ) - : this.getConnectionMonomerEndpoint( + ) as IKetConnectionEndPoint) + : (this.getConnectionMonomerEndpoint( polymerBond.secondMonomer, polymerBond, - ), + ) as IKetConnectionEndPoint), }); }); diff --git a/packages/ketcher-react/src/script/editor/tool/bond.ts b/packages/ketcher-react/src/script/editor/tool/bond.ts index 01e61c2092..0632f55697 100644 --- a/packages/ketcher-react/src/script/editor/tool/bond.ts +++ b/packages/ketcher-react/src/script/editor/tool/bond.ts @@ -91,12 +91,15 @@ class BondTool implements Tool { functionalGroups, id, ); + console.log(fgId); if (fgId !== null && !result.includes(fgId)) { result.push(fgId); } } - this.editor.event.removeFG.dispatch({ fgIds: result }); - return; + if (result.length) { + this.editor.event.removeFG.dispatch({ fgIds: result }); + return; + } } else if (bondResult.length > 0) { for (const id of bondResult) { const fgId = FunctionalGroup.findFunctionalGroupByBond( @@ -108,8 +111,10 @@ class BondTool implements Tool { result.push(fgId); } } - this.editor.event.removeFG.dispatch({ fgIds: result }); - return; + if (result.length) { + this.editor.event.removeFG.dispatch({ fgIds: result }); + return; + } } let attachmentAtomId: number | undefined; diff --git a/packages/ketcher-react/src/script/editor/tool/chain.ts b/packages/ketcher-react/src/script/editor/tool/chain.ts index 5348d3c767..d40e6a8043 100644 --- a/packages/ketcher-react/src/script/editor/tool/chain.ts +++ b/packages/ketcher-react/src/script/editor/tool/chain.ts @@ -93,8 +93,10 @@ class ChainTool implements Tool { result.push(fgId); } } - this.editor.event.removeFG.dispatch({ fgIds: result }); - return; + if (result.length) { + this.editor.event.removeFG.dispatch({ fgIds: result }); + return; + } } else if (bondResult.length > 0) { for (const id of bondResult) { const fgId = FunctionalGroup.findFunctionalGroupByBond( @@ -107,8 +109,10 @@ class ChainTool implements Tool { result.push(fgId); } } - this.editor.event.removeFG.dispatch({ fgIds: result }); - return; + if (result.length) { + this.editor.event.removeFG.dispatch({ fgIds: result }); + return; + } } this.editor.hover(null); diff --git a/packages/ketcher-react/src/script/editor/tool/helper/filterNotInCollapsedSGroup.ts b/packages/ketcher-react/src/script/editor/tool/helper/filterNotInCollapsedSGroup.ts index 3832d8cbd9..ec65eb5a93 100644 --- a/packages/ketcher-react/src/script/editor/tool/helper/filterNotInCollapsedSGroup.ts +++ b/packages/ketcher-react/src/script/editor/tool/helper/filterNotInCollapsedSGroup.ts @@ -1,4 +1,4 @@ -import { Atom, Bond, Struct } from 'ketcher-core'; +import { Struct } from 'ketcher-core'; /** * return only such elements ids that not part of collapsed group @@ -41,34 +41,18 @@ function isNotCollapsedSGroup(groupId: number | null, struct: Struct): boolean { return sGroup.checkAttr('expanded', true); } -export function filterNotSuperatomLeavingGroups( +export function filterNotPartOfSuperatomWithoutLabel( itemsToFilter: { atoms?: number[]; bonds?: number[] }, struct: Struct, ) { return { atoms: itemsToFilter.atoms?.filter((atomId) => { - return !( - Atom.isSuperatomLeavingGroupAtom(struct, atomId) && - Atom.isAttachmentAtomHasExternalConnections(struct, atomId) - ); + return !struct.getGroupFromAtomId(atomId)?.isSuperatomWithoutLabel; }) ?? [], bonds: itemsToFilter.bonds?.filter((bondId) => { - const bond = struct.bonds.get(bondId) as Bond; - const beginSuperatomAttachmentPoint = - Atom.getSuperAtomAttachmentPointByLeavingGroup(struct, bond.begin); - const endSuperatomAttachmentPoint = - Atom.getSuperAtomAttachmentPointByLeavingGroup(struct, bond.end); - - return !( - (beginSuperatomAttachmentPoint && - Atom.isAttachmentAtomHasExternalConnections(struct, bond.begin) && - bond.end === beginSuperatomAttachmentPoint.atomId) || - (endSuperatomAttachmentPoint && - Atom.isAttachmentAtomHasExternalConnections(struct, bond.end) && - bond.begin === endSuperatomAttachmentPoint.atomId) - ); + return !struct.getGroupFromBondId(bondId)?.isSuperatomWithoutLabel; }) ?? [], }; } diff --git a/packages/ketcher-react/src/script/editor/tool/select.ts b/packages/ketcher-react/src/script/editor/tool/select.ts index cf11ad88a7..0882866121 100644 --- a/packages/ketcher-react/src/script/editor/tool/select.ts +++ b/packages/ketcher-react/src/script/editor/tool/select.ts @@ -48,10 +48,7 @@ import { dropAndMerge } from './helper/dropAndMerge'; import { getGroupIdsFromItemArrays } from './helper/getGroupIdsFromItems'; import { updateSelectedAtoms } from 'src/script/ui/state/modal/atoms'; import { updateSelectedBonds } from 'src/script/ui/state/modal/bonds'; -import { - filterNotInContractedSGroup, - filterNotSuperatomLeavingGroups, -} from './helper/filterNotInCollapsedSGroup'; +import { filterNotInContractedSGroup } from './helper/filterNotInCollapsedSGroup'; import { Tool } from './Tool'; import { handleMovingPosibilityCursor } from '../utils'; diff --git a/packages/ketcher-react/src/script/editor/tool/sgroup.ts b/packages/ketcher-react/src/script/editor/tool/sgroup.ts index 5207c32824..a6a688c2ae 100644 --- a/packages/ketcher-react/src/script/editor/tool/sgroup.ts +++ b/packages/ketcher-react/src/script/editor/tool/sgroup.ts @@ -33,6 +33,7 @@ import { isEqual } from 'lodash/fp'; import { selMerge } from './select'; import Editor, { Selection } from '../Editor'; import { Tool } from './Tool'; +import { filterNotPartOfSuperatomWithoutLabel } from './helper/filterNotInCollapsedSGroup'; const searchMaps = [ 'atoms', @@ -55,12 +56,27 @@ class SGroupTool implements Tool { } checkSelection() { - const selection = this.editor.selection() || {}; + let selection = this.editor.selection() || {}; + const struct = this.editor.render.ctab; + const molecule = struct.molecule; + const filteredAtomsAndBonds = filterNotPartOfSuperatomWithoutLabel( + { atoms: selection.atoms, bonds: selection.bonds }, + molecule, + ); + + selection = { + ...selection, + atoms: filteredAtomsAndBonds.atoms, + bonds: filteredAtomsAndBonds.bonds, + }; + + selection = this.editor.selection(selection) || {}; + this.editor.rotateController.rerender(); + this.editor.update(true); if (selection.atoms && selection.bonds) { const selectedAtoms = this.editor.selection()?.atoms; - const struct = this.editor.render.ctab; - const molecule = struct.molecule; + const sgroups: Pool = molecule.sgroups; const newSelected: { atoms: Array; bonds: Array } = { atoms: [], @@ -356,7 +372,12 @@ class SGroupTool implements Tool { const fgId = FunctionalGroup.findFunctionalGroupByAtom( functionalGroups, id, - ) as number; + ); + + if (fgId === null) { + return; + } + const sgroupAtoms = SGroup.getAtoms( molecule, struct.sgroups.get(fgId)?.item, @@ -371,7 +392,12 @@ class SGroupTool implements Tool { molecule, functionalGroups, id, - ) as number; + ); + + if (fgId === null) { + return; + } + const sgroupBonds = SGroup.getBonds( molecule, struct.sgroups.get(fgId)?.item, @@ -426,6 +452,18 @@ class SGroupTool implements Tool { newSelected.atoms.length > 0 ? selMerge(this.lassoHelper.end(event), newSelected, false) : this.lassoHelper.end(event); + + const filteredAtomsAndBonds = filterNotPartOfSuperatomWithoutLabel( + { atoms: selection.atoms, bonds: selection.bonds }, + molecule, + ); + + selection = { + ...selection, + atoms: filteredAtomsAndBonds.atoms, + bonds: filteredAtomsAndBonds.bonds, + }; + this.editor.selection(selection); } else { if (!ci) { diff --git a/packages/ketcher-react/src/script/ui/views/components/ContextMenu/ContextMenuTrigger.tsx b/packages/ketcher-react/src/script/ui/views/components/ContextMenu/ContextMenuTrigger.tsx index 146993515f..ded8ddb710 100644 --- a/packages/ketcher-react/src/script/ui/views/components/ContextMenu/ContextMenuTrigger.tsx +++ b/packages/ketcher-react/src/script/ui/views/components/ContextMenu/ContextMenuTrigger.tsx @@ -50,13 +50,16 @@ const ContextMenuTrigger: React.FC = ({ children }) => { ); functionalGroup !== null && + functionalGroup.relatedSGroup && + !functionalGroup.relatedSGroup.isSuperatomWithoutLabel && selectedFunctionalGroups.set( functionalGroup.relatedSGroupId, functionalGroup, ); - const sGroupId = struct.sgroups.find((_, sGroup) => - sGroup.atoms.includes(atomId), + const sGroupId = struct.sgroups.find( + (_, sGroup) => + !sGroup.isSuperatomWithoutLabel && sGroup.atoms.includes(atomId), ); sGroupId !== null && selectedSGroupsIds.add(sGroupId); diff --git a/packages/ketcher-react/src/script/ui/views/components/ContextMenu/hooks/useRemoveAttachmentPoint.ts b/packages/ketcher-react/src/script/ui/views/components/ContextMenu/hooks/useRemoveAttachmentPoint.ts new file mode 100644 index 0000000000..8cd1f024c0 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/views/components/ContextMenu/hooks/useRemoveAttachmentPoint.ts @@ -0,0 +1,45 @@ +import { fromOneAtomDeletion, fromSgroupDeletion } from 'ketcher-core'; +import { useCallback } from 'react'; +import { useAppContext } from 'src/hooks'; +import Editor from 'src/script/editor'; +import { ItemEventParams } from '../contextMenu.types'; +import { isNumber } from 'lodash'; + +const useRemoveAttachmentPoint = () => { + const { getKetcherInstance } = useAppContext(); + + const handler = useCallback( + async ({ props }: ItemEventParams) => { + const editor = getKetcherInstance().editor as Editor; + const restruct = editor.render.ctab; + const struct = editor.struct(); + const atomId = props?.atomIds?.[0]; + const sgroup = struct.getGroupFromAtomId(atomId); + const attachmentPoints = sgroup?.getAttachmentPoints() || []; + const attachmentPoint = attachmentPoints?.find( + (attachmentPoint) => attachmentPoint.atomId === atomId, + ); + + if (!isNumber(atomId) || !attachmentPoint) { + return; + } + + const action = fromOneAtomDeletion( + restruct, + attachmentPoint.leaveAtomId as number, + ); + + if (sgroup && attachmentPoints.length === 0) { + action.mergeWith(fromSgroupDeletion(restruct, sgroup.id)); + } + + editor.update(action); + editor.selection(null); + }, + [getKetcherInstance], + ); + + return [handler]; +}; + +export default useRemoveAttachmentPoint; diff --git a/packages/ketcher-react/src/script/ui/views/components/ContextMenu/menuItems/AtomMenuItems.tsx b/packages/ketcher-react/src/script/ui/views/components/ContextMenu/menuItems/AtomMenuItems.tsx index 2ed6147d58..8d58974a9d 100644 --- a/packages/ketcher-react/src/script/ui/views/components/ContextMenu/menuItems/AtomMenuItems.tsx +++ b/packages/ketcher-react/src/script/ui/views/components/ContextMenu/menuItems/AtomMenuItems.tsx @@ -15,10 +15,13 @@ import { AtomQueryPropertiesName, AtomQueryProperties, AtomAllAttributeName, + Atom, } from 'ketcher-core'; import { atom } from 'src/script/ui/data/schema/struct-schema'; import styles from '../ContextMenu.module.less'; import useAddAttachmentPoint from '../hooks/useAddAttachmentPoint'; +import { isNumber } from 'lodash'; +import useRemoveAttachmentPoint from '../hooks/useRemoveAttachmentPoint'; const { ringBondCount, @@ -95,10 +98,12 @@ const atomPropertiesForSubMenu: { const AtomMenuItems: FC = (props) => { const [handleEdit] = useAtomEdit(); const [handleAddAttachmentPoint] = useAddAttachmentPoint(); + const [handleRemoveAttachmentPoint] = useRemoveAttachmentPoint(); const [handleStereo, stereoDisabled] = useAtomStereo(); const handleDelete = useDelete(); const { getKetcherInstance } = useAppContext(); const editor = getKetcherInstance().editor as Editor; + const struct = editor.struct(); const getPropertyValue = (key: AtomAllAttributeName) => { const { ctab } = editor.render; @@ -135,6 +140,23 @@ const AtomMenuItems: FC = (props) => { } }; + const onlyOneAtomSelected = props.propsFromTrigger?.atomIds?.length === 1; + const selectedAtomId = props.propsFromTrigger?.atomIds?.[0]; + const sgroup = isNumber(selectedAtomId) + ? struct.getGroupFromAtomId(selectedAtomId) + : undefined; + const atomInSgroupWithLabel = sgroup && !sgroup?.isSuperatomWithoutLabel; + const attachmentPoints = sgroup?.getAttachmentPoints() || []; + const maxAttachmentPointsAmount = attachmentPoints.length >= 8; + const isAtomSuperatomAttachmentPoint = Atom.isSuperatomAttachmentAtom( + struct, + selectedAtomId, + ); + const isAtomSuperatomLeavingGroup = Atom.isSuperatomLeavingGroupAtom( + struct, + selectedAtomId, + ); + return ( <> @@ -142,11 +164,9 @@ const AtomMenuItems: FC = (props) => { ? 'Edit selected atoms...' : 'Edit...'} - Enhanced stereochemistry... - = (props) => { ); })} - - Add attachment point - - - Remove attachment point - + {onlyOneAtomSelected && + !atomInSgroupWithLabel && + !maxAttachmentPointsAmount && + !isAtomSuperatomLeavingGroup && + !isAtomSuperatomAttachmentPoint && ( + + Add attachment point + + )} + {isAtomSuperatomAttachmentPoint && ( + + Remove attachment point + + )} Delete