From b95abe8d40a306e6110cbddaf45c04deb85cbfd7 Mon Sep 17 00:00:00 2001 From: AnastasiiaPlyako Date: Wed, 15 Mar 2023 12:45:20 +0400 Subject: [PATCH] #2246 Multiple bond editing changes bond types to all selected bonds --- .../src/script/editor/tool/select.ts | 43 ++++++------ .../src/script/ui/data/convert/structconv.js | 2 +- .../script/ui/data/schema/struct-schema.js | 2 + .../src/script/ui/state/editor/index.js | 17 +++-- .../editor/utils/generateCommonProperties.ts | 28 ++++++++ .../src/script/ui/state/editor/utils/index.ts | 1 + .../src/script/ui/state/modal/atoms.ts | 66 ++----------------- .../src/script/ui/state/modal/bonds.ts | 38 +++++++++++ .../src/script/ui/state/modal/utils/index.ts | 1 + .../utils/updateOnlyChangedProperties.ts | 17 +++++ .../ContextMenu/hooks/useBondEdit.ts | 10 +-- 11 files changed, 135 insertions(+), 90 deletions(-) create mode 100644 packages/ketcher-react/src/script/ui/state/editor/utils/generateCommonProperties.ts create mode 100644 packages/ketcher-react/src/script/ui/state/editor/utils/index.ts create mode 100644 packages/ketcher-react/src/script/ui/state/modal/bonds.ts create mode 100644 packages/ketcher-react/src/script/ui/state/modal/utils/index.ts create mode 100644 packages/ketcher-react/src/script/ui/state/modal/utils/updateOnlyChangedProperties.ts diff --git a/packages/ketcher-react/src/script/editor/tool/select.ts b/packages/ketcher-react/src/script/editor/tool/select.ts index 41e5307943..aaee80a9dc 100644 --- a/packages/ketcher-react/src/script/editor/tool/select.ts +++ b/packages/ketcher-react/src/script/editor/tool/select.ts @@ -15,9 +15,7 @@ ***************************************************************************/ import { - Action, SGroup, - fromBondsAttrs, fromItemsFuse, fromMultipleMove, fromTextDeletion, @@ -29,7 +27,8 @@ import { ReStruct, ReSGroup, Vec2, - Atom + Atom, + Bond } from 'ketcher-core' import LassoHelper from './helper/lasso' @@ -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, @@ -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'], @@ -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))) || @@ -553,6 +544,13 @@ 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) @@ -560,6 +558,13 @@ export function mapAtomIdsToAtoms(atomsIds: number[], molecule): Atom[] { }) } +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]) diff --git a/packages/ketcher-react/src/script/ui/data/convert/structconv.js b/packages/ketcher-react/src/script/ui/data/convert/structconv.js index d70e5c80f3..ace970924e 100644 --- a/packages/ketcher-react/src/script/ui/data/convert/structconv.js +++ b/packages/ketcher-react/src/script/ui/data/convert/structconv.js @@ -232,7 +232,7 @@ function fromBondType(type, stereo) { ) return caption } - throw Error('No such bond caption') + return '' } const bondCaptionMap = { diff --git a/packages/ketcher-react/src/script/ui/data/schema/struct-schema.js b/packages/ketcher-react/src/script/ui/data/schema/struct-schema.js index b8d3c8b4cb..86f32bd09b 100644 --- a/packages/ketcher-react/src/script/ui/data/schema/struct-schema.js +++ b/packages/ketcher-react/src/script/ui/data/schema/struct-schema.js @@ -155,6 +155,7 @@ export const bond = { type: { title: 'Type', enum: [ + '', 'single', 'up', 'down', @@ -171,6 +172,7 @@ export const bond = { 'dative' ], enumNames: [ + '', 'Single', 'Single Up', 'Single Down', diff --git a/packages/ketcher-react/src/script/ui/state/editor/index.js b/packages/ketcher-react/src/script/ui/state/editor/index.js index b2e8bf0599..3dc210f18b 100644 --- a/packages/ketcher-react/src/script/ui/state/editor/index.js +++ b/packages/ketcher-react/src/script/ui/state/editor/index.js @@ -15,6 +15,7 @@ ***************************************************************************/ import { + fromAtom, fromBond, fromElement, fromSgroup, @@ -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) { @@ -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 @@ -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() diff --git a/packages/ketcher-react/src/script/ui/state/editor/utils/generateCommonProperties.ts b/packages/ketcher-react/src/script/ui/state/editor/utils/generateCommonProperties.ts new file mode 100644 index 0000000000..594d152b88 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/state/editor/utils/generateCommonProperties.ts @@ -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 +} diff --git a/packages/ketcher-react/src/script/ui/state/editor/utils/index.ts b/packages/ketcher-react/src/script/ui/state/editor/utils/index.ts new file mode 100644 index 0000000000..42d7be3847 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/state/editor/utils/index.ts @@ -0,0 +1 @@ +export * from './generateCommonProperties' diff --git a/packages/ketcher-react/src/script/ui/state/modal/atoms.ts b/packages/ketcher-react/src/script/ui/state/modal/atoms.ts index 42d5c1f68f..5c51c0ec09 100644 --- a/packages/ketcher-react/src/script/ui/state/modal/atoms.ts +++ b/packages/ketcher-react/src/script/ui/state/modal/atoms.ts @@ -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 ( @@ -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, @@ -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) diff --git a/packages/ketcher-react/src/script/ui/state/modal/bonds.ts b/packages/ketcher-react/src/script/ui/state/modal/bonds.ts new file mode 100644 index 0000000000..5523daf0b7 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/state/modal/bonds.ts @@ -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 + 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) + } +} diff --git a/packages/ketcher-react/src/script/ui/state/modal/utils/index.ts b/packages/ketcher-react/src/script/ui/state/modal/utils/index.ts new file mode 100644 index 0000000000..7ba589db89 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/state/modal/utils/index.ts @@ -0,0 +1 @@ +export * from './updateOnlyChangedProperties' diff --git a/packages/ketcher-react/src/script/ui/state/modal/utils/updateOnlyChangedProperties.ts b/packages/ketcher-react/src/script/ui/state/modal/utils/updateOnlyChangedProperties.ts new file mode 100644 index 0000000000..cd329a24b2 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/state/modal/utils/updateOnlyChangedProperties.ts @@ -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 + }, + {} + ) +} diff --git a/packages/ketcher-react/src/script/ui/views/components/ContextMenu/hooks/useBondEdit.ts b/packages/ketcher-react/src/script/ui/views/components/ContextMenu/hooks/useBondEdit.ts index 3146e8ef2f..4ba4751341 100644 --- a/packages/ketcher-react/src/script/ui/views/components/ContextMenu/hooks/useBondEdit.ts +++ b/packages/ketcher-react/src/script/ui/views/components/ContextMenu/hooks/useBondEdit.ts @@ -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' @@ -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() }