Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2088 Selection Tool: use rounded rectangles for selection of bonds and atom labels #2602

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 9 additions & 5 deletions packages/ketcher-core/src/application/render/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function defaultOptions(opt) {
if (opt.rotationStep) utils.setFracAngle(opt.rotationStep)
Copy link
Collaborator Author

@KonstantinEpam23 KonstantinEpam23 May 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In options.js we just change the values to suit the new UX


const labelFontSize = Math.ceil(1.9 * (scaleFactor / 6))
const subFontSize = Math.ceil(0.7 * labelFontSize)
const subFontSize = Math.ceil(0.5 * labelFontSize)

const defaultOptions = {
'dearomatize-on-load': false,
Expand Down Expand Up @@ -66,6 +66,8 @@ function defaultOptions(opt) {
fontRLabel: labelFontSize * 1.2,
fontRLogic: labelFontSize * 0.7,

radiusScaleFactor: 0.38,

/* styles */
lineattr: {
stroke: '#000',
Expand All @@ -75,11 +77,13 @@ function defaultOptions(opt) {
},
/* eslint-enable quote-props */
selectionStyle: {
fill: '#7f7',
stroke: 'none'
fill: '#57FF8F',
stroke: '#57FF8F'
},
hoverStyle: {
stroke: '#0c0',
stroke: '#0097A8',
fill: 'transparent',
fillSelected: '#CCFFDD',
'stroke-width': (0.6 * scaleFactor) / 20
},
sgroupBracketStyle: {
Expand All @@ -96,7 +100,7 @@ function defaultOptions(opt) {
'stroke-linecap': 'round',
'stroke-opacity': 0.6
},
atomSelectionPlateRadius: labelFontSize * 1.2,
atomSelectionPlateRadius: labelFontSize,
contractedFunctionalGroupSize: 50
}

Expand Down
54 changes: 41 additions & 13 deletions packages/ketcher-core/src/application/render/restruct/reatom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,41 @@ class ReAtom extends ReObject {
return ret
}

makeHoverPlate(render: Render) {
const paper = render.paper
const options = render.options
getLabeledSelectionContour(render: Render) {
const { paper, ctab: restruct, options } = render
const { fontsz, radiusScaleFactor } = options
const padding = fontsz * radiusScaleFactor
const radius = fontsz * radiusScaleFactor * 2
const box = this.getVBoxObj(restruct.render)!
const ps1 = Scale.obj2scaled(box.p0, restruct.render.options)
const ps2 = Scale.obj2scaled(box.p1, restruct.render.options)
const width = ps2.x - ps1.x
const height = fontsz * 1.23
return paper.rect(
ps1.x - padding,
ps1.y - padding,
width + padding * 2,
height + padding * 2,
radius
)
}

getUnlabeledSelectionContour(render: Render) {
const { paper, options } = render
const { atomSelectionPlateRadius } = options
const ps = Scale.obj2scaled(this.a.pp, options)
return paper.circle(ps.x, ps.y, atomSelectionPlateRadius)
}

getSelectionContour(render: Render) {
return this.showLabel && this.a.implicitH !== 0
? this.getLabeledSelectionContour(render)
: this.getUnlabeledSelectionContour(render)
}

makeHoverPlate(render: Render) {
const atom = this.a
const { options } = render
const sgroups = render.ctab.sgroups
const functionalGroups = render.ctab.molecule.functionalGroups
if (
Expand All @@ -113,15 +143,16 @@ class ReAtom extends ReObject {
) {
return null
}
return paper
.circle(ps.x, ps.y, options.atomSelectionPlateRadius)
.attr(options.hoverStyle)

return this.getSelectionContour(render).attr(options.hoverStyle)
}

makeSelectionPlate(restruct: ReStruct, paper: any, styles: any) {
makeSelectionPlate(restruct: ReStruct) {
const atom = this.a
const sgroups = restruct.render.ctab.sgroups
const functionalGroups = restruct.render.ctab.molecule.functionalGroups
const { render } = restruct
const { options } = render
const sgroups = render.ctab.sgroups
const functionalGroups = render.ctab.molecule.functionalGroups
if (
FunctionalGroup.isAtomInContractedFunctionalGroup(
atom,
Expand All @@ -133,10 +164,7 @@ class ReAtom extends ReObject {
return null
}

const ps = Scale.obj2scaled(this.a.pp, restruct.render.options)
return paper
.circle(ps.x, ps.y, styles.atomSelectionPlateRadius)
.attr(styles.selectionStyle)
return this.getSelectionContour(render).attr(options.selectionStyle)
}

show(restruct: ReStruct, aid: number, options: any): void {
Expand Down
209 changes: 161 additions & 48 deletions packages/ketcher-core/src/application/render/restruct/rebond.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,167 @@ class ReBond extends ReObject {
return true
}

static bondRecalc(bond: ReBond, restruct: ReStruct, options: any): void {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is just converted from member to static function, b/c we need to recalculate bond positions after they moved (it is used later in dropAndMerge.ts)

const render = restruct.render
const atom1 = restruct.atoms.get(bond.b.begin)
const atom2 = restruct.atoms.get(bond.b.end)

if (
!atom1 ||
!atom2 ||
bond.b.hb1 === undefined ||
bond.b.hb2 === undefined
) {
return
}

const p1 = Scale.obj2scaled(atom1.a.pp, render.options)
const p2 = Scale.obj2scaled(atom2.a.pp, render.options)
const hb1 = restruct.molecule.halfBonds.get(bond.b.hb1)
const hb2 = restruct.molecule.halfBonds.get(bond.b.hb2)

if (!hb1?.dir || !hb2?.dir) return

hb1.p = shiftBondEnd(atom1, p1, hb1.dir, 2 * options.lineWidth)
hb2.p = shiftBondEnd(atom2, p2, hb2.dir, 2 * options.lineWidth)
bond.b.center = Vec2.lc2(atom1.a.pp, 0.5, atom2.a.pp, 0.5)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be you can add a comment for lc2 (where it is defined)?
For me it is still a mystery, what it does.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nitvex I actually just moved the bondRecalc function so it is a static method now, its a bit of a mystery for myself what lc2 does 😄 I'll try to figure it out and a dd a comment

Copy link
Collaborator

@yuleicul yuleicul May 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KonstantinEpam23 @Nitvex I thought lc was the abbreviation of Linear Combination. The difference between lc() and lc2() was the number of arguments they could receive. lc() could combine lots of vectors, but lc2() could only combine 2 vectors, so functionality-wise lc2 could be replaced by lc.

bond.b.len = Vec2.dist(p1, p2)
bond.b.sb = options.lineWidth * 5
/* eslint-disable no-mixed-operators */
bond.b.sa = Math.max(bond.b.sb, bond.b.len / 2 - options.lineWidth * 2)
/* eslint-enable no-mixed-operators */
bond.b.angle = (Math.atan2(hb1.dir.y, hb1.dir.x) * 180) / Math.PI
}

drawHover(render: Render) {
const ret = this.makeHoverPlate(render)
render.ctab.addReObjectPath(LayerMap.hovering, this.visel, ret)
return ret
}

getSelectionPoints(render: Render) {
const bond: Bond = this.b
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We render both regular and stereo bonds using the same SVG path, just with slightly different parameters.
Both bond types use two lines and two bezier curves to draw their paths:
Screenshot 2023-05-19 at 15 14 48

const { ctab: restruct, options } = render
const { bondThickness, doubleBondWidth, stereoBondWidth } = options
const regularSelectionThikness = doubleBondWidth + bondThickness

// half-bonds
const halfBondStart = restruct.molecule.halfBonds.get(bond.hb1!)!.p
const halfBondEnd = restruct.molecule.halfBonds.get(bond.hb2!)!.p

const isStereoBond =
bond.stereo !== Bond.PATTERN.STEREO.NONE &&
bond.stereo !== Bond.PATTERN.STEREO.CIS_TRANS

const addStereoPadding = isStereoBond ? stereoBondWidth / 2 : 0
const contourStart = Vec2.getLinePoint(
halfBondEnd,
halfBondStart,
-bondThickness * 2.5 - addStereoPadding
)
const contourEnd = Vec2.getLinePoint(
halfBondStart,
halfBondEnd,
-bondThickness * 2.5
)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2023-05-22 at 09 17 00

const addStart = isStereoBond
? stereoBondWidth * 0.25
: regularSelectionThikness
const addEnd = isStereoBond
? stereoBondWidth + (regularSelectionThikness * 4) / stereoBondWidth
: regularSelectionThikness

const contourPaddedStart = Vec2.getLinePoint(
contourStart,
contourEnd,
addEnd
)
const contourPaddedEnd = Vec2.getLinePoint(
contourEnd,
contourStart,
addStart
)

const startPoint = contourStart.add(new Vec2(addEnd, 0))
const endPoint = contourEnd.add(new Vec2(addStart, 0))
const padStartPoint = contourPaddedStart.add(new Vec2(addEnd, 0))
const padEndPoint = contourPaddedEnd.add(new Vec2(addStart, 0))

const { angle } = bond

const startTop = startPoint.rotateAroundOrigin(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2023-05-22 at 09 49 48

angle + 90,
new Vec2(contourStart.x, contourStart.y)
)
const startBottom = startPoint.rotateAroundOrigin(
angle - 90,
new Vec2(contourStart.x, contourStart.y)
)
const startPadTop = padStartPoint.rotateAroundOrigin(
angle + 90,
contourPaddedStart
)
const startPadBottom = padStartPoint.rotateAroundOrigin(
angle - 90,
contourPaddedStart
)
const endTop = endPoint.rotateAroundOrigin(angle + 90, contourEnd)
const endBottom = endPoint.rotateAroundOrigin(angle - 90, contourEnd)
const endPadTop = padEndPoint.rotateAroundOrigin(
angle + 90,
contourPaddedEnd
)
const endPadBottom = padEndPoint.rotateAroundOrigin(
angle - 90,
contourPaddedEnd
)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hover_selection_exp

return [
startPadTop,
startTop,
endTop,
endPadTop,
endPadBottom,
endBottom,
startPadBottom,
startBottom
]
}

getSelectionContour(render: Render) {
const { paper } = render
const [
startPadTop,
startTop,
endTop,
endPadTop,
endPadBottom,
endBottom,
startPadBottom,
startBottom
] = this.getSelectionPoints(render)

// for a visual representation of the points
// please refer to: ketcher-core/docs/data/hover_selection_exp.png
const pathString = `
M ${startTop.x} ${startTop.y}
L ${endTop.x} ${endTop.y}
C ${endPadTop.x} ${endPadTop.y}, ${endPadBottom.x} ${endPadBottom.y}, ${endBottom.x} ${endBottom.y}
L ${startBottom.x} ${startBottom.y}
C ${startPadBottom.x} ${startPadBottom.y}, ${startPadTop.x} ${startPadTop.y}, ${startTop.x} ${startTop.y}
`

return paper.path(pathString)
}

makeHoverPlate(render: Render) {
const restruct = render.ctab
const options = render.options
bondRecalc(this, render.ctab, options)
ReBond.bondRecalc(this, restruct, options)
const bond = this.b
const sgroups = render.ctab.sgroups
const functionalGroups = render.ctab.molecule.functionalGroups
const sgroups = restruct.sgroups
const functionalGroups = restruct.molecule.functionalGroups
if (
FunctionalGroup.isBondInContractedFunctionalGroup(
bond,
Expand All @@ -79,14 +228,13 @@ class ReBond extends ReObject {
return null
}

const c = Scale.obj2scaled(this.b.center, options)
return render.paper
.circle(c.x, c.y, 0.8 * options.atomSelectionPlateRadius)
.attr(options.hoverStyle)
const rect = this.getSelectionContour(render)

return rect.attr({ ...options.hoverStyle })
}

makeSelectionPlate(restruct: ReStruct, paper: any, options: any) {
bondRecalc(this, restruct, options)
makeSelectionPlate(restruct: ReStruct, _: any, options: any) {
ReBond.bondRecalc(this, restruct, options)
const bond = this.b
const sgroups = restruct.render.ctab.sgroups
const functionalGroups = restruct.render.ctab.molecule.functionalGroups
Expand All @@ -100,10 +248,9 @@ class ReBond extends ReObject {
return null
}

const c = Scale.obj2scaled(this.b.center, options)
return paper
.circle(c.x, c.y, 0.8 * options.atomSelectionPlateRadius)
.attr(options.selectionStyle)
const rect = this.getSelectionContour(restruct.render)

return rect.attr(options.selectionStyle)
}

show(restruct: ReStruct, bid: number, options: any): void {
Expand Down Expand Up @@ -131,7 +278,7 @@ class ReBond extends ReObject {
this.b.hb2 !== undefined ? struct.halfBonds.get(this.b.hb2) : null

checkStereoBold(bid, this, restruct)
bondRecalc(this, restruct, options)
ReBond.bondRecalc(this, restruct, options)
setDoubleBondShift(this, struct)
if (!hb1 || !hb2) return
this.path = getBondPath(restruct, this, hb1, hb2)
Expand Down Expand Up @@ -261,8 +408,6 @@ class ReBond extends ReObject {
visel: this.visel
})
}

this.path.toBack()
}
}

Expand Down Expand Up @@ -1000,38 +1145,6 @@ function setDoubleBondShift(bond: ReBond, struct: Struct): void {
}
}

function bondRecalc(bond: ReBond, restruct: ReStruct, options: any): void {
const render = restruct.render
const atom1 = restruct.atoms.get(bond.b.begin)
const atom2 = restruct.atoms.get(bond.b.end)

if (
!atom1 ||
!atom2 ||
bond.b.hb1 === undefined ||
bond.b.hb2 === undefined
) {
return
}

const p1 = Scale.obj2scaled(atom1.a.pp, render.options)
const p2 = Scale.obj2scaled(atom2.a.pp, render.options)
const hb1 = restruct.molecule.halfBonds.get(bond.b.hb1)
const hb2 = restruct.molecule.halfBonds.get(bond.b.hb2)

if (!hb1?.dir || !hb2?.dir) return

hb1.p = shiftBondEnd(atom1, p1, hb1.dir, 2 * options.lineWidth)
hb2.p = shiftBondEnd(atom2, p2, hb2.dir, 2 * options.lineWidth)
bond.b.center = Vec2.lc2(atom1.a.pp, 0.5, atom2.a.pp, 0.5)
bond.b.len = Vec2.dist(p1, p2)
bond.b.sb = options.lineWidth * 5
/* eslint-disable no-mixed-operators */
bond.b.sa = Math.max(bond.b.sb, bond.b.len / 2 - options.lineWidth * 2)
/* eslint-enable no-mixed-operators */
bond.b.angle = (Math.atan2(hb1.dir.y, hb1.dir.x) * 180) / Math.PI
}

function shiftBondEnd(
atom: ReAtom,
pos0: Vec2,
Expand Down
Loading