Skip to content

Commit

Permalink
#2246 Multiple bond editing changes bond types to all selected bonds
Browse files Browse the repository at this point in the history
  • Loading branch information
AnastasiiaPlyako committed Mar 15, 2023
1 parent b721f6e commit b95abe8
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 90 deletions.
43 changes: 24 additions & 19 deletions packages/ketcher-react/src/script/editor/tool/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
***************************************************************************/

import {
Action,
SGroup,
fromBondsAttrs,
fromItemsFuse,
fromMultipleMove,
fromTextDeletion,
Expand All @@ -29,7 +27,8 @@ import {
ReStruct,
ReSGroup,
Vec2,
Atom
Atom,
Bond
} from 'ketcher-core'

import LassoHelper from './helper/lasso'
Expand All @@ -42,6 +41,7 @@ import { dropAndMerge } from './helper/dropAndMerge'
import { getGroupIdsFromItemArrays } from './helper/getGroupIdsFromItems'
import { getMergeItems } from './helper/getMergeItems'
import { updateSelectedAtoms } from 'src/script/ui/state/modal/atoms'
import { updateSelectedBonds } from 'src/script/ui/state/modal/bonds'
import {
isCloseToEdgeOfCanvas,
isCloseToEdgeOfScreen,
Expand Down Expand Up @@ -287,7 +287,6 @@ class SelectTool {
const struct = editor.render.ctab
const { molecule, sgroups } = struct
const functionalGroups = molecule.functionalGroups
const rnd = editor.render
const ci = editor.findItem(
event,
['atoms', 'bonds', 'sgroups', 'functionalGroups', 'sgroupData', 'texts'],
Expand Down Expand Up @@ -372,21 +371,13 @@ class SelectTool {
changeAtomPromise
})
} else if (ci.map === 'bonds') {
const bond = rnd.ctab.bonds.get(ci.id)?.b
const rb = editor.event.bondEdit.dispatch(bond)

if (selection?.bonds) {
const action = new Action()
const bondsSelection = selection.bonds
Promise.resolve(rb)
.then((newbond) => {
bondsSelection.forEach((bid) => {
action.mergeWith(fromBondsAttrs(struct, bid, newbond))
})
editor.update(action)
})
.catch(() => null) // w/o changes
}
const bonds = getSelectedBonds(selection, molecule)
const changeBondPromise = editor.event.bondEdit.dispatch(bonds)
updateSelectedBonds({
bonds: selection?.bonds || [],
changeBondPromise,
editor
})
} else if (
(ci.map === 'sgroups' &&
!FunctionalGroup.isFunctionalGroup(molecule.sgroups.get(ci.id))) ||
Expand Down Expand Up @@ -553,13 +544,27 @@ export function getSelectedAtoms(selection, molecule) {
return []
}

export function getSelectedBonds(selection, molecule) {
if (selection?.bonds) {
return mapBondIdsToBonds(selection?.bonds, molecule)
}
return []
}

export function mapAtomIdsToAtoms(atomsIds: number[], molecule): Atom[] {
return atomsIds.map((atomId) => {
const atomOrReAtom = molecule.atoms.get(atomId)
return atomOrReAtom.a || atomOrReAtom
})
}

export function mapBondIdsToBonds(bondsIds: number[], molecule): Bond[] {
return bondsIds.map((bondId) => {
const bondOrReBond = molecule.bonds.get(bondId)
return bondOrReBond?.b || bondOrReBond
})
}

function uniqArray(dest, add, reversible: boolean) {
return add.reduce((_, item) => {
if (reversible) dest = xor(dest, [item])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ function fromBondType(type, stereo) {
)
return caption
}
throw Error('No such bond caption')
return ''
}

const bondCaptionMap = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export const bond = {
type: {
title: 'Type',
enum: [
'',
'single',
'up',
'down',
Expand All @@ -171,6 +172,7 @@ export const bond = {
'dative'
],
enumNames: [
'',
'Single',
'Single Up',
'Single Down',
Expand Down
17 changes: 13 additions & 4 deletions packages/ketcher-react/src/script/ui/state/editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
***************************************************************************/

import {
fromAtom,
fromBond,
fromElement,
fromSgroup,
Expand All @@ -31,7 +32,8 @@ import { debounce } from 'lodash/fp'
import { openDialog } from '../modal'
import { highlightFG } from '../functionalGroups'
import { serverCall } from '../server'
import { generateCommonProperties, isAtomsArray } from '../modal/atoms'
import { isAtomsArray } from '../modal/atoms'
import { generateCommonProperties } from './utils'
import { saveSettings } from '../options'

export default function initEditor(dispatch, getState) {
Expand Down Expand Up @@ -65,7 +67,10 @@ export default function initEditor(dispatch, getState) {
},
onElementEdit: (selem) => {
if (isAtomsArray(selem)) {
const atomAttributes = generateCommonProperties(selem)
const atomAttributes = generateCommonProperties(
selem,
fromAtom(selem[0])
)
return openDialog(dispatch, 'atomProps', {
...atomAttributes,
isMultipleAtoms: true
Expand Down Expand Up @@ -129,8 +134,12 @@ export default function initEditor(dispatch, getState) {
}),

onQuickEdit: (atom) => openDialog(dispatch, 'labelEdit', atom),
onBondEdit: (bond) =>
openDialog(dispatch, 'bondProps', fromBond(bond)).then(toBond),
onBondEdit: (bonds) => {
const bondsAttributes = generateCommonProperties(bonds, bonds[0])
return openDialog(dispatch, 'bondProps', fromBond(bondsAttributes)).then(
toBond
)
},
onRgroupEdit: (rgroup) => {
const struct = getState().editor.struct()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Atom, Bond } from 'ketcher-core'

type partialPropertiesOfElement = Partial<{
[attribute in keyof (Atom | Bond)]: string | number | boolean
}>

// If all elements have the same value, then this value is used as a baseline
// otherwise it is set to an empty string. Afterwards, empty string denotes that the value is not changed
export function generateCommonProperties(
selectedElements: Atom[] | Bond[],
normalizedElement
): partialPropertiesOfElement {
const properties = Object.getOwnPropertyNames(normalizedElement)
const resultElementAttributes: partialPropertiesOfElement = {}
properties.forEach((property) => {
const uniqueValues = new Set()
selectedElements.forEach((element) => {
uniqueValues.add(element[property])
})
const allElementsHaveTheSameValue = uniqueValues.size === 1
if (allElementsHaveTheSameValue) {
resultElementAttributes[property] = normalizedElement[property]
} else {
resultElementAttributes[property] = ''
}
})
return resultElementAttributes
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './generateCommonProperties'
66 changes: 4 additions & 62 deletions packages/ketcher-react/src/script/ui/state/modal/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Action, Atom, fromAtomsAttrs } from 'ketcher-core'
import { fromElement } from '../../data/convert/structconv'
import { updateOnlyChangedProperties } from './utils'

export function isAtomsArray(selectedElements: Atom | Atom[]): boolean {
return (
Expand All @@ -8,64 +8,6 @@ export function isAtomsArray(selectedElements: Atom | Atom[]): boolean {
)
}

type somePropertiesOfAtom = Partial<{
[attribute in keyof Atom]: string | number | boolean
}>

// If all atoms have the same value, then this value is used as a baseline
// otherwise it is set to an empty string. Afterwards, empty string denotes that the value is not changed
export function generateCommonProperties(
selectedElements: Atom[]
): somePropertiesOfAtom {
const normalizedAtom = fromElement(selectedElements[0])
const properties = Object.getOwnPropertyNames(normalizedAtom)
const resultAtomAttributes: somePropertiesOfAtom = {}
properties.forEach((property) => {
const uniqueValues = new Set()
selectedElements.forEach((element) => {
uniqueValues.add(element[property])
})
const allAtomsHaveTheSameValue = uniqueValues.size === 1
if (allAtomsHaveTheSameValue) {
resultAtomAttributes[property] = normalizedAtom[property]
} else {
resultAtomAttributes[property] = ''
}
})
return resultAtomAttributes
}

function castAtomPropToType(property, value) {
const typesMapping = {
charge: Number,
exactChangeFlag: Number,
unsaturatedAtom: Number
}
if (typesMapping[property]) {
return typesMapping[property](value)
}
return value
}

export function updateOnlyChangedProperties(atomId, userChangedAtom, molecule) {
const unchangedAtom = molecule.atoms.get(atomId)
const updatedKeys = Object.getOwnPropertyNames(userChangedAtom).filter(
(key) => userChangedAtom[key] !== ''
)
return Object.getOwnPropertyNames(unchangedAtom).reduce(
(updatedAtom, key) => {
const isPropertyChanged = updatedKeys.includes(key)
if (isPropertyChanged) {
updatedAtom[key] = castAtomPropToType(key, userChangedAtom[key])
} else {
updatedAtom[key] = unchangedAtom[key]
}
return updatedAtom
},
{}
)
}

export function updateSelectedAtoms({
atoms,
changeAtomPromise,
Expand All @@ -84,10 +26,10 @@ export function updateSelectedAtoms({
// TODO: deep compare to not produce dummy, e.g.
// atom.label != attrs.label || !atom.atomList.equals(attrs.atomList)
atoms.forEach((atomId) => {
const unchangedAtom = molecule.atoms.get(atomId)
const atomWithChangedProperties = updateOnlyChangedProperties(
atomId,
userChangedAtom,
molecule
unchangedAtom,
userChangedAtom
)
action.mergeWith(
fromAtomsAttrs(struct, atomId, atomWithChangedProperties, false)
Expand Down
38 changes: 38 additions & 0 deletions packages/ketcher-react/src/script/ui/state/modal/bonds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Action, Bond, fromBondsAttrs } from 'ketcher-core'
import { updateOnlyChangedProperties } from './utils'

export function updateSelectedBonds({
bonds,
changeBondPromise,
editor
}: {
bonds: number[]
changeBondPromise: Promise<Bond>
editor
}) {
const action = new Action()
const struct = editor.render.ctab
const { molecule } = struct
if (bonds) {
Promise.resolve(changeBondPromise)
.then((userChangedBond) => {
bonds.forEach((bondId) => {
const unchangedBond = molecule.bonds.get(bondId)
const bondWithChangedProperties = updateOnlyChangedProperties(
unchangedBond,
userChangedBond
)
action.mergeWith(
fromBondsAttrs(
struct,
bondId,
bondWithChangedProperties as Bond,
false
)
)
})
editor.update(action)
})
.catch(() => null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './updateOnlyChangedProperties'
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function updateOnlyChangedProperties(
unchangedElement,
userChangedElement
) {
const updatedKeys = Object.getOwnPropertyNames(userChangedElement).filter(
(key) => userChangedElement[key] !== ''
)
return Object.getOwnPropertyNames(unchangedElement).reduce(
(updatedElement, key) => {
updatedElement[key] = updatedKeys.includes(key)
? userChangedElement[key]
: unchangedElement[key]
return updatedElement
},
{}
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { fromBondsAttrs } from 'ketcher-core'
import { useCallback } from 'react'
import { useAppContext } from 'src/hooks'
import Editor from 'src/script/editor'
import { updateSelectedBonds } from 'src/script/ui/state/modal/bonds'
import { mapBondIdsToBonds } from 'src/script/editor/tool/select'
import { ItemEventParams } from '../contextMenu.types'
import { noOperation } from '../utils'

Expand All @@ -12,10 +13,11 @@ const useBondEdit = () => {
async ({ props }: ItemEventParams) => {
const editor = getKetcherInstance().editor as Editor
const bondIds = props?.bondIds || []
const bond = editor.render.ctab.bonds.get(bondIds[0])?.b
const molecule = editor.render.ctab
try {
const newBond = await editor.event.bondEdit.dispatch(bond)
editor.update(fromBondsAttrs(editor.render.ctab, bondIds, newBond))
const bonds = mapBondIdsToBonds(bondIds, molecule)
const changeBondPromise = await editor.event.bondEdit.dispatch(bonds)
updateSelectedBonds({ bonds: bondIds, changeBondPromise, editor })
} catch (error) {
noOperation()
}
Expand Down

0 comments on commit b95abe8

Please sign in to comment.