Skip to content

Commit

Permalink
#1990: Functional group does not connect with another functional grou…
Browse files Browse the repository at this point in the history
…p on click&drag (#2208)

* #1990 - Detect if group is attached to smth + get attachment atom

* #1990 - template tool mousedown() refactor

* #1990 - Functional group does not connect with another functional group on click&drag

* #1990 - #2195 merge fixes

* #1990 - cleanup

* #1990 - hotfix

* #1990 - fixes and clarifications after review

---------

Co-authored-by: Stanislav Permiakov <Stanislav.Permiakov@primark.onmicrosoft.com>
Co-authored-by: Stanislav Permiakov <stanislav_permiakov@epam.com>
  • Loading branch information
3 people authored and ansivgit committed Feb 22, 2023
1 parent 48eff72 commit beb9651
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ export function getHoverToFuse(items) {

const hoverItems = {
atoms: Array.from(items.atoms.values()),
bonds: Array.from(items.bonds.values())
bonds: Array.from(items.bonds.values()),
...(items.functionalGroups && {
functionalGroups: Array.from(items.functionalGroups.values())
})
}

return { map: 'merge', id: +Date.now(), items: hoverItems }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Atom, Vec2 } from 'domain/entities'
import { AtomAdd, BondAdd, CalcImplicitH } from '../operations'
import { atomForNewBond, atomGetAttr } from './utils'
import { fromAtomsAttrs, mergeSgroups } from './atom'
import { fromBondAddition, fromBondStereoUpdate, fromBondsAttrs } from './bond'
import { fromBondStereoUpdate, fromBondsAttrs, fromBondAddition } from './bond'

import { Action } from './action'
import closest from '../shared/closest'
Expand Down Expand Up @@ -83,6 +83,8 @@ export function fromTemplateOnAtom(restruct, template, aid, angle, extraBond) {
const tmpl = template.molecule
const struct = restruct.molecule

const isTmplSingleGroup = template.molecule.isSingleGroup()

let atom = struct.atoms.get(aid) // aid - the atom that was clicked on
let aid1 = aid // aid1 - the atom on the other end of the extra bond || aid

Expand Down Expand Up @@ -134,7 +136,8 @@ export function fromTemplateOnAtom(restruct, template, aid, angle, extraBond) {
pasteItems.atoms.push(operation.data.aid)
}
})
mergeSgroups(action, restruct, pasteItems.atoms, aid)

if (!isTmplSingleGroup) mergeSgroups(action, restruct, pasteItems.atoms, aid)

