diff --git a/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts b/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts index 9c6a6f6834..5418362d50 100644 --- a/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts +++ b/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts @@ -61,7 +61,7 @@ export class MacromoleculesConverter { return Number(attachmentPointName?.replace('R', '')); } - private static findAttachmentPointAtom( + public static findAttachmentPointAtom( polymerBond: PolymerBond, monomer: BaseMonomer, monomerToAtomIdMap: Map>, diff --git a/packages/ketcher-core/src/application/render/restruct/reatom.ts b/packages/ketcher-core/src/application/render/restruct/reatom.ts index 85aab92041..c812d9e9db 100644 --- a/packages/ketcher-core/src/application/render/restruct/reatom.ts +++ b/packages/ketcher-core/src/application/render/restruct/reatom.ts @@ -150,6 +150,8 @@ class ReAtom extends ReObject { const { options } = render; const sgroups = render.ctab.sgroups; const functionalGroups = render.ctab.molecule.functionalGroups; + const struct = render.ctab.molecule; + const atomId = struct.atoms.keyOf(atom) as number; if ( FunctionalGroup.isAtomInContractedFunctionalGroup( @@ -157,7 +159,9 @@ class ReAtom extends ReObject { sgroups, functionalGroups, true, - ) + ) || + (Atom.isSuperatomLeavingGroupAtom(struct, atomId) && + Atom.isAttachmentAtomHasExternalConnections(struct, atomId)) ) { return null; } @@ -171,13 +175,18 @@ class ReAtom extends ReObject { const { options } = render; const sgroups = render.ctab.sgroups; const functionalGroups = render.ctab.molecule.functionalGroups; + const struct = render.ctab.molecule; + const atomId = struct.atoms.keyOf(atom) as number; + if ( FunctionalGroup.isAtomInContractedFunctionalGroup( atom, sgroups, functionalGroups, true, - ) + ) || + (Atom.isSuperatomLeavingGroupAtom(struct, atomId) && + Atom.isAttachmentAtomHasExternalConnections(struct, atomId)) ) { return null; } diff --git a/packages/ketcher-core/src/application/render/restruct/rebond.ts b/packages/ketcher-core/src/application/render/restruct/rebond.ts index 0d60bc4245..125151606f 100644 --- a/packages/ketcher-core/src/application/render/restruct/rebond.ts +++ b/packages/ketcher-core/src/application/render/restruct/rebond.ts @@ -262,7 +262,8 @@ class ReBond extends ReObject { bond, sgroups, functionalGroups, - ) + ) || + Bond.isBondToHiddenLeavingGroup(restruct.molecule, bond) ) { return null; } @@ -282,7 +283,8 @@ class ReBond extends ReObject { bond, sgroups, functionalGroups, - ) + ) || + Bond.isBondToHiddenLeavingGroup(restruct.molecule, bond) ) { return null; } @@ -299,19 +301,8 @@ class ReBond extends ReObject { const bond = restruct.molecule.bonds.get(bid)!; const sgroups = restruct.molecule.sgroups; const functionalGroups = restruct.molecule.functionalGroups; - const beginSuperatomAttachmentPoint = - Atom.getSuperAtomAttachmentPointByLeavingGroup(struct, bond.begin); - const endSuperatomAttachmentPoint = - Atom.getSuperAtomAttachmentPointByLeavingGroup(struct, bond.end); - if ( - (beginSuperatomAttachmentPoint && - Atom.isAttachmentAtomHasExternalConnections(struct, bond.begin) && - bond.end === beginSuperatomAttachmentPoint.atomId) || - (endSuperatomAttachmentPoint && - Atom.isAttachmentAtomHasExternalConnections(struct, bond.end) && - bond.begin === endSuperatomAttachmentPoint.atomId) - ) { + if (Bond.isBondToHiddenLeavingGroup(struct, bond)) { return; } diff --git a/packages/ketcher-core/src/domain/entities/BaseMonomer.ts b/packages/ketcher-core/src/domain/entities/BaseMonomer.ts index 06a271ab85..77f8977266 100644 --- a/packages/ketcher-core/src/domain/entities/BaseMonomer.ts +++ b/packages/ketcher-core/src/domain/entities/BaseMonomer.ts @@ -412,9 +412,7 @@ export abstract class BaseMonomer extends DrawingEntity { }, type: this.attachmentPointNumberToType[ - getAttachmentPointLabelWithBinaryShift( - superatomAttachmentPointIndex + 1, - ) + superatomAttachmentPointIndex + 1 ] || this.attachmentPointNumberToType.moreThanTwo, }); }, diff --git a/packages/ketcher-core/src/domain/entities/atom.ts b/packages/ketcher-core/src/domain/entities/atom.ts index 531c6e2d45..4d1b797dbb 100644 --- a/packages/ketcher-core/src/domain/entities/atom.ts +++ b/packages/ketcher-core/src/domain/entities/atom.ts @@ -765,6 +765,13 @@ export class Atom extends BaseMicromoleculeEntity { return isAttachmentAtomHasExternalConnection; } + + public static isHiddenLeavingGroupAtom(struct: Struct, atomId: number) { + return ( + Atom.isSuperatomLeavingGroupAtom(struct, atomId) && + Atom.isAttachmentAtomHasExternalConnections(struct, atomId) + ); + } } export function radicalElectrons(radical: any) { diff --git a/packages/ketcher-core/src/domain/entities/bond.ts b/packages/ketcher-core/src/domain/entities/bond.ts index f6c4a8e40f..12b4f1c5ae 100644 --- a/packages/ketcher-core/src/domain/entities/bond.ts +++ b/packages/ketcher-core/src/domain/entities/bond.ts @@ -314,4 +314,20 @@ export class Bond extends BaseMicromoleculeEntity { const sGroupsWithEndAtom = struct.atoms.get(this.end)?.sgs || new Pile(); return sGroupsWithBeginAtom?.intersection(sGroupsWithEndAtom); } + + public static isBondToHiddenLeavingGroup(struct: Struct, bond: 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) + ); + } } diff --git a/packages/ketcher-core/src/domain/entities/struct.ts b/packages/ketcher-core/src/domain/entities/struct.ts index d0f8c07dcb..19f5eeb5b7 100644 --- a/packages/ketcher-core/src/domain/entities/struct.ts +++ b/packages/ketcher-core/src/domain/entities/struct.ts @@ -183,12 +183,16 @@ export class Struct { return atomSet; } - getFragment(fid: number | number[], copyNonFragmentObjects = true): Struct { + getFragment( + fid: number | number[], + copyNonFragmentObjects = true, + aidMap?: Map, + ): Struct { return this.clone( this.getFragmentIds(fid), null, true, - undefined, + aidMap, copyNonFragmentObjects ? undefined : new Pile(), copyNonFragmentObjects ? undefined : new Pile(), copyNonFragmentObjects ? undefined : new Pile(), 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 f684b399ff..0fcc7515a6 100644 --- a/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts +++ b/packages/ketcher-core/src/domain/serializers/ket/fromKet/polymerBondToDrawingEntity.ts @@ -2,33 +2,57 @@ import { IKetConnection } from 'application/formatters/types/ket'; import { Command } from 'domain/entities/Command'; import { DrawingEntitiesManager } from 'domain/entities/DrawingEntitiesManager'; import assert from 'assert'; +import { getAttachmentPointLabel } from 'domain/helpers/attachmentPointCalculations'; export function polymerBondToDrawingEntity( connection: IKetConnection, drawingEntitiesManager: DrawingEntitiesManager, monomerIdsMap: { [monomerIdFromKet: string]: number }, + atomIdMap: Map, ) { const command = new Command(); - // TODO remove assertion when group connections will be supported on indigo side - assert(connection.endpoint1.monomerId); - assert(connection.endpoint2.monomerId); const firstMonomer = drawingEntitiesManager.monomers.get( - Number(monomerIdsMap[connection.endpoint1.monomerId]), + Number( + monomerIdsMap[ + connection.endpoint1.monomerId || connection.endpoint1.moleculeId + ], + ), ); const secondMonomer = drawingEntitiesManager.monomers.get( - Number(monomerIdsMap[connection.endpoint2.monomerId]), + Number( + monomerIdsMap[ + connection.endpoint2.monomerId || connection.endpoint2.moleculeId + ], + ), ); - assert(firstMonomer); assert(secondMonomer); - assert(connection.endpoint1.attachmentPointId); - assert(connection.endpoint2.attachmentPointId); command.merge( drawingEntitiesManager.createPolymerBond( firstMonomer, secondMonomer, - connection.endpoint1.attachmentPointId, - connection.endpoint2.attachmentPointId, + connection.endpoint1.attachmentPointId || + getAttachmentPointLabel( + firstMonomer.monomerItem.struct.sgroups + .get(0) + .getAttachmentPoints() + .findIndex( + (attachmentPoint) => + attachmentPoint.atomId === + atomIdMap.get(connection.endpoint1.atomId), + ) + 1, + ), + connection.endpoint2.attachmentPointId || + getAttachmentPointLabel( + secondMonomer.monomerItem.struct.sgroups + .get(0) + .getAttachmentPoints() + .findIndex( + (attachmentPoint) => + attachmentPoint.atomId === + atomIdMap.get(connection.endpoint2.atomId), + ) + 1, + ), ), ); 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 c7ccc64270..bcdd205a18 100644 --- a/packages/ketcher-core/src/domain/serializers/ket/ketSerializer.ts +++ b/packages/ketcher-core/src/domain/serializers/ket/ketSerializer.ts @@ -66,6 +66,7 @@ import { MacromoleculesConverter } from 'application/editor/MacromoleculesConver import { getAttachmentPointLabelWithBinaryShift } from 'domain/helpers/attachmentPointCalculations'; import { isNumber } from 'lodash'; import { MonomerItemType } from 'domain/types'; +import { PolymerBond } from 'domain/entities/PolymerBond'; function parseNode(node: any, struct: any) { const type = node.type; @@ -197,13 +198,7 @@ export class KetSerializer implements Serializer { connection: IKetConnection, editor: CoreEditor, ) { - if ( - connection.connectionType !== KetConnectionType.SINGLE || - !connection.endpoint1.monomerId || - !connection.endpoint2.monomerId || - !connection.endpoint1.attachmentPointId || - !connection.endpoint2.attachmentPointId - ) { + if (connection.connectionType !== KetConnectionType.SINGLE) { editor.events.error.dispatch('Error during file parsing'); return true; } @@ -413,10 +408,12 @@ export class KetSerializer implements Serializer { const fragments = MacromoleculesConverter.getFragmentsGroupedBySgroup( deserializedMicromolecules, ); + const atomIdMap = new Map(); fragments.forEach((_fragment) => { const fragmentStruct = deserializedMicromolecules.getFragment( _fragment, false, + atomIdMap, ); const fragmentBbox = fragmentStruct.getCoordBoundingBox(); const monomerAdditionCommand = drawingEntitiesManager.addMonomer( @@ -438,6 +435,8 @@ export class KetSerializer implements Serializer { fragmentBbox.max.y - (fragmentBbox.max.y - fragmentBbox.min.y) / 2, ), ); + const monomer = monomerAdditionCommand.operations[0].monomer; + monomerIdsMap[`mol${fragmentNumber - 1}`] = monomer?.id; command.merge(monomerAdditionCommand); fragmentNumber++; }); @@ -449,6 +448,7 @@ export class KetSerializer implements Serializer { connection, drawingEntitiesManager, monomerIdsMap, + atomIdMap, ); command.merge(bondAdditionCommand); break; @@ -469,6 +469,31 @@ export class KetSerializer implements Serializer { return this.deserializeToStruct(fileContent); } + getConnectionMonomerEndpoint(monomer: BaseMonomer, polymerBond: PolymerBond) { + return { + monomerId: setMonomerPrefix(monomer.id), + attachmentPointId: monomer.getAttachmentPointByBond(polymerBond), + }; + } + + getConnectionMoleculeEndpoint( + monomer: BaseMonomer, + polymerBond: PolymerBond, + monomerToAtomIdMap: Map>, + struct: Struct, + ) { + const atomId = MacromoleculesConverter.findAttachmentPointAtom( + polymerBond, + monomer, + monomerToAtomIdMap, + ) as number; + + return { + moleculeId: `mol${struct.atoms.get(atomId)?.fragment}`, + atomId, + }; + } + serializeMacromolecules( struct: Struct, drawingEntitiesManager: DrawingEntitiesManager, @@ -480,13 +505,23 @@ export class KetSerializer implements Serializer { templates: [], }, }; + const monomerToAtomIdMap = new Map>(); drawingEntitiesManager.monomers.forEach((monomer) => { if ( monomer instanceof Chem && monomer.monomerItem.props.isMicromoleculeFragment ) { - monomer.monomerItem.struct.mergeInto(struct); + const atomIdMap = new Map(); + monomer.monomerItem.struct.mergeInto( + struct, + null, + null, + false, + false, + atomIdMap, + ); + monomerToAtomIdMap.set(monomer, atomIdMap); } else { const templateId = monomer.monomerItem.props.id || @@ -533,24 +568,32 @@ export class KetSerializer implements Serializer { }); drawingEntitiesManager.polymerBonds.forEach((polymerBond) => { assert(polymerBond.secondMonomer); - if ( - polymerBond.firstMonomer.monomerItem.props.isMicromoleculeFragment || - polymerBond.secondMonomer.monomerItem.props.isMicromoleculeFragment - ) { - return; - } fileContent.root.connections.push({ connectionType: KetConnectionType.SINGLE, - endpoint1: { - monomerId: setMonomerPrefix(polymerBond.firstMonomer.id), - attachmentPointId: - polymerBond.firstMonomer.getAttachmentPointByBond(polymerBond), - }, - endpoint2: { - monomerId: setMonomerPrefix(polymerBond.secondMonomer.id), - attachmentPointId: - polymerBond.secondMonomer?.getAttachmentPointByBond(polymerBond), - }, + endpoint1: polymerBond.firstMonomer.monomerItem.props + .isMicromoleculeFragment + ? this.getConnectionMoleculeEndpoint( + polymerBond.firstMonomer, + polymerBond, + monomerToAtomIdMap, + struct, + ) + : this.getConnectionMonomerEndpoint( + polymerBond.firstMonomer, + polymerBond, + ), + endpoint2: polymerBond.secondMonomer.monomerItem.props + .isMicromoleculeFragment + ? this.getConnectionMoleculeEndpoint( + polymerBond.secondMonomer, + polymerBond, + monomerToAtomIdMap, + struct, + ) + : this.getConnectionMonomerEndpoint( + polymerBond.secondMonomer, + polymerBond, + ), }); }); @@ -577,6 +620,8 @@ export class KetSerializer implements Serializer { const { serializedMacromolecules, micromoleculesStruct } = this.serializeMacromolecules(new Struct(), drawingEntitiesManager); + micromoleculesStruct.enableInitiallySelected(); + const serializedMicromoleculesStruct = JSON.parse( this.serializeMicromolecules(micromoleculesStruct), ); diff --git a/packages/ketcher-react/src/script/editor/shared/closest.js b/packages/ketcher-react/src/script/editor/shared/closest.js index 4100a22089..31dd09a6ad 100644 --- a/packages/ketcher-react/src/script/editor/shared/closest.js +++ b/packages/ketcher-react/src/script/editor/shared/closest.js @@ -22,6 +22,8 @@ import { SGroup, // eslint-disable-next-line @typescript-eslint/no-unused-vars ReStruct, + Atom, + Bond, } from 'ketcher-core'; const SELECTION_DISTANCE_COEFFICIENT = 0.4; @@ -146,7 +148,8 @@ function findClosestAtom(restruct, pos, skip, minDist) { sGroups, functionalGroups, true, - ) + ) || + Atom.isHiddenLeavingGroupAtom(restruct.molecule, aid) ) { return null; } @@ -202,7 +205,8 @@ function findClosestBond(restruct, pos, skip, minDist, options) { sGroups, functionalGroups, ) || - SGroup.isBondInContractedSGroup(bond.b, sGroups) + SGroup.isBondInContractedSGroup(bond.b, sGroups) || + Bond.isBondToHiddenLeavingGroup(restruct.molecule, bond.b) ) { return null; } diff --git a/packages/ketcher-react/src/script/editor/tool/select.ts b/packages/ketcher-react/src/script/editor/tool/select.ts index 9e264cfbe7..cf11ad88a7 100644 --- a/packages/ketcher-react/src/script/editor/tool/select.ts +++ b/packages/ketcher-react/src/script/editor/tool/select.ts @@ -250,15 +250,9 @@ class SelectTool implements Tool { if (this.#lassoHelper.running()) { const sel = this.#lassoHelper.addPoint(event); - const filteredSelection = filterNotSuperatomLeavingGroups( - { atoms: sel?.atoms, bonds: sel?.bonds }, - restruct.molecule, - ); editor.selection( - !event.shiftKey - ? filteredSelection - : selMerge(filteredSelection, editor.selection(), false), + !event.shiftKey ? sel : selMerge(sel, editor.selection(), false), ); return true; } @@ -293,12 +287,7 @@ class SelectTool implements Tool { const selectedSgroups = selected ? getGroupIdsFromItemArrays(molecule, selected) : []; - let newSelected = getNewSelectedItems(editor, selectedSgroups); - - newSelected = filterNotSuperatomLeavingGroups( - { atoms: newSelected?.atoms, bonds: newSelected?.bonds }, - molecule, - ); + const newSelected = getNewSelectedItems(editor, selectedSgroups); if (this.dragCtx?.stopTapping) this.dragCtx.stopTapping();