diff --git a/packages/ketcher-core/src/application/editor/actions/paste.ts b/packages/ketcher-core/src/application/editor/actions/paste.ts index 3bccfa6f3f..bafdceeee9 100644 --- a/packages/ketcher-core/src/application/editor/actions/paste.ts +++ b/packages/ketcher-core/src/application/editor/actions/paste.ts @@ -32,6 +32,10 @@ import { Action } from './action' import { Vec2 } from 'domain/entities' import { fromSgroupAddition } from './sgroup' +export type PasteItems = { + atoms: number[] + bonds: number[] +} export function fromPaste(restruct, pstruct, point, angle = 0) { const xy0 = getStructCenter(pstruct) const offset = Vec2.diff(point, xy0) @@ -41,7 +45,7 @@ export function fromPaste(restruct, pstruct, point, angle = 0) { const aidMap = new Map() const fridMap = new Map() - const pasteItems: any = { + const pasteItems: PasteItems = { // only atoms and bonds now atoms: [], bonds: [] @@ -158,7 +162,7 @@ export function fromPaste(restruct, pstruct, point, angle = 0) { }) action.operations.reverse() - return [action, pasteItems] + return [action, pasteItems] as const } function getStructCenter(struct) { diff --git a/packages/ketcher-core/src/application/editor/actions/template.ts b/packages/ketcher-core/src/application/editor/actions/template.ts index cc5015de0f..2b0411d8d2 100644 --- a/packages/ketcher-core/src/application/editor/actions/template.ts +++ b/packages/ketcher-core/src/application/editor/actions/template.ts @@ -37,7 +37,7 @@ export function fromTemplateOnCanvas(restruct, template, pos, angle) { action.addOp(new CalcImplicitH(pasteItems.atoms).perform(restruct)) - return [action, pasteItems] + return [action, pasteItems] as const } function extraBondAction(restruct, aid, angle) { diff --git a/packages/ketcher-react/src/script/editor/tool/template.ts b/packages/ketcher-react/src/script/editor/tool/template.ts index 82f69879eb..4c030d6531 100644 --- a/packages/ketcher-react/src/script/editor/tool/template.ts +++ b/packages/ketcher-react/src/script/editor/tool/template.ts @@ -29,7 +29,9 @@ import { fromFragmentDeletion, fromPaste, fromSgroupDeletion, - Action + Action, + PasteItems, + Bond } from 'ketcher-core' import utils from '../shared/utils' @@ -37,75 +39,240 @@ import Editor from '../Editor' import { getGroupIdsFromItemArrays } from './helper/getGroupIdsFromItems' import { getMergeItems } from './helper/getMergeItems' -type MergeItems = Record> | null +type TemplateToolOptionProps = { + /** Attachment atom ID */ + atomid?: string + /** Attachment bond ID */ + bondid?: string + /** + * For the Complex Template, it's folder name. + * For the Functional Group, the value is `"Functional Groups"`. + * For the Salt or Solvent, the value is `"Salts and Solvents"` + */ + group: string + /** Only for Complex Templates */ + prerender?: string + name?: string + abbreviation?: string +} + +type TemplateToolOptions = { + struct: Struct + /** Simple Templates don't have it. + * (See type `TemplateCategory` for more details) */ + props?: TemplateToolOptionProps +} + +/** + * - Simple Templates, e.g. Benzene + * - Complex Templates (which in Template Library), e.g. alpha-D-Allopyranose + * - Functional Groups, e.g. Bn + * - Salts and Solvents, e.g formic acid + */ +type TemplateCategory = + | 'Simple Templates' + | 'Complex Templates' + | 'Functional Groups' + | 'Salts and Solvents' + +enum MODE { + /** In this mode, current template is one of `'Simple Templates'` and `'Complex Templates'`. + * (See type `TemplateCategory` for more details) */ + COMPLETE_STRUCT, + /** In this mode, current template is one of `'Functional Groups'` and `'Salts and Solvents'`. + * (See type `TemplateCategory` for more details) */ + ABBREVIATION +} + +type Template = { + /** Attachment atom ID */ + aid: number + /** Attachment bond ID */ + bid: number + molecule: Struct + /** Template center */ + xy0: Vec2 + /** Center tilt */ + angle0?: number + /** Template location sign against attachment bond */ + sign?: -1 | 0 | 1 +} + +type MergeItems = Record> + +type DragContextItem = { + map: string + id: number +} + +type DragContext = { + xy0: Vec2 + mergeItems: MergeItems | null + item?: DragContextItem + action?: Action + angle?: number + extra_bond?: boolean + sign1?: -1 | 1 + sign2?: -1 | 0 | 1 +} class TemplateTool { editor: Editor - mode: any - template: any - findItems: Array - mergeItems: MergeItems = null - dragCtx: any + mode!: MODE + category!: TemplateCategory + template!: Template + findItems!: Array + + mergeItems: MergeItems | null = null + dragCtx?: DragContext targetGroupsIds: Array = [] - isSaltOrSolvent: boolean - followAction: any + followAction?: Action - constructor(editor, tmpl) { + constructor(editor: Editor, options: TemplateToolOptions) { this.editor = editor - this.mode = getTemplateMode(tmpl) this.editor.selection(null) - this.isSaltOrSolvent = SGroup.isSaltOrSolvent(tmpl.struct.name) - this.template = { - aid: parseInt(tmpl.aid) || 0, - bid: parseInt(tmpl.bid) || 0 - } + this.initModeAndCategory(options) + this.initTemplateAndFindItems(options) + } - const frag = tmpl.struct - frag.rescale() + private initTemplateAndFindItems(options: TemplateToolOptions) { + const templateStruct = options.struct + templateStruct.rescale() const xy0 = new Vec2() - frag.atoms.forEach((atom) => { - xy0.add_(atom.pp) // eslint-disable-line no-underscore-dangle + templateStruct.atoms.forEach((atom) => { + xy0.add_(atom.pp) }) - this.template.molecule = frag // preloaded struct - this.findItems = [] - this.template.xy0 = xy0.scaled(1 / (frag.atoms.size || 1)) // template center + this.template = { + aid: options.props?.atomid ? parseInt(options.props.atomid) : 0, + bid: options.props?.bondid ? parseInt(options.props.bondid) : 0, + molecule: templateStruct, + xy0: xy0.scaled(1 / (templateStruct.atoms.size || 1)) + } - const atom = frag.atoms.get(this.template.aid) + this.findItems = [] + const atom = templateStruct.atoms.get(this.template.aid) if (atom) { - this.template.angle0 = utils.calcAngle(atom.pp, this.template.xy0) // center tilt + this.template.angle0 = utils.calcAngle(atom.pp, this.template.xy0) this.findItems.push('atoms') } - const bond = frag.bonds.get(this.template.bid) - if (bond && this.mode !== 'fg') { - // template location sign against attachment bond - this.template.sign = getSign(frag, bond, this.template.xy0) + const bond = templateStruct.bonds.get(this.template.bid) + if (bond && this.mode === MODE.COMPLETE_STRUCT) { + this.template.sign = getSign(templateStruct, bond, this.template.xy0) this.findItems.push('bonds') } - const sgroup = frag.sgroups.size + const sgroup = templateStruct.sgroups.size if (sgroup) { this.findItems.push('functionalGroups') } } + private initModeAndCategory(options: TemplateToolOptions) { + if (!options.props) { + this.mode = MODE.COMPLETE_STRUCT + this.category = 'Simple Templates' + } else if (options.props.group === 'Functional Groups') { + this.mode = MODE.ABBREVIATION + this.category = 'Functional Groups' + } else if (options.props.group === 'Salts and Solvents') { + this.mode = MODE.ABBREVIATION + this.category = 'Salts and Solvents' + } else { + this.mode = MODE.COMPLETE_STRUCT + this.category = 'Complex Templates' + } + } + mousedown(event) { - if (this.followAction) { - this.followAction.perform(this.editor.render.ctab) - delete this.followAction + this.undoFollowAction() + this.mouseDownFunctionalGroups(event) + + const dragCtxItem = getDragCtxItem( + this.editor, + event, + this.mode, + this.mergeItems, + this.findItems + ) + + this.dragCtx = { + xy0: this.editor.render.page2obj(event), + item: dragCtxItem, + mergeItems: null + } + + this.editor.hover(null) + + if (this.mode === MODE.COMPLETE_STRUCT && dragCtxItem?.map === 'bonds') { + // NOTE(by @yuleicul): this if-condition seems always false after #1954 + this.mouseDownBond() + } + } + + private mouseDownBond() { + const closestItem = this.dragCtx?.item + const struct = this.editor.struct() + + // calculate fragment center + const xy0 = new Vec2() + const bond = closestItem && struct.bonds.get(closestItem.id) + const frid = struct.atoms.get(bond?.begin as number)?.fragment + const frIds = struct.getFragmentIds(frid as number) + let count = 0 + + let loop = struct.halfBonds.get(bond?.hb1 as number)?.loop + + if (loop && loop < 0) { + loop = struct.halfBonds.get(bond?.hb2 as number)?.loop + } + + if (loop && loop >= 0) { + const loopHbs = struct.loops.get(loop)?.hbs + loopHbs?.forEach((hb) => { + const halfBondBegin = struct.halfBonds.get(hb)?.begin + + if (halfBondBegin) { + const hbbAtom = struct.atoms.get(halfBondBegin) + + if (hbbAtom) { + xy0.add_(hbbAtom.pp) + count++ + } + } + }) + } else { + frIds.forEach((id) => { + const atomById = struct.atoms.get(id) + + if (atomById) { + xy0.add_(atomById.pp) + count++ + } + }) } + const v0 = xy0.scaled(1 / count) + + // calculate default template flip + if (this.dragCtx && bond) { + this.dragCtx.sign1 = getSign(struct, bond, v0) || 1 + this.dragCtx.sign2 = this.template.sign + } + } + + private mouseDownFunctionalGroups(event) { const closestItem = this.editor.findItem(event, [ 'atoms', 'bonds', 'sgroups', 'functionalGroups' ]) - const ctab = this.editor.render.ctab - const struct = ctab.molecule + + const struct = this.editor.struct() if (struct.functionalGroups.size) { this.targetGroupsIds = getGroupIdsFromItemArrays(struct, { @@ -123,153 +290,145 @@ class TemplateTool { this.targetGroupsIds.push(closestItem.id) } } + } - this.editor.hover(null) - - const dragCtxItem = getDragCtxItem( - this.editor, - event, - this.mode, - this.mergeItems, - this.findItems - ) - - this.dragCtx = { - xy0: this.editor.render.page2obj(event), - item: dragCtxItem + mousemove(event) { + if (!this.dragCtx) { + this.mouseHover(event) + return } - const dragCtx = this.dragCtx - const ci = dragCtx.item - - if (!ci || this.isSaltOrSolvent) { - // ci.type == 'Canvas' - delete dragCtx.item + if (this.category === 'Salts and Solvents') { + delete this.dragCtx.item return } - if (ci.map === 'bonds' && this.mode !== 'fg') { - // calculate fragment center - const xy0 = new Vec2() - const bond = struct.bonds.get(ci.id) - const frid = struct.atoms.get(bond?.begin as number)?.fragment - const frIds = struct.getFragmentIds(frid as number) - let count = 0 - - let loop = struct.halfBonds.get(bond?.hb1 as number)?.loop - - if (loop && loop < 0) { - loop = struct.halfBonds.get(bond?.hb2 as number)?.loop - } - - if (loop && loop >= 0) { - const loopHbs = struct.loops.get(loop)?.hbs - loopHbs?.forEach((hb) => { - const halfBondBegin = struct.halfBonds.get(hb)?.begin - - if (halfBondBegin) { - const hbbAtom = struct.atoms.get(halfBondBegin) - - if (hbbAtom) { - xy0.add_(hbbAtom.pp) // eslint-disable-line no-underscore-dangle, max-len - count++ - } - } - }) - } else { - frIds.forEach((id) => { - const atomById = struct.atoms.get(id) - - if (atomById) { - xy0.add_(atomById.pp) // eslint-disable-line no-underscore-dangle - count++ - } - }) - } - - dragCtx.v0 = xy0.scaled(1 / count) + const ci = this.dragCtx.item + if (this.mode === MODE.COMPLETE_STRUCT && ci?.map === 'bonds') { + // NOTE(by @yuleicul): this if-condition seems always false after #1954 + this.mouseMoveBond(event) + return true + } - const sign = getSign(struct, bond, dragCtx.v0) + const isNothingChanged = this.calculateMouseMovePosition(event) + if (isNothingChanged === true) { + return true + } + const positionData = isNothingChanged + const [targetPos, extraBond, angle, degrees] = positionData - // calculate default template flip - dragCtx.sign1 = sign || 1 - dragCtx.sign2 = this.template.sign + // undo previous action + if (this.dragCtx.action) { + this.dragCtx.action.perform(this.editor.render.ctab) } - } - mousemove(event) { - if (!this.dragCtx) { - if (this.followAction) { - this.followAction.perform(this.editor.render.ctab) - } + // create new action + this.dragCtx.angle = degrees + let action: Action | undefined + let pasteItems: PasteItems | undefined + const struct = this.editor.struct() - const [followAction, pasteItems] = fromPaste( + if (!ci) { + const isAddingFunctionalGroup = this.template?.molecule?.sgroups.size + if (isAddingFunctionalGroup) { + // skip, b/c we dont want to do any additional actions (e.g. rotating for s-groups) + return true + } + ;[action, pasteItems] = fromTemplateOnCanvas( + this.editor.render.ctab, + this.template, + targetPos, + angle + ) + } else if (ci?.map === 'atoms' || ci?.map === 'functionalGroups') { + const atomId = getTargetAtomId(struct, ci) + ;[action, pasteItems] = fromTemplateOnAtom( this.editor.render.ctab, - this.template.molecule, - this.editor.render.page2obj(event) + this.template, + atomId, + angle, + extraBond ) + this.dragCtx.extra_bond = extraBond + } - this.followAction = followAction - this.editor.update(followAction, true, { extendCanvas: false }) + this.dragCtx.action = action + this.dragCtx.action && this.editor.update(this.dragCtx.action, true) - if (this.mode === 'fg') { - const skip = getIgnoredGroupItem(this.editor.struct(), pasteItems) - const ci = this.editor.findItem(event, this.findItems, skip) + if (this.mode === MODE.COMPLETE_STRUCT) { + this.dragCtx.mergeItems = getItemsToFuse(this.editor, pasteItems) + this.editor.hover(getHoverToFuse(this.dragCtx.mergeItems)) + } - this.editor.hover(ci ?? null, null, event) - } else { - this.mergeItems = getMergeItems(this.editor, pasteItems) - this.editor.hover(getHoverToFuse(this.mergeItems)) - } + // TODO: refactor after #2195 comes into effect + if (this.targetGroupsIds.length) this.targetGroupsIds.length = 0 + return true + } + + private mouseMoveBond(event) { + if (!this.dragCtx || !this.dragCtx.item) { return } - const dragCtx = this.dragCtx - const ci = dragCtx.item - let targetPos: Vec2 | null | undefined = null + const closestItem = this.dragCtx.item + + const struct = this.editor.struct() const eventPos = this.editor.render.page2obj(event) - const struct = this.editor.render.ctab.molecule - /* moving when attached to bond */ - if (ci && ci.map === 'bonds' && this.mode !== 'fg') { - const bond = struct.bonds.get(ci.id) - let sign = getSign(struct, bond, eventPos) + const bond = struct.bonds.get(closestItem.id) + if (!bond) { + return + } + let sign: -1 | 0 | 1 = getSign(struct, bond, eventPos) - if (dragCtx.sign1 * this.template.sign > 0) { - sign = -sign - } + if ( + this.dragCtx.sign1 && + this.template.sign && + this.dragCtx.sign1 * this.template.sign > 0 + ) { + sign = -sign as -1 | 0 | 1 + } - if (sign !== dragCtx.sign2 || !dragCtx.action) { - if (dragCtx.action) { - dragCtx.action.perform(this.editor.render.ctab) - } // undo previous action + if (sign !== this.dragCtx.sign2 || !this.dragCtx.action) { + if (this.dragCtx.action) { + this.dragCtx.action.perform(this.editor.render.ctab) + } // undo previous action - dragCtx.sign2 = sign - const [action, pasteItems] = fromTemplateOnBondAction( - this.editor.render.ctab, - this.template, - ci.id, - this.editor.event, - dragCtx.sign1 * dragCtx.sign2 > 0, - false - ) as Array + this.dragCtx.sign2 = sign + const [action, pasteItems] = fromTemplateOnBondAction( + this.editor.render.ctab, + this.template, + closestItem.id, + this.editor.event, + this.dragCtx.sign1 && + this.template.sign && + this.dragCtx.sign1 * this.dragCtx.sign2 > 0, + false + ) as Array - dragCtx.action = action - this.editor.update(dragCtx.action, true) + this.dragCtx.action = action + this.dragCtx.action && this.editor.update(this.dragCtx.action, true) - dragCtx.mergeItems = getItemsToFuse(this.editor, pasteItems) - this.editor.hover(getHoverToFuse(dragCtx.mergeItems)) - } - return true + this.dragCtx.mergeItems = getItemsToFuse(this.editor, pasteItems) + this.editor.hover(getHoverToFuse(this.dragCtx.mergeItems)) } - /* end */ + } + + /** + * @returns `true`: no `targetPos` or nothing changed + */ + private calculateMouseMovePosition(event) { + let extraBond: boolean | undefined + let targetPos: Vec2 | null | undefined = null + const ci = this.dragCtx?.item + const struct = this.editor.struct() + const eventPos = this.editor.render.page2obj(event) - let extraBond: boolean | null = null // calc initial pos and is extra bond needed if (!ci) { // ci.type == 'Canvas' - targetPos = dragCtx.xy0 + targetPos = this.dragCtx?.xy0 } else if (ci.map === 'atoms' || ci.map === 'functionalGroups') { const atomId = getTargetAtomId(struct, ci) @@ -279,7 +438,9 @@ class TemplateTool { if (targetPos) { extraBond = - this.mode === 'fg' ? true : Vec2.dist(targetPos, eventPos) > 1 + this.mode === MODE.ABBREVIATION + ? true + : Vec2.dist(targetPos, eventPos) > 1 } } } @@ -301,139 +462,80 @@ class TemplateTool { // check if anything changed since last time if ( // eslint-disable-next-line no-prototype-builtins - dragCtx.hasOwnProperty('angle') && - dragCtx.angle === degrees && + this.dragCtx?.hasOwnProperty('angle') && + this.dragCtx.angle === degrees && // eslint-disable-next-line no-prototype-builtins - (!dragCtx.hasOwnProperty('extra_bond') || - dragCtx.extra_bond === extraBond) + (!this.dragCtx.hasOwnProperty('extra_bond') || + this.dragCtx.extra_bond === extraBond) ) { return true } - // undo previous action - if (dragCtx.action) { - dragCtx.action.perform(this.editor.render.ctab) - } - - // create new action - dragCtx.angle = degrees - let action = null - let pasteItems + return [targetPos, extraBond, angle, degrees] as const + } - if (!ci) { - const isAddingFunctionalGroup = this.template?.molecule?.sgroups.size - if (isAddingFunctionalGroup) { - // skip, b/c we dont want to do any additional actions (e.g. rotating for s-groups) - return true - } - ;[action, pasteItems] = fromTemplateOnCanvas( - this.editor.render.ctab, - this.template, - targetPos, - angle - ) - } else if (ci?.map === 'atoms' || ci?.map === 'functionalGroups') { - const atomId = getTargetAtomId(struct, ci) - ;[action, pasteItems] = fromTemplateOnAtom( - this.editor.render.ctab, - this.template, - atomId, - angle, - extraBond - ) - dragCtx.extra_bond = extraBond + private mouseHover(event) { + if (this.followAction) { + this.followAction.perform(this.editor.render.ctab) } - dragCtx.action = action - this.editor.update(dragCtx.action, true) + const [followAction, pasteItems] = fromPaste( + this.editor.render.ctab, + this.template.molecule, + this.editor.render.page2obj(event) + ) - if (this.mode !== 'fg') { - dragCtx.mergeItems = getItemsToFuse(this.editor, pasteItems) - this.editor.hover(getHoverToFuse(dragCtx.mergeItems)) - } + this.followAction = followAction + this.editor.update(followAction, true, { extendCanvas: false }) - // TODO: refactor after #2195 comes into effect - if (this.targetGroupsIds.length) this.targetGroupsIds.length = 0 + if (this.mode === MODE.ABBREVIATION) { + const skip = getIgnoredGroupItem(this.editor.struct(), pasteItems) + const ci = this.editor.findItem(event, this.findItems, skip) - return true + this.editor.hover(ci ?? null, null, event) + } else { + this.mergeItems = getMergeItems(this.editor, pasteItems) + this.editor.hover(getHoverToFuse(this.mergeItems)) + } } mouseup(event) { - const dragCtx = this.dragCtx - - if (this.targetGroupsIds.length && this.mode !== 'fg') { + if (this.targetGroupsIds.length && this.mode === MODE.COMPLETE_STRUCT) { this.editor.event.removeFG.dispatch({ fgIds: this.targetGroupsIds }) return } - if (!dragCtx) { + if (!this.dragCtx) { return true } + const dragCtx = this.dragCtx delete this.dragCtx const restruct = this.editor.render.ctab - const struct = restruct.molecule let ci = dragCtx.item - const functionalGroups = struct.functionalGroups - /* after moving around bond */ - if (dragCtx.action && ci && ci.map === 'bonds' && this.mode !== 'fg') { + if ( + dragCtx.action && + ci?.map === 'bonds' && + this.mode === MODE.COMPLETE_STRUCT + ) { + // NOTE(by @yuleicul): this if-condition seems always false after #1954 dragCtx.action.perform(restruct) // revert drag action - - const promise = fromTemplateOnBondAction( - restruct, - this.template, - ci.id, - this.editor.event, - dragCtx.sign1 * dragCtx.sign2 > 0, - true - ) as Promise - - promise.then(([action, pasteItems]) => { - const mergeItems = getItemsToFuse(this.editor, pasteItems) - action = fromItemsFuse(restruct, mergeItems).mergeWith(action) - this.editor.update(action) - }) + this.mouseUpBond(dragCtx) return true } - /* end */ - - let action, functionalGroupRemoveAction - let pasteItems = null - - if (this.isSaltOrSolvent) { - addSaltsAndSolventsOnCanvasWithoutMerge( - restruct, - this.template, - dragCtx, - this.editor - ) - return true - } else if ( - ci?.map === 'functionalGroups' && - FunctionalGroup.isContractedFunctionalGroup(ci.id, functionalGroups) && - this.mode === 'fg' && - this.targetGroupsIds.length - ) { - functionalGroupRemoveAction = new Action() - const struct = this.editor.struct() - const restruct = this.editor.render.ctab - const functionalGroupToReplace = struct.sgroups.get(ci.id)! - const attachmentAtomId = functionalGroupToReplace.getAttAtomId(struct) - const atomsWithoutAttachmentAtom = SGroup.getAtoms( - struct, - functionalGroupToReplace - ).filter((id) => id !== attachmentAtomId) - functionalGroupRemoveAction.mergeWith(fromSgroupDeletion(restruct, ci.id)) - functionalGroupRemoveAction.mergeWith( - fromFragmentDeletion(restruct, { atoms: atomsWithoutAttachmentAtom }) - ) - - ci = { map: 'atoms', id: attachmentAtomId } + const isSaltAdded = this.mouseUpFunctionalGroup(ci, dragCtx) + if (isSaltAdded === true) { + return } + const functionalGroupRemoveAction = isSaltAdded?.functionalGroupRemoveAction + ci = isSaltAdded?.ci || ci + const struct = restruct.molecule + let action: Action | undefined + let pasteItems: PasteItems | undefined if (!dragCtx.action) { if (!ci) { // ci.type == 'Canvas' @@ -446,25 +548,19 @@ class TemplateTool { dragCtx.action = action } else if (ci.map === 'atoms') { const degree = restruct.atoms.get(ci.id)?.a.neighbors.length - let angle - - if (degree && degree > 1) { - // common case - angle = null - } else if (degree === 1) { - // on chain end - const atom = struct.atoms.get(ci.id) - const neiId = atom && struct.halfBonds.get(atom.neighbors[0])?.end - const nei: any = (neiId || neiId === 0) && struct.atoms.get(neiId) - - angle = event.ctrlKey - ? utils.calcAngle(nei?.pp, atom?.pp) - : utils.fracAngle(utils.calcAngle(nei.pp, atom?.pp), null) - } else { - // on single atom - angle = 0 + + if (degree && degree >= 1 && this.category === 'Salts and Solvents') { + addSaltsAndSolventsOnCanvasWithoutMerge( + restruct, + this.template, + dragCtx, + this.editor + ) + return true } + const angle = this.calculateMouseUpAngle(degree, struct, ci, event) + ;[action, pasteItems] = fromTemplateOnAtom( restruct, this.template, @@ -472,35 +568,26 @@ class TemplateTool { angle, false ) + if (functionalGroupRemoveAction) { action = functionalGroupRemoveAction.mergeWith(action) } - dragCtx.action = action - } else if (ci.map === 'bonds' && this.mode !== 'fg') { - const promise = fromTemplateOnBondAction( - restruct, - this.template, - ci.id, - this.editor.event, - dragCtx.sign1 * dragCtx.sign2 > 0, - true - ) as Promise - - promise.then(([action, pasteItems]) => { - if (this.mode !== 'fg') { - const mergeItems = getItemsToFuse(this.editor, pasteItems) - action = fromItemsFuse(restruct, mergeItems).mergeWith(action) - this.editor.update(action) - } - }) + dragCtx.action = action + } else if (ci.map === 'bonds' && this.mode === MODE.COMPLETE_STRUCT) { + // NOTE(by @yuleicul): this if-condition seems always false after #1954 + this.mouseUpBond(dragCtx) return true } } this.editor.selection(null) - if (!dragCtx.mergeItems && pasteItems && this.mode !== 'fg') { + if ( + !dragCtx.mergeItems && + pasteItems && + this.mode === MODE.COMPLETE_STRUCT + ) { dragCtx.mergeItems = getItemsToFuse(this.editor, pasteItems) } dragCtx.action = dragCtx.action @@ -518,13 +605,115 @@ class TemplateTool { return true } + private calculateMouseUpAngle( + degree: number | undefined, + struct: Struct, + ci: DragContextItem, + event + ) { + let angle: number | null = null + if (degree && degree > 1) { + // common case + angle = null + } else if (degree === 1) { + // on chain end + const atom = struct.atoms.get(ci.id) + const neiId = atom && struct.halfBonds.get(atom.neighbors[0])?.end + const nei = (neiId || neiId === 0) && struct.atoms.get(neiId) + + angle = event.ctrlKey + ? utils.calcAngle(nei && nei.pp, atom?.pp) + : utils.fracAngle(utils.calcAngle(nei && nei.pp, atom?.pp), null) + } else { + // on single atom + angle = 0 + } + return angle + } + + /** + * @returns `true`: salt or solvent is added without merge + */ + private mouseUpFunctionalGroup( + ci: DragContextItem | undefined, + dragCtx: DragContext + ) { + const struct = this.editor.struct() + const functionalGroups = struct.functionalGroups + + if ( + ci?.map === 'functionalGroups' && + FunctionalGroup.isContractedFunctionalGroup(ci.id, functionalGroups) && + this.mode === MODE.ABBREVIATION && + this.targetGroupsIds.length + ) { + const restruct = this.editor.render.ctab + const functionalGroupToReplace = struct.sgroups.get(ci.id)! + + if ( + this.category === 'Salts and Solvents' && + functionalGroupToReplace.isGroupAttached(struct) + ) { + addSaltsAndSolventsOnCanvasWithoutMerge( + restruct, + this.template, + dragCtx, + this.editor + ) + return true + } + + const attachmentAtomId = functionalGroupToReplace.getAttAtomId(struct) + const atomsWithoutAttachmentAtom = SGroup.getAtoms( + struct, + functionalGroupToReplace + ).filter((id) => id !== attachmentAtomId) + + const functionalGroupRemoveAction = new Action() + functionalGroupRemoveAction.mergeWith(fromSgroupDeletion(restruct, ci.id)) + functionalGroupRemoveAction.mergeWith( + fromFragmentDeletion(restruct, { atoms: atomsWithoutAttachmentAtom }) + ) + + return { + ci: { map: 'atoms', id: attachmentAtomId }, + functionalGroupRemoveAction + } + } + + return undefined + } + + private mouseUpBond(dragCtx: DragContext) { + const restruct = this.editor.render.ctab + const ci = dragCtx.item + + const promise = fromTemplateOnBondAction( + restruct, + this.template, + ci?.id, + this.editor.event, + dragCtx.sign1 && dragCtx.sign2 && dragCtx.sign1 * dragCtx.sign2 > 0, + true + ) as Promise + + promise.then(([action, pasteItems]) => { + const mergeItems = getItemsToFuse(this.editor, pasteItems) + action = fromItemsFuse(restruct, mergeItems).mergeWith(action) + this.editor.update(action) + }) + } + cancel(e) { + this.undoFollowAction() + this.mouseup(e) + } + + private undoFollowAction() { if (this.followAction) { this.followAction.perform(this.editor.render.ctab) delete this.followAction } - - this.mouseup(e) } mouseleave(e) { @@ -534,8 +723,8 @@ class TemplateTool { function addSaltsAndSolventsOnCanvasWithoutMerge( restruct: ReStruct, - template: Struct, - dragCtx, + template: Template, + dragCtx: DragContext, editor: Editor ) { const [action] = fromTemplateOnCanvas(restruct, template, dragCtx.xy0, 0) @@ -547,16 +736,9 @@ function addSaltsAndSolventsOnCanvasWithoutMerge( }) } -function getTemplateMode(tmpl) { - if (tmpl.mode) return tmpl.mode - if (['Functional Groups', 'Salts and Solvents'].includes(tmpl.props?.group)) - return 'fg' - return null -} - -function getSign(molecule, bond, v) { - const begin = molecule.atoms.get(bond.begin).pp - const end = molecule.atoms.get(bond.end).pp +function getSign(molecule: Struct, bond: Bond, v: Vec2) { + const begin = molecule.atoms.get(bond.begin)!.pp + const end = molecule.atoms.get(bond.end)!.pp const sign = Vec2.cross(Vec2.diff(begin, end), Vec2.diff(v, end)) @@ -571,7 +753,7 @@ function getSign(molecule, bond, v) { return 0 } -function getTargetAtomId(struct: Struct, ci): number | void { +function getTargetAtomId(struct: Struct, ci: DragContextItem) { if (ci.map === 'atoms') { return ci.id } @@ -580,6 +762,8 @@ function getTargetAtomId(struct: Struct, ci): number | void { const group = struct.sgroups.get(ci.id) return group?.getAttAtomId(struct) } + + return undefined } function getIgnoredGroupItem(struct: Struct, pasteItems) { @@ -590,16 +774,16 @@ function getIgnoredGroupItem(struct: Struct, pasteItems) { function getDragCtxItem( editor: Editor, event, - mode: string, - mergeItems: MergeItems, - findItems -): { map: string; id: number } | null { - if (mode === 'fg') return editor.findItem(event, findItems) + mode: MODE, + mergeItems: MergeItems | null, + findItems: string[] +): DragContextItem | undefined { + if (mode === MODE.ABBREVIATION) return editor.findItem(event, findItems) if (mergeItems?.atoms.size === 1 && mergeItems.bonds.size === 0) { // get ID of single dst (target) atom we are hovering over return { map: 'atoms', id: mergeItems.atoms.values().next().value } } - return null + return undefined } export default TemplateTool