tmpl.bonds.forEach((bond) => {
const operation = new BondAdd(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,15 @@ class ReBond extends ReObject {
const bond = restruct.molecule.bonds.get(bid)
const sgroups = restruct.molecule.sgroups
const functionalGroups = restruct.molecule.functionalGroups
const sgroupsIds = struct.getGroupsIdsFromBondId(bid)
if (
FunctionalGroup.isBondInContractedFunctionalGroup(
bond,
sgroups,
functionalGroups,
false
)
) &&
sgroupsIds.length < 2
) {
return
}
Expand Down
18 changes: 18 additions & 0 deletions packages/ketcher-core/src/domain/entities/struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,7 @@ export class Struct {
}

// TODO: simplify if bonds ids ever appear in sgroup
// ! deprecate
getGroupIdFromBondId(bondId: number): number | null {
const bond = this.bonds.get(bondId)
if (!bond) return null
Expand All @@ -1092,4 +1093,21 @@ export class Struct {
}
return null
}

getGroupsIdsFromBondId(bondId: number): number[] {
const bond = this.bonds.get(bondId)
if (!bond) return []

const groupsIds: number[] = []

for (const [groupId, sgroup] of Array.from(this.sgroups)) {
if (
sgroup.atoms.includes(bond.begin) ||
sgroup.atoms.includes(bond.end)
) {
groupsIds.push(groupId)
}
}
return groupsIds
}
}
7 changes: 5 additions & 2 deletions packages/ketcher-react/src/script/editor/shared/closest.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,9 +427,12 @@ function findClosestSGroup(restruct, pos) {
return null
}

function findClosestFG(restruct, pos) {
function findClosestFG(restruct, pos, skip) {
const sGroups = restruct.sgroups
for (const reSGroup of sGroups.values()) {
const skipId = skip && skip.map === 'functionalGroups' ? skip.id : null
for (const [reSGroupId, reSGroup] of sGroups.entries()) {
if (reSGroupId === skipId) continue

const { startX, startY, width, height } =
reSGroup.getTextHighlightDimensions()
const { x, y } = Scale.obj2scaled(pos, restruct.render.options)
Expand Down
186 changes: 165 additions & 21 deletions packages/ketcher-react/src/script/editor/tool/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,40 @@
* limitations under the License.
***************************************************************************/

import { fromPaste, getHoverToFuse, getItemsToFuse, Struct } from 'ketcher-core'
import {
fromItemsFuse,
fromPaste,
fromTemplateOnAtom,
getHoverToFuse,
getItemsToFuse,
SGroup,
Struct,
Vec2
} from 'ketcher-core'
import Editor from '../Editor'
import { dropAndMerge } from './helper/dropAndMerge'
import { getGroupIdsFromItemArrays } from './helper/getGroupIdsFromItems'
import { getMergeItems } from './helper/getMergeItems'
import utils from '../shared/utils'

class PasteTool {
editor: Editor
struct: Struct
action: any
templateAction: any
dragCtx: any
findItems: string[]
mergeItems: any
isSingleContractedGroup: boolean

constructor(editor, struct) {
this.editor = editor
this.editor.selection(null)
this.struct = struct

this.isSingleContractedGroup =
struct.isSingleGroup() && !struct.functionalGroups.get(0).isExpanded

const rnd = this.editor.render
const { clientHeight, clientWidth } = rnd.clientArea
const point = this.editor.lastEvent
Expand All @@ -44,33 +61,116 @@ class PasteTool {
this.action = action
this.editor.update(this.action, true)

this.findItems = ['functionalGroups']
this.mergeItems = getItemsToFuse(this.editor, pasteItems)
this.editor.hover(getHoverToFuse(this.mergeItems), this)
}

mousemove(event) {
const rnd = this.editor.render
mousedown(event) {
if (
!this.isSingleContractedGroup ||
SGroup.isSaltOrSolvent(this.struct.sgroups.get(0)?.data.name)
) {
return
}

if (this.action) {
this.action.perform(rnd.ctab)
// remove pasted group from canvas to find closest group correctly
this.action?.perform(this.editor.render.ctab)
}

const [action, pasteItems] = fromPaste(
rnd.ctab,
this.struct,
rnd.page2obj(event)
)
this.action = action
this.editor.update(this.action, true)
const closestGroupItem = this.editor.findItem(event, ['functionalGroups'])
const closestGroup = this.editor.struct().sgroups.get(closestGroupItem.id)

// not dropping on a group (tmp, should be removed when dealing with other entities)
if (!closestGroupItem || SGroup.isSaltOrSolvent(closestGroup?.data.name)) {
// recreate action and continue as usual
const [action] = fromPaste(
this.editor.render.ctab,
this.struct,
this.editor.render.page2obj(event)
)
this.action = action
return
}

// remove action to prevent error when trying to "perform" it again in mousemove
this.action = null

this.mergeItems = getMergeItems(this.editor, pasteItems)
this.editor.hover(getHoverToFuse(this.mergeItems))
this.dragCtx = {
xy0: this.editor.render.page2obj(event),
item: closestGroupItem
}
}

mouseup() {
const struct = this.editor.render.ctab
const molecule = struct.molecule
mousemove(event) {
if (this.action) {
this.action?.perform(this.editor.render.ctab)
}

if (this.dragCtx) {
// template-like logic for group-on-group actions
let pos0: Vec2 | null | undefined = null
const pos1 = this.editor.render.page2obj(event)

const extraBond = true

const targetGroup = this.editor.struct().sgroups.get(this.dragCtx.item.id)
const atomId = targetGroup?.getAttAtomId(this.editor.struct())

if (atomId !== undefined) {
const atom = this.editor.struct().atoms.get(atomId)
pos0 = atom?.pp
}

// calc angle
let angle = utils.calcAngle(pos0, pos1)

if (!event.ctrlKey) {
angle = utils.fracAngle(angle, null)
}

const degrees = utils.degrees(angle)

// check if anything changed since last time
if (
this.dragCtx.hasOwnProperty('angle') &&
this.dragCtx.angle === degrees
)
return

if (this.dragCtx.action) {
this.dragCtx.action.perform(this.editor.render.ctab)
}

this.dragCtx.angle = degrees

const [action] = fromTemplateOnAtom(
this.editor.render.ctab,
prepareTemplateFromSingleGroup(this.struct),
atomId,
angle,
extraBond
)

this.dragCtx.action = action
this.editor.update(this.dragCtx.action, true)
} else {
// common paste logic
const [action, pasteItems] = fromPaste(
this.editor.render.ctab,
this.struct,
this.editor.render.page2obj(event)
)
this.action = action
this.editor.update(this.action, true)

this.mergeItems = getMergeItems(this.editor, pasteItems)
this.editor.hover(getHoverToFuse(this.mergeItems))
}
}

mouseup() {
const idsOfItemsMerged = this.mergeItems && {
...(this.mergeItems.atoms && {
atoms: Array.from(this.mergeItems.atoms.values())
Expand All @@ -81,7 +181,7 @@ class PasteTool {
}

const groupsIdsInvolvedInMerge = getGroupIdsFromItemArrays(
molecule,
this.editor.struct(),
idsOfItemsMerged
)

Expand All @@ -90,10 +190,25 @@ class PasteTool {
return
}

// need to delete action first, because editor.update calls this.cancel() and thus action revert 🤦‍♂️
const action = this.action
delete this.action
dropAndMerge(this.editor, this.mergeItems, action)
if (this.dragCtx) {
const dragCtx = this.dragCtx
delete this.dragCtx

dragCtx.action = dragCtx.action
? fromItemsFuse(this.editor.render.ctab, dragCtx.mergeItems).mergeWith(
dragCtx.action
)
: fromItemsFuse(this.editor.render.ctab, dragCtx.mergeItems)

this.editor.hover(null)
this.editor.update(dragCtx.action)
this.editor.event.message.dispatch({ info: false })
} else {
// need to delete action first, because editor.update calls this.cancel() and thus action revert 🤦‍♂️
const action = this.action
delete this.action
dropAndMerge(this.editor, this.mergeItems, action)
}
}

cancel() {
Expand All @@ -112,4 +227,33 @@ class PasteTool {
}
}

type Template = {
aid?: number
molecule?: Struct
xy0?: Vec2
angle0?: number
}

/** Adds position and angle info to the molecule, similar to Template tool native behavior */
function prepareTemplateFromSingleGroup(molecule: Struct): Template | null {
const template: Template = {}
const sgroup = molecule.sgroups.get(0)
const xy0 = new Vec2()

molecule.atoms.forEach((atom) => {
xy0.add_(atom.pp) // eslint-disable-line no-underscore-dangle
})

template.aid = sgroup?.getAttAtomId(molecule) || 0
template.molecule = molecule
template.xy0 = xy0.scaled(1 / (molecule.atoms.size || 1)) // template center

const atom = molecule.atoms.get(template.aid)
if (atom) {
template.angle0 = utils.calcAngle(atom.pp, template.xy0) // center tilt
}

return template
}

export default PasteTool
Loading

0 comments on commit beb9651

Please sign in to comment.