From 4d491abe32d4e7b480cb113ff5acb2f5347ed67d Mon Sep 17 00:00:00 2001 From: Yulei Chen Date: Fri, 9 Jun 2023 21:19:09 +0800 Subject: [PATCH] #2584 #2750 - Selection tool: add flip and delete buttons & Rotation Tool: non-selected end of the selected bond should be the rotation center (#2666) * #2584 - Refactor: add type declarations to external module 'subscription' * #2584 - Add new reducer for floating tools * #2584 - Add new component FloatingTools * #2584 - Update position of floating tools * #2584 - Fix position to be correct after scrolling and zooming * #2584 - Fix rotation box to be rerendered after flipping * #2584 - Fix floating tools to update position when scrolling * #2584 - Update erase button's icon to delete * #2584 - Remove transform tool group from left toolbar * #2584 - Rename handlePos to rotateHandlePosition * #2584 - Expose a public method to handle scrolling * #2584 - Make handleCenter private * #2584 - Change Vec2 to {x, y} * #2584 - Remove raphael2view * #2584 - Fix ci * #2584 - Fix ci after merging * #2584 - Support to flip multi structures as a whole * #2584 - Support to flip rxnArrow * #2584 - Support to flip texts and rxnPluses * #2584 - Remove functionality of flipping one single bond * #2584 - Remove unused code * #2584 - Fix flip positions of texts * #2584 - Decouple action generators and UI * #2750 - Fix unselected end of the selected bond to be the center * #2584 - Avoid importing type from ui to editor (code review) --- .../application/editor/actions/fragment.ts | 4 +- .../src/application/editor/actions/rotate.ts | 330 +++++++++--------- .../src/application/editor/actions/utils.ts | 14 +- .../src/application/editor/editor.types.ts | 9 +- .../editor/operations/Text/TextMove.ts | 15 +- .../application/editor/shared/constants.ts | 2 +- .../src/application/render/raphaelRender.ts | 2 +- .../src/icons/files/transform-rotate.svg | 4 - packages/ketcher-react/src/icons/index.tsx | 2 - .../ketcher-react/src/script/editor/Editor.ts | 17 +- .../editor/tool/rotate-controller.test.ts | 21 +- .../script/editor/tool/rotate-controller.ts | 53 ++- .../src/script/editor/tool/rotate.ts | 146 ++++---- .../src/script/ui/action/tools.js | 4 - .../src/script/ui/state/editor/index.js | 15 +- .../script/ui/state/floatingTools/index.ts | 40 +++ .../ui/state/floatingTools/selectors/index.ts | 11 + .../src/script/ui/state/index.js | 4 +- .../components/StructEditor/StructEditor.jsx | 11 + .../FloatingTools/FloatingTools.container.ts | 32 ++ .../FloatingTools/FloatingTools.module.less | 24 ++ .../toolbars/FloatingTools/FloatingTools.tsx | 59 ++++ .../ui/views/toolbars/FloatingTools/index.ts | 1 + .../toolbars/LeftToolbar/LeftToolbar.tsx | 16 +- .../LeftToolbar/Transform/Transform.tsx | 52 --- .../toolbars/LeftToolbar/Transform/index.ts | 17 - .../LeftToolbar/leftToolbarOptions.ts | 7 - .../src/script/ui/views/toolbars/index.ts | 1 + .../script/ui/views/toolbars/toolbar.types.ts | 9 +- packages/ketcher-react/src/typings.d.ts | 30 ++ 30 files changed, 567 insertions(+), 385 deletions(-) delete mode 100644 packages/ketcher-react/src/icons/files/transform-rotate.svg create mode 100644 packages/ketcher-react/src/script/ui/state/floatingTools/index.ts create mode 100644 packages/ketcher-react/src/script/ui/state/floatingTools/selectors/index.ts create mode 100644 packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.container.ts create mode 100644 packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.module.less create mode 100644 packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.tsx create mode 100644 packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/index.ts delete mode 100644 packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/Transform/Transform.tsx delete mode 100644 packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/Transform/index.ts diff --git a/packages/ketcher-core/src/application/editor/actions/fragment.ts b/packages/ketcher-core/src/application/editor/actions/fragment.ts index 10110df3e3..c4a23a5cb8 100644 --- a/packages/ketcher-core/src/application/editor/actions/fragment.ts +++ b/packages/ketcher-core/src/application/editor/actions/fragment.ts @@ -34,7 +34,7 @@ import { fromRGroupFragment, fromUpdateIfThen } from './rgroup' import { Action } from './action' import { fromAtomsFragmentAttr } from './atom' -import { getRelSgroupsBySelection } from './utils' +import { getRelSGroupsBySelection } from './utils' export function fromMultipleMove(restruct, lists, d) { d = new Vec2(d) @@ -84,7 +84,7 @@ export function fromMultipleMove(restruct, lists, d) { }) if (lists.sgroupData && lists.sgroupData.length === 0) { - const sgroups = getRelSgroupsBySelection(restruct, lists.atoms) + const sgroups = getRelSGroupsBySelection(struct, lists.atoms) sgroups.forEach((sg) => { action.addOp(new SGroupDataMove(sg.id, d)) }) diff --git a/packages/ketcher-core/src/application/editor/actions/rotate.ts b/packages/ketcher-core/src/application/editor/actions/rotate.ts index 2a55b31d0a..17122b7a5a 100644 --- a/packages/ketcher-core/src/application/editor/actions/rotate.ts +++ b/packages/ketcher-core/src/application/editor/actions/rotate.ts @@ -18,208 +18,212 @@ import { AtomMove, BondAttr, EnhancedFlagMove, + RxnArrowMove, RxnArrowRotate, RxnPlusMove, SGroupDataMove, TextMove } from '../operations' -import { Bond, Fragment, Pile, Vec2 } from 'domain/entities' -import { - getRelSgroupsBySelection, - structSelection, - isAttachmentBond -} from './utils' - +import { Bond, Fragment, Struct, Vec2 } from 'domain/entities' +import { ReStruct } from 'application/render' +import { getRelSGroupsBySelection, structSelection } from './utils' import { Action } from './action' -import utils from '../shared/utils' +import { Selection } from '../editor.types' -export function fromFlip(restruct, selection, dir, center) { - // eslint-disable-line max-statements - const struct = restruct.molecule +export type FlipDirection = 'horizontal' | 'vertical' +export function fromFlip( + reStruct: ReStruct, + selection: Selection | null, + flipDirection: FlipDirection, + center: Vec2 +) { const action = new Action() + const structToFlip = selection || structSelection(reStruct.molecule) - if (!selection) { - selection = structSelection(struct) + action.mergeWith( + fromStructureFlip(reStruct, structToFlip, flipDirection, center) + ) + + if (structToFlip.rxnArrows) { + action.mergeWith( + fromRxnArrowFlip(reStruct, structToFlip.rxnArrows, flipDirection, center) + ) } - if (!selection.atoms) { - return action.perform(restruct) + if (structToFlip.rxnPluses) { + action.mergeWith( + fromRxnPlusFlip(reStruct, structToFlip.rxnPluses, flipDirection, center) + ) } - const fids = selection.atoms.reduce((acc, aid) => { - const atom = struct.atoms.get(aid) + if (structToFlip.texts) { + action.mergeWith( + fromTextFlip(reStruct, structToFlip.texts, flipDirection, center) + ) + } - if (!acc[atom.fragment]) { - acc[atom.fragment] = [] - } + return action +} - acc[atom.fragment].push(aid) - return acc - }, {}) +function fromRxnArrowFlip( + reStruct: ReStruct, + rxnArrowIds: number[], + flipDirection: FlipDirection, + center: Vec2 +) { + const action = new Action() - const fidsNumberKeys = Object.keys(fids).map((frag) => parseInt(frag, 10)) + rxnArrowIds.forEach((arrowId) => { + const rxnArrow = reStruct.molecule.rxnArrows.get(arrowId) + if (!rxnArrow) { + return + } - const isFragFound = fidsNumberKeys.find((frag) => { - const allFragmentsOfStructure = struct.getFragmentIds(frag) - const selectedFragmentsOfStructure = new Pile(fids[frag]) - const res = allFragmentsOfStructure.equals(selectedFragmentsOfStructure) - return !res + const [start, end] = rxnArrow.pos + const oxAngle = end.sub(start).oxAngle() + const oyAngle = oxAngle - Math.PI / 2 + const rotateAngle = + flipDirection === 'vertical' ? -2 * oxAngle : -2 * oyAngle + action.addOp(new RxnArrowRotate(arrowId, rotateAngle, rxnArrow.center())) + + const difference = flipPointByCenter( + rxnArrow.center(), + center, + flipDirection + ) + action.addOp(new RxnArrowMove(arrowId, difference)) }) - if (typeof isFragFound === 'number') { - return flipPartOfStructure({ - fids, - struct, - restruct, - dir, - action, - selection - }) - } - - return flipStandaloneStructure({ - fids, - struct, - restruct, - center, - dir, - action, - selection - }) + return action.perform(reStruct) } -function getRotationPoint(struct, selection) { - const { bonds } = struct - const isSelectedAtom = (atomId) => selection.atoms.includes(atomId) - const getAttachmentBond = () => { - for (const [bondId, bond] of bonds.entries()) { - if (isAttachmentBond(bond, selection)) { - return [bondId, bond] - } - } - return [null, null] - } - const getRotationPointAtomId = (attachmentBondId, attachmentBond) => { - if (selection.bonds.includes(attachmentBondId)) { - return [attachmentBond.begin, attachmentBond.end].find( - (atomId) => !isSelectedAtom(atomId) - ) +function fromRxnPlusFlip( + reStruct: ReStruct, + rxnPlusIds: number[], + flipDirection: FlipDirection, + center: Vec2 +) { + const action = new Action() + + rxnPlusIds.forEach((plusId) => { + const rxnPlus = reStruct.molecule.rxnPluses.get(plusId) + if (!rxnPlus) { + return } - return [attachmentBond.begin, attachmentBond.end].find(isSelectedAtom) - } - const [attachmentBondId, attachmentBond] = getAttachmentBond() - const rotationPointAtomId = getRotationPointAtomId( - attachmentBondId, - attachmentBond - ) - return struct.atoms.get(rotationPointAtomId).pp + const difference = flipPointByCenter(rxnPlus.pp, center, flipDirection) + action.addOp(new RxnPlusMove(plusId, difference)) + }) + + return action.perform(reStruct) } -function flipBonds(selection, struct, action) { - if (selection.bonds) { - selection.bonds.forEach((bid) => { - const bond = struct.bonds.get(bid) +function fromTextFlip( + reStruct: ReStruct, + textIds: number[], + flipDirection: FlipDirection, + center: Vec2 +) { + const action = new Action() - if (bond.type !== Bond.PATTERN.TYPE.SINGLE) { - return - } + textIds.forEach((textId) => { + const text = reStruct.molecule.texts.get(textId) + if (!text) { + return + } - if (bond.stereo === Bond.PATTERN.STEREO.UP) { - action.addOp(new BondAttr(bid, 'stereo', Bond.PATTERN.STEREO.DOWN)) - return - } + // Note: text has two position properties + // `position`: the middle left point + // `pos`: a box (not bounding box) + // `TextMove` is to move `position`, so we have to calculate the flipped `position` + const textMiddleLeft = text.pos[0] + const textMiddleRight = text.pos[3] + const textCenter = Vec2.centre(textMiddleLeft, textMiddleRight) - if (bond.stereo === Bond.PATTERN.STEREO.DOWN) { - action.addOp(new BondAttr(bid, 'stereo', Bond.PATTERN.STEREO.UP)) - } - }) - } + const difference = flipPointByCenter(textCenter, center, flipDirection) + action.addOp(new TextMove(textId, difference)) + }) + + return action.perform(reStruct) } -function flipPartOfStructure({ - fids, - struct, - restruct, - dir, - action, - selection -}) { - const rotationPoint = getRotationPoint(struct, selection) +function fromStructureFlip( + reStruct: ReStruct, + selection: Selection | null, + flipDirection: FlipDirection, + center: Vec2 +) { + const struct = reStruct.molecule + const action = new Action() - Object.keys(fids).forEach((frag) => { - const fragment = new Pile(fids[frag]) + selection?.atoms?.forEach((atomId) => { + const atom = struct.atoms.get(atomId) + if (!atom) { + return + } - fragment.forEach((aid) => { - const atom = struct.atoms.get(aid) - const d = flipItemByCenter(atom, rotationPoint, dir) - action.addOp(new AtomMove(aid, d)) - }) + const difference = flipPointByCenter(atom.pp, center, flipDirection) + action.addOp(new AtomMove(atomId, difference)) + }) - const sgroups = getRelSgroupsBySelection(restruct, Array.from(fragment)) - sgroups.forEach((sg) => { - const d = flipItemByCenter(sg, rotationPoint, dir) - action.addOp(new SGroupDataMove(sg.id, d)) - }) + const sGroups = getRelSGroupsBySelection(struct, selection?.atoms || []) + sGroups.forEach((sGroup) => { + if (!sGroup.pp) { + return + } + const difference = flipPointByCenter(sGroup.pp, center, flipDirection) + action.addOp(new SGroupDataMove(sGroup.id, difference)) }) - flipBonds(selection, struct, action) + if (selection?.bonds) { + flipBonds(selection.bonds, struct, action) + } - return action.perform(restruct) + return action.perform(reStruct) } -function flipStandaloneStructure({ - fids, - struct, - restruct, - center, - dir, - action, - selection -}) { - Object.keys(fids).forEach((frag) => { - const fragment = new Pile(fids[frag]) - - const bbox = struct.getCoordBoundingBox(fragment) - const calcCenter = - center || - new Vec2((bbox.max.x + bbox.min.x) / 2, (bbox.max.y + bbox.min.y) / 2) - - fragment.forEach((aid) => { - const atom = struct.atoms.get(aid) - const d = flipItemByCenter(atom, calcCenter, dir) - action.addOp(new AtomMove(aid, d)) - }) +function flipBonds(bondIds: number[], struct: Struct, action: Action) { + bondIds.forEach((bondId) => { + const bond = struct.bonds.get(bondId) - const sgroups = getRelSgroupsBySelection(restruct, Array.from(fragment)) - sgroups.forEach((sg) => { - const d = flipItemByCenter(sg, calcCenter, dir) - action.addOp(new SGroupDataMove(sg.id, d)) - }) - }) + if (!bond) { + return + } - flipBonds(selection, struct, action) + if (bond.type !== Bond.PATTERN.TYPE.SINGLE) { + return + } - return action.perform(restruct) + if (bond.stereo === Bond.PATTERN.STEREO.UP) { + action.addOp(new BondAttr(bondId, 'stereo', Bond.PATTERN.STEREO.DOWN)) + return + } + + if (bond.stereo === Bond.PATTERN.STEREO.DOWN) { + action.addOp(new BondAttr(bondId, 'stereo', Bond.PATTERN.STEREO.UP)) + } + }) } -function flipItemByCenter(item, center, dir) { +function flipPointByCenter( + pointToFlip: Vec2, + center: Vec2, + flipDirection: FlipDirection +) { const d = new Vec2() - /* eslint-disable no-mixed-operators */ - if (dir === 'horizontal') { + if (flipDirection === 'horizontal') { d.x = - center.x > item.pp.x - ? 2 * (center.x - item.pp.x) - : -2 * (item.pp.x - center.x) + center.x > pointToFlip.x + ? 2 * (center.x - pointToFlip.x) + : -2 * (pointToFlip.x - center.x) } else { // 'vertical' d.y = - center.y > item.pp.y - ? 2 * (center.y - item.pp.y) - : -2 * (item.pp.y - center.y) + center.y > pointToFlip.y + ? 2 * (center.y - pointToFlip.y) + : -2 * (pointToFlip.y - center.y) } - /* eslint-enable no-mixed-operators */ return d } @@ -240,7 +244,7 @@ export function fromRotate(restruct, selection, center, angle) { }) if (!selection.sgroupData) { - const sgroups = getRelSgroupsBySelection(restruct, selection.atoms) + const sgroups = getRelSGroupsBySelection(struct, selection.atoms) sgroups.forEach((sg) => { action.addOp( @@ -302,26 +306,6 @@ export function fromRotate(restruct, selection, center, angle) { return action.perform(restruct) } -export function fromBondAlign(restruct, bid, dir) { - const struct = restruct.molecule - const bond = struct.bonds.get(bid) - const begin = struct.atoms.get(bond.begin) - const end = struct.atoms.get(bond.end) - - const center = begin.pp.add(end.pp).scaled(0.5) - let angle = utils.calcAngle(begin.pp, end.pp) - const atoms = Array.from(struct.getFragmentIds(begin.fragment)) - - // TODO: choose minimal angle - angle = dir === 'horizontal' ? -angle : Math.PI / 2 - angle - - if (angle === 0 || Math.abs(angle) === Math.PI) { - return fromFlip(restruct, { atoms }, dir, center) - } - - return fromRotate(restruct, { atoms }, center, angle) -} - function rotateDelta(v, center, angle) { let v1 = v.sub(center) v1 = v1.rotate(angle) diff --git a/packages/ketcher-core/src/application/editor/actions/utils.ts b/packages/ketcher-core/src/application/editor/actions/utils.ts index 94bcd82806..077670f955 100644 --- a/packages/ketcher-core/src/application/editor/actions/utils.ts +++ b/packages/ketcher-core/src/application/editor/actions/utils.ts @@ -14,12 +14,13 @@ * limitations under the License. ***************************************************************************/ -import { AtomAttributes, Bond, Vec2 } from 'domain/entities' +import { AtomAttributes, Bond, Struct, Vec2 } from 'domain/entities' import closest from '../shared/closest' import { difference } from 'lodash' import { ReStruct } from 'application/render' import { selectionKeys } from '../shared/constants' +import { Selection } from '../editor.types' type AtomAttributeName = keyof AtomAttributes @@ -53,7 +54,7 @@ export function findStereoAtoms(struct, aids: number[] | undefined): number[] { return aids.filter((aid) => struct.atoms.get(aid).stereoLabel !== null) } -export function structSelection(struct) { +export function structSelection(struct): Selection { return selectionKeys.reduce((res, key) => { res[key] = Array.from(struct[key].keys()) return res @@ -188,8 +189,11 @@ export function atomForNewBond(restruct, id, bond?) { return { atom: a, pos: v } } -export function getRelSgroupsBySelection(restruct, selectedAtoms) { - return restruct.molecule.sgroups.filter( +export function getRelSGroupsBySelection( + struct: Struct, + selectedAtoms: number[] +) { + return struct.sgroups.filter( (_sgid, sg) => !sg.data.attached && !sg.data.absolute && @@ -197,7 +201,7 @@ export function getRelSgroupsBySelection(restruct, selectedAtoms) { ) } -export function isAttachmentBond({ begin, end }: Bond, selection): boolean { +export function isAttachmentBond({ begin, end }: Bond, selection: Selection) { if (!selection.atoms) { return false } diff --git a/packages/ketcher-core/src/application/editor/editor.types.ts b/packages/ketcher-core/src/application/editor/editor.types.ts index 35497fcbc4..a803997222 100644 --- a/packages/ketcher-core/src/application/editor/editor.types.ts +++ b/packages/ketcher-core/src/application/editor/editor.types.ts @@ -16,6 +16,7 @@ import { Action } from 'application/actions' import { Struct } from 'domain/entities' +import { selectionKeys } from './shared/constants' export interface EditorHistory { readonly current?: number @@ -29,12 +30,8 @@ export interface LoadOptions { fragment: boolean } -interface Selection { - atoms?: Array - bonds?: Array - enhancedFlags?: Array - rxnPluses?: Array - rxnArrows?: Array +export type Selection = { + [key in typeof selectionKeys[number]]?: number[] } export interface Editor { diff --git a/packages/ketcher-core/src/application/editor/operations/Text/TextMove.ts b/packages/ketcher-core/src/application/editor/operations/Text/TextMove.ts index 4409532f49..9e3661628b 100644 --- a/packages/ketcher-core/src/application/editor/operations/Text/TextMove.ts +++ b/packages/ketcher-core/src/application/editor/operations/Text/TextMove.ts @@ -38,11 +38,18 @@ export class TextMove extends BaseOperation { const id = this.data.id const difference = this.data.d const item = struct.texts.get(id) + const renderItem = restruct.texts.get(id) - item?.position?.add_(difference) - restruct.texts - .get(id) - ?.visel.translate(Scale.obj2scaled(difference, restruct.render.options)) + if (!item || !renderItem) { + return + } + + item.position.add_(difference) + item.setPos(renderItem.getReferencePoints()) + + renderItem.visel.translate( + Scale.obj2scaled(difference, restruct.render.options) + ) this.data.d = difference.negated() diff --git a/packages/ketcher-core/src/application/editor/shared/constants.ts b/packages/ketcher-core/src/application/editor/shared/constants.ts index 1e0a30fcc1..1b2d75a88f 100644 --- a/packages/ketcher-core/src/application/editor/shared/constants.ts +++ b/packages/ketcher-core/src/application/editor/shared/constants.ts @@ -32,4 +32,4 @@ export const selectionKeys = [ 'rxnPluses', 'simpleObjects', 'texts' -] +] as const diff --git a/packages/ketcher-core/src/application/render/raphaelRender.ts b/packages/ketcher-core/src/application/render/raphaelRender.ts index 1da2408fe1..d11edd8131 100644 --- a/packages/ketcher-core/src/application/render/raphaelRender.ts +++ b/packages/ketcher-core/src/application/render/raphaelRender.ts @@ -85,7 +85,7 @@ export class Render { return Scale.scaled2obj(point, this.options) } - obj2view(vector: Vec2, isRelative: true) { + obj2view(vector: Vec2, isRelative?: boolean) { let p = Scale.obj2scaled(vector, this.options) p = isRelative ? p diff --git a/packages/ketcher-react/src/icons/files/transform-rotate.svg b/packages/ketcher-react/src/icons/files/transform-rotate.svg deleted file mode 100644 index 4426a5b295..0000000000 --- a/packages/ketcher-react/src/icons/files/transform-rotate.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ketcher-react/src/icons/index.tsx b/packages/ketcher-react/src/icons/index.tsx index 51af5f335f..2057274467 100644 --- a/packages/ketcher-react/src/icons/index.tsx +++ b/packages/ketcher-react/src/icons/index.tsx @@ -122,7 +122,6 @@ import TextIcon from './files/text.svg' import TextItalic from './files/text-italic.svg' import TextSubscript from './files/text-subscript.svg' import TextSuperscript from './files/text-superscript.svg' -import TransformRotateIcon from './files/transform-rotate.svg' import TransformFlipHIcon from './files/transform-flip-h.svg' import TransformFlipVIcon from './files/transform-flip-v.svg' import UndoIcon from './files/undo.svg' @@ -366,7 +365,6 @@ const icons = { 'text-superscript': TextSuperscript, 'transform-flip-h': TransformFlipHIcon, 'transform-flip-v': TransformFlipVIcon, - 'transform-rotate': TransformRotateIcon, undo: UndoIcon, 'zoom-in': ZoomInIcon, 'zoom-out': ZoomOutIcon, diff --git a/packages/ketcher-react/src/script/editor/Editor.ts b/packages/ketcher-react/src/script/editor/Editor.ts index 0e2eb1edcb..2c707ca6bd 100644 --- a/packages/ketcher-react/src/script/editor/Editor.ts +++ b/packages/ketcher-react/src/script/editor/Editor.ts @@ -101,6 +101,11 @@ function selectStereoFlagsIfNecessary( return stereoFlags } +export type FloatingToolsParams = { + visible?: boolean + rotateHandlePosition?: { x: number; y: number } +} + export interface Selection { atoms?: Array bonds?: Array @@ -142,6 +147,7 @@ class Editor implements KetcherEditor { showInfo: PipelineSubscription apiSettings: PipelineSubscription cursor: Subscription + updateFloatingTools: Subscription } lastEvent: any @@ -195,7 +201,8 @@ class Editor implements KetcherEditor { confirm: new PipelineSubscription(), cursor: new PipelineSubscription(), showInfo: new PipelineSubscription(), - apiSettings: new PipelineSubscription() + apiSettings: new PipelineSubscription(), + updateFloatingTools: new Subscription() } domEventSetup(this, clientArea) @@ -234,15 +241,15 @@ class Editor implements KetcherEditor { this.hoverIcon.hide(true) } + if (!tool || tool.isNotActiveTool) { + return null + } + const isSelectToolChosen = name === 'select' if (!isSelectToolChosen) { this.rotateController.clean() } - if (!tool || tool.isNotActiveTool) { - return null - } - this._tool = tool return this._tool /* eslint-enable no-underscore-dangle */ diff --git a/packages/ketcher-react/src/script/editor/tool/rotate-controller.test.ts b/packages/ketcher-react/src/script/editor/tool/rotate-controller.test.ts index b21d076ccc..6e7b22cdcd 100644 --- a/packages/ketcher-react/src/script/editor/tool/rotate-controller.test.ts +++ b/packages/ketcher-react/src/script/editor/tool/rotate-controller.test.ts @@ -27,7 +27,8 @@ describe('Rotate controller', () => { expect(tool()).toBeInstanceOf(SelectTool) expect(selection()).toBe(null) - controller.rerender() + // @ts-ignore + controller.show() expect(paper).toBeCalledTimes(0) @@ -35,7 +36,8 @@ describe('Rotate controller', () => { // @ts-ignore controller.rotateTool.getCenter = () => [new Vec2(), visibleAtoms] - expect(controller.rerender).toThrow() + // @ts-ignore + expect(controller.show).toThrow() }) /** @@ -60,7 +62,8 @@ describe('Rotate controller', () => { controller.rotateTool.getCenter = () => [new Vec2(), []] expect(tool()).toBeInstanceOf(SelectTool) - controller.rerender() + // @ts-ignore + controller.show() expect(paper).toBeCalledTimes(0) @@ -68,17 +71,20 @@ describe('Rotate controller', () => { // @ts-ignore controller.rotateTool.getCenter = () => [new Vec2(), []] - controller.rerender() + // @ts-ignore + controller.show() expect(paper).toBeCalledTimes(0) selection = () => ({ texts: [1], rxnPlus: [1], rxnArrow: [] }) - expect(controller.rerender).toThrow() + // @ts-ignore + expect(controller.show).toThrow() selection = () => ({ texts: [], rxnPlus: [], rxnArrow: [1] }) - expect(controller.rerender).toThrow() + // @ts-ignore + expect(controller.show).toThrow() }) /** @@ -100,7 +106,8 @@ describe('Rotate controller', () => { controller.rotateTool.getCenter = () => [new Vec2(), visibleAtoms] expect(visibleAtoms.length).toBeGreaterThan(1) - controller.rerender() + // @ts-ignore + controller.show() expect(paper).toBeCalledTimes(0) }) diff --git a/packages/ketcher-react/src/script/editor/tool/rotate-controller.ts b/packages/ketcher-react/src/script/editor/tool/rotate-controller.ts index 5721efdaba..23ca2c61f3 100644 --- a/packages/ketcher-react/src/script/editor/tool/rotate-controller.ts +++ b/packages/ketcher-react/src/script/editor/tool/rotate-controller.ts @@ -45,7 +45,7 @@ class RotateController { constructor(editor: Editor) { this.editor = editor this.init() - this.rotateTool = new RotateTool(this.editor, undefined, true) + this.rotateTool = new RotateTool(this.editor, undefined) } private init() { @@ -78,6 +78,11 @@ class RotateController { } clean() { + this.editor.event.updateFloatingTools.dispatch({ + visible: false, + rotateHandlePosition: new Vec2() + }) + this.handle?.unhover(this.hoverIn, this.hoverOut) this.handle?.unmousedown(this.dragStart) this.handle?.unmouseup(this.dragEnd) @@ -140,7 +145,7 @@ class RotateController { this.editor ) - const { texts, rxnArrows, rxnPluses } = this.editor.selection() || {} + const { texts, rxnArrows, rxnPluses, bonds } = this.editor.selection() || {} const isMoreThanOneItemBeingSelected = visibleAtoms.concat(texts || [], rxnArrows || [], rxnPluses || []) @@ -161,7 +166,8 @@ class RotateController { visibleAtoms, texts, rxnArrows, - rxnPluses + rxnPluses, + bonds ) this.handleCenter = new Vec2( @@ -169,6 +175,16 @@ class RotateController { rectStartY - STYLE.HANDLE_MARGIN - STYLE.HANDLE_RADIUS ) + const handleCenterInViewport = this.render.obj2view( + this.handleCenter + .sub(this.render.options.offset) + .scaled(1 / this.render.options.scale) + ) + this.editor.event.updateFloatingTools.dispatch({ + visible: true, + rotateHandlePosition: handleCenterInViewport + }) + this.drawLink() this.drawCross() this.drawHandle() @@ -245,7 +261,8 @@ class RotateController { visibleAtoms: number[], texts?: number[], rxnArrows?: number[], - rxnPluses?: number[] + rxnPluses?: number[], + bonds?: number[] ) { const RECT_RADIUS = 20 const RECT_PADDING = 10 @@ -256,10 +273,12 @@ class RotateController { texts, rxnArrows, rxnPluses, + sgroups: getGroupIdsFromItemArrays(this.render.ctab.molecule, { atoms: visibleAtoms - }) - })! + }), + bonds + }) .transform(Scale.obj2scaled, this.render.options) .translate(this.render.options.offset || new Vec2()) @@ -606,6 +625,11 @@ class RotateController { this.isRotating = true + this.editor.event.updateFloatingTools.dispatch({ + visible: false, + rotateHandlePosition: new Vec2() + }) + this.boundingRect?.remove() delete this.boundingRect @@ -629,7 +653,7 @@ class RotateController { const originalHandleCenter = this.handleCenter .sub(this.render.options.offset) .scaled(1 / this.render.options.scale) - this.rotateTool.mousedown(event, originalHandleCenter, this.originalCenter) + this.rotateTool.mousedownHandle(originalHandleCenter, this.originalCenter) } private dragMove = () => { @@ -755,6 +779,21 @@ class RotateController { this.isMovingCenter = false this.rerender() } + + updateFloatingToolsPosition = throttle(() => { + if (!this.handle) { + return + } + + const handleCenterInViewport = this.render.obj2view( + this.handleCenter + .sub(this.render.options.offset) + .scaled(1 / this.render.options.scale) + ) + this.editor.event.updateFloatingTools.dispatch({ + rotateHandlePosition: handleCenterInViewport + }) + }, 40) } export default RotateController diff --git a/packages/ketcher-react/src/script/editor/tool/rotate.ts b/packages/ketcher-react/src/script/editor/tool/rotate.ts index c07f9d95d5..0db3bbd215 100644 --- a/packages/ketcher-react/src/script/editor/tool/rotate.ts +++ b/packages/ketcher-react/src/script/editor/tool/rotate.ts @@ -15,63 +15,53 @@ ***************************************************************************/ import { + Bond, + FlipDirection, FunctionalGroup, Vec2, - fromBondAlign, fromFlip, fromItemsFuse, fromRotate, getHoverToFuse, - getItemsToFuse + getItemsToFuse, + isAttachmentBond } from 'ketcher-core' import utils from '../shared/utils' import Editor from '../Editor' import { Tool } from './Tool' +import { intersection } from 'lodash' class RotateTool implements Tool { private readonly editor: Editor dragCtx: any - isNotActiveTool: boolean | undefined + isNotActiveTool = true - constructor(editor, dir, isNotActiveTool?: boolean) { + constructor(editor: Editor, flipDirection?: FlipDirection) { this.editor = editor - if (dir) { + if (flipDirection) { const restruct = editor.render.ctab const selection = editor.selection() - const singleBond = - selection && - selection.bonds && - Object.keys(selection).length === 1 && - selection.bonds.length === 1 - const action = !singleBond - ? fromFlip(restruct, selection, dir, null) - : fromBondAlign(restruct, selection.bonds[0], dir) - editor.update(action) - this.isNotActiveTool = true - return - } - this.isNotActiveTool = isNotActiveTool - if (!editor.selection()?.atoms) { - !isNotActiveTool && this.editor.selection(null) + const selectionCenter = this.getCenter(this.editor)[0] + const canvasCenter = restruct.getVBoxObj().centre() + const action = fromFlip( + restruct, + selection, + flipDirection, + selectionCenter || canvasCenter + ) + editor.update(action) + editor.rotateController.rerender() } } - mousedown(event, handleCenter?: Vec2, center?: Vec2) { - const xy0 = - center || - this.getCenter(this.editor)[0] || - this.editor.render.page2obj(event) + mousedownHandle(handleCenter: Vec2, center: Vec2) { this.dragCtx = { - xy0, - angle1: utils.calcAngle( - xy0, - handleCenter || this.editor.render.page2obj(event) - ) + xy0: center, + angle1: utils.calcAngle(center, handleCenter) } - return true } /** @@ -82,12 +72,19 @@ class RotateTool implements Tool { */ getCenter(editor: Editor) { const selection = editor.selection() + if (!selection) { + return [undefined, [] as number[]] as const + } + const struct = editor.render.ctab.molecule - const { texts, rxnArrows, rxnPluses } = selection || {} + const { texts, rxnArrows, rxnPluses } = selection const visibleAtoms = - selection?.atoms?.filter((atomId) => { - const atom = struct.atoms.get(atomId)! + selection.atoms?.filter((atomId) => { + const atom = struct.atoms.get(atomId) + if (!atom) { + return false + } return ( !FunctionalGroup.isAtomInContractedFunctionalGroup( atom, @@ -98,39 +95,58 @@ class RotateTool implements Tool { ) }) || [] - let xy0: Vec2 | undefined - - let attachAtomId: number | null = null - let isMoreThanOneAttachAtom = false - visibleAtoms.forEach((aid) => { - const atom = struct.atoms.get(aid) + let center: Vec2 | undefined - if (isMoreThanOneAttachAtom) return - - atom?.neighbors.find((nei) => { - const hb = struct.halfBonds.get(nei) + const attachmentBonds = struct.bonds.filter((_bondId, bond) => + isAttachmentBond(bond, selection) + ) - if (hb) { - if (editor.selection()?.atoms?.indexOf(hb.end as number) === -1) { - if (attachAtomId === null) { - attachAtomId = aid - } else if (attachAtomId !== aid) { - isMoreThanOneAttachAtom = true - return true - } - } - } - return false + if (attachmentBonds.size > 1) { + /** + * Handle multiple attachment bonds with one intersection: + * 0 (selected atom) + * / | \ (bonds) + */ + const bondPoints: [number, number][] = [] + attachmentBonds.forEach((bond) => { + bondPoints.push([bond.begin, bond.end]) }) - }) + const intersectionAtoms = intersection(...bondPoints) + if ( + intersectionAtoms.length === 1 && + visibleAtoms.includes(intersectionAtoms[0]) + ) { + center = struct.atoms.get(intersectionAtoms[0])?.pp + } + } else if (attachmentBonds.size === 1) { + /** + * Handle one attachment bond: + * 1. the bond is unselected + * 0 (selected atom) --> center + * | (unselected bond) + * o (unselected atom) + * 2. the bond is selected + * 0 (selected atom) + * | (selected bond) + * o (unselected atom) --> center + */ + const attachmentBondId = attachmentBonds.keys().next().value as number + const attachmentBond = attachmentBonds.get(attachmentBondId) as Bond + const rotatePoint = [attachmentBond.begin, attachmentBond.end].find( + (atomId) => + selection.bonds?.includes(attachmentBondId) + ? !visibleAtoms.includes(atomId) + : visibleAtoms.includes(atomId) + ) as number + center = struct.atoms.get(rotatePoint)?.pp + } - if (!isMoreThanOneAttachAtom && attachAtomId !== null) { - xy0 = struct.atoms.get(attachAtomId)?.pp as Vec2 - } else if ( - visibleAtoms.length || - texts?.length || - rxnArrows?.length || - rxnPluses?.length + if ( + !center && + (visibleAtoms.length || + texts?.length || + rxnArrows?.length || + rxnPluses?.length) ) { const selectionBoundingBox = editor.render.ctab.getVBoxObj({ atoms: visibleAtoms, @@ -138,10 +154,10 @@ class RotateTool implements Tool { rxnArrows, rxnPluses }) - xy0 = selectionBoundingBox?.centre() + center = selectionBoundingBox?.centre() } - return [xy0, visibleAtoms] as const + return [center, visibleAtoms] as const } mousemove(event) { diff --git a/packages/ketcher-react/src/script/ui/action/tools.js b/packages/ketcher-react/src/script/ui/action/tools.js index bcbf5cd89e..1111189b59 100644 --- a/packages/ketcher-react/src/script/ui/action/tools.js +++ b/packages/ketcher-react/src/script/ui/action/tools.js @@ -79,11 +79,7 @@ const toolActions = { action: { tool: 'charge', opts: -1 }, hidden: (options) => isHidden(options, 'charge-minus') }, - transforms: { - hidden: (options) => isHidden(options, 'transforms') - }, 'transform-rotate': { - shortcut: 'Alt+r', title: 'Rotate Tool', action: { tool: 'rotate' }, hidden: (options) => isHidden(options, 'transform-rotate') 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 3dc210f18b..eb87f00024 100644 --- a/packages/ketcher-react/src/script/ui/state/editor/index.js +++ b/packages/ketcher-react/src/script/ui/state/editor/index.js @@ -26,7 +26,7 @@ import { toStereoLabel } from '../../data/convert/structconv' -import { Elements } from 'ketcher-core' +import { Elements, Vec2 } from 'ketcher-core' import acts from '../../action' import { debounce } from 'lodash/fp' import { openDialog } from '../modal' @@ -35,6 +35,8 @@ import { serverCall } from '../server' import { isAtomsArray } from '../modal/atoms' import { generateCommonProperties } from './utils' import { saveSettings } from '../options' +import { memoizedDebounce } from '../../utils' +import { updateFloatingTools } from '../floatingTools' export default function initEditor(dispatch, getState) { const updateAction = debounce(100, () => dispatch({ type: 'UPDATE' })) @@ -217,6 +219,15 @@ export default function initEditor(dispatch, getState) { highlightFG(dispatch, { groupStruct: null, sGroup: null }) } }, - onApiSettings: (payload) => dispatch(saveSettings(payload)) + onApiSettings: (payload) => dispatch(saveSettings(payload)), + + onUpdateFloatingTools: memoizedDebounce( + /** + * @param {import('src/script/editor/Editor').FloatingToolsParams} payload + */ + (payload) => { + dispatch(updateFloatingTools(payload)) + } + ) } } diff --git a/packages/ketcher-react/src/script/ui/state/floatingTools/index.ts b/packages/ketcher-react/src/script/ui/state/floatingTools/index.ts new file mode 100644 index 0000000000..33c0bbe113 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/state/floatingTools/index.ts @@ -0,0 +1,40 @@ +import { Reducer } from 'react' + +export interface FloatingToolsState { + visible: boolean + rotateHandlePosition: { x: number; y: number } +} + +export type FloatingToolsPayload = Partial + +export interface FloatingToolsAction { + type: 'UPDATE_FLOATING_TOOLS' + payload: Partial +} + +const initialState: FloatingToolsState = { + visible: false, + rotateHandlePosition: { x: 0, y: 0 } +} + +export const updateFloatingTools = (payload: FloatingToolsPayload) => { + const action: FloatingToolsAction = { + type: 'UPDATE_FLOATING_TOOLS', + payload + } + return action +} + +const floatingToolsReducer: Reducer = ( + state = initialState, + { type, payload } +) => { + switch (type) { + case 'UPDATE_FLOATING_TOOLS': + return { ...state, ...payload } + default: + return state + } +} + +export default floatingToolsReducer diff --git a/packages/ketcher-react/src/script/ui/state/floatingTools/selectors/index.ts b/packages/ketcher-react/src/script/ui/state/floatingTools/selectors/index.ts new file mode 100644 index 0000000000..b0cab0a968 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/state/floatingTools/selectors/index.ts @@ -0,0 +1,11 @@ +import type { FloatingToolsState } from '..' + +export const getFloatingToolsVisible = (state) => { + const floatingTools: FloatingToolsState = state.floatingTools + return floatingTools.visible +} + +export const getFloatingToolsRotateHandlePosition = (state) => { + const floatingTools: FloatingToolsState = state.floatingTools + return floatingTools.rotateHandlePosition +} diff --git a/packages/ketcher-react/src/script/ui/state/index.js b/packages/ketcher-react/src/script/ui/state/index.js index 5c777a2e90..8fbe2e7383 100644 --- a/packages/ketcher-react/src/script/ui/state/index.js +++ b/packages/ketcher-react/src/script/ui/state/index.js @@ -28,6 +28,7 @@ import { pick } from 'lodash/fp' import requestReducer from './request' import thunk from 'redux-thunk' import toolbarReducer from './toolbar' +import floatingToolsReducer from './floatingTools' export { onAction, load } @@ -41,7 +42,8 @@ const shared = combineReducers({ templates: templatesReducer, functionalGroups: functionalGroupsReducer, saltsAndSolvents: saltsAndSolventsReducer, - requestsStatuses: requestReducer + requestsStatuses: requestReducer, + floatingTools: floatingToolsReducer }) function getRootReducer(setEditor) { diff --git a/packages/ketcher-react/src/script/ui/views/components/StructEditor/StructEditor.jsx b/packages/ketcher-react/src/script/ui/views/components/StructEditor/StructEditor.jsx index 4af934fd55..4b395fc572 100644 --- a/packages/ketcher-react/src/script/ui/views/components/StructEditor/StructEditor.jsx +++ b/packages/ketcher-react/src/script/ui/views/components/StructEditor/StructEditor.jsx @@ -23,6 +23,7 @@ import clsx from 'clsx' import { upperFirst } from 'lodash/fp' import handIcon from '../../../../../icons/files/hand.svg' import compressedHandIcon from '../../../../../icons/files/compressed-hand.svg' +import { FloatingToolContainer } from '../../toolbars' import Cursor from '../Cursor' import { ContextMenu, ContextMenuTrigger } from '../ContextMenu' @@ -76,6 +77,8 @@ class StructEditor extends Component { } this.editorRef = createRef() this.logRef = createRef() + this.updateFloatingToolsPositionOnScroll = + this.updateFloatingToolsPositionOnScroll.bind(this) } shouldComponentUpdate(nextProps, nextState) { @@ -174,6 +177,10 @@ class StructEditor extends Component { removeEditorHandlers(this.editor, this.props) } + updateFloatingToolsPositionOnScroll() { + this.editor.rotateController.updateFloatingToolsPosition() + } + render() { const { Tag = 'div', @@ -201,6 +208,7 @@ class StructEditor extends Component { onShowInfo, onApiSettings, showAttachmentPoints = true, + onUpdateFloatingTools, ...props } = this.props @@ -212,6 +220,7 @@ class StructEditor extends Component {
{/* svg here */}
@@ -239,6 +248,8 @@ class StructEditor extends Component { sGroup={this.props.sGroup} /> + + ) diff --git a/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.container.ts b/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.container.ts new file mode 100644 index 0000000000..8850a9be23 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.container.ts @@ -0,0 +1,32 @@ +import { connect } from 'react-redux' +import { Dispatch } from 'redux' +import { + getFloatingToolsRotateHandlePosition, + getFloatingToolsVisible +} from 'src/script/ui/state/floatingTools/selectors' +import { onAction } from '../../../state' +import { + FloatingTools, + type FloatingToolsCallProps, + type FloatingToolsProps +} from './FloatingTools' + +const mapStateToProps = (state: any): FloatingToolsProps => { + return { + visible: getFloatingToolsVisible(state), + rotateHandlePosition: getFloatingToolsRotateHandlePosition(state), + status: state.actionState || {}, + indigoVerification: state.requestsStatuses.indigoVerification + } +} + +const mapDispatchToProps = (dispatch: Dispatch): FloatingToolsCallProps => ({ + onAction: (action) => dispatch(onAction(action)) +}) + +const FloatingToolContainer = connect( + mapStateToProps, + mapDispatchToProps +)(FloatingTools) + +export { FloatingToolContainer } diff --git a/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.module.less b/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.module.less new file mode 100644 index 0000000000..ab1b4ff5bb --- /dev/null +++ b/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.module.less @@ -0,0 +1,24 @@ +@import '../toolbar'; + +.wrapper { + .toolbar(); + + position: absolute; + translate: -50% calc(-50% - 40px); + + .item { + box-shadow: 0 6px 10px @color-select-box-shadow; + border-radius: 4px; + width: 28px; + height: 28px; + background-color: @color-background-primary; + + > svg { + width: 20px; + height: 20px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } +} diff --git a/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.tsx b/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.tsx new file mode 100644 index 0000000000..411e3e43c2 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/FloatingTools.tsx @@ -0,0 +1,59 @@ +import action, { UiActionAction } from '../../../action' +import { + ActionButton, + ActionButtonProps +} from '../ToolbarGroupItem/ActionButton' +import { type ToolbarItemVariant } from '../toolbar.types' +import classes from './FloatingTools.module.less' + +export type FloatingToolsProps = { + visible: boolean + rotateHandlePosition: { x: number; y: number } + status: { + disabled?: boolean + hidden?: boolean + } + indigoVerification: boolean +} +export type FloatingToolsCallProps = { + onAction: (action: UiActionAction) => void +} +type Props = FloatingToolsProps & FloatingToolsCallProps + +const FLOATING_TOOLS: ToolbarItemVariant[] = [ + 'transform-flip-h', + 'transform-flip-v', + 'erase' +] + +export const FloatingTools: React.FC = ({ + visible, + rotateHandlePosition, + status, + indigoVerification, + onAction +}) => { + if (!visible) return null + + return ( +
+ {FLOATING_TOOLS.map((name) => ( + + ))} +
+ ) +} diff --git a/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/index.ts b/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/index.ts new file mode 100644 index 0000000000..2ff93e7ac5 --- /dev/null +++ b/packages/ketcher-react/src/script/ui/views/toolbars/FloatingTools/index.ts @@ -0,0 +1 @@ +export * from './FloatingTools.container' diff --git a/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/LeftToolbar.tsx b/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/LeftToolbar.tsx index 3437ef44f6..1ed2dcd0a4 100644 --- a/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/LeftToolbar.tsx +++ b/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/LeftToolbar.tsx @@ -30,15 +30,13 @@ import { mappingOptions, rGroupOptions, selectOptions, - shapeOptions, - transformOptions + shapeOptions } from './leftToolbarOptions' import { ArrowScroll } from '../ArrowScroll' import { Bond } from './Bond' import { RGroup } from './RGroup' import { Shape } from './Shape' -import { Transform } from './Transform' import classes from './LeftToolbar.module.less' import clsx from 'clsx' import { useInView } from 'react-intersection-observer' @@ -102,8 +100,6 @@ const LeftToolbar = (props: Props) => { switch (item.id) { case 'bond-common': return - case 'transform-rotate': - return case 'rgroup': return case 'shapes': @@ -156,16 +152,6 @@ const LeftToolbar = (props: Props) => { items={[{ id: 'charge-plus' }, { id: 'charge-minus' }]} /> - - { - height?: number -} -type TransformCallProps = ToolbarGroupItemCallProps - -type Props = TransformProps & TransformCallProps - -const Transform = (props: Props) => { - const { height, ...rest } = props - - if (height && height <= mediaSizes.transformCollapsableHeight) { - return ( - - ) - } - - return ( - <> - - - - - ) -} - -export type { TransformProps, TransformCallProps } -export { Transform } diff --git a/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/Transform/index.ts b/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/Transform/index.ts deleted file mode 100644 index 8ef9566e0d..0000000000 --- a/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/Transform/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/**************************************************************************** - * Copyright 2021 EPAM Systems - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ***************************************************************************/ - -export * from './Transform' diff --git a/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/leftToolbarOptions.ts b/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/leftToolbarOptions.ts index fef7f55d88..ca95930282 100644 --- a/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/leftToolbarOptions.ts +++ b/packages/ketcher-react/src/script/ui/views/toolbars/LeftToolbar/leftToolbarOptions.ts @@ -14,12 +14,6 @@ const shapeOptions: ToolbarItem[] = makeItems([ 'shape-line' ]) -const transformOptions: ToolbarItem[] = makeItems([ - 'transform-rotate', - 'transform-flip-h', - 'transform-flip-v' -]) - const selectOptions: ToolbarItem[] = makeItems([ 'select-rectangle', 'select-lasso', @@ -59,7 +53,6 @@ export { bondSpecial, bondStereo, shapeOptions, - transformOptions, selectOptions, arrowsOptions, mappingOptions diff --git a/packages/ketcher-react/src/script/ui/views/toolbars/index.ts b/packages/ketcher-react/src/script/ui/views/toolbars/index.ts index b32a8ca7dc..03a0567514 100644 --- a/packages/ketcher-react/src/script/ui/views/toolbars/index.ts +++ b/packages/ketcher-react/src/script/ui/views/toolbars/index.ts @@ -18,3 +18,4 @@ export * from './BottomToolbar' export * from './LeftToolbar' export * from './RightToolbar' export * from './TopToolbar' +export * from './FloatingTools' diff --git a/packages/ketcher-react/src/script/ui/views/toolbars/toolbar.types.ts b/packages/ketcher-react/src/script/ui/views/toolbars/toolbar.types.ts index 697f922c97..18889bed44 100644 --- a/packages/ketcher-react/src/script/ui/views/toolbars/toolbar.types.ts +++ b/packages/ketcher-react/src/script/ui/views/toolbars/toolbar.types.ts @@ -94,11 +94,6 @@ type LeftToolbarItemVariant = // charge group | 'charge-plus' | 'charge-minus' - // transform group - | 'transforms' - | 'transform-rotate' - | 'transform-flip-h' - | 'transform-flip-v' // sgroup group | 'sgroup' // reaction @@ -154,11 +149,14 @@ type RightToolbarItemVariant = | 'extended-table' | 'any-atom' +type FloatingToolItemVariant = 'transform-flip-h' | 'transform-flip-v' | 'erase' + type ToolbarItemVariant = | TopToolbarItemVariant | LeftToolbarItemVariant | BottomToolbarItemVariant | RightToolbarItemVariant + | FloatingToolItemVariant interface ToolbarItem { id: ToolbarItemVariant @@ -172,6 +170,7 @@ export type { LeftToolbarItemVariant, RightToolbarItemVariant, TopToolbarItemVariant, + FloatingToolItemVariant, ToolbarItemVariant } diff --git a/packages/ketcher-react/src/typings.d.ts b/packages/ketcher-react/src/typings.d.ts index f54963aca3..6f7e16b32f 100644 --- a/packages/ketcher-react/src/typings.d.ts +++ b/packages/ketcher-react/src/typings.d.ts @@ -32,3 +32,33 @@ interface HTMLElement { mozRequestFullScreen?: () => void webkitRequestFullscreen?: () => void } + +declare module 'subscription' { + export class Subscription { + handlers: Array + handlersForDispatch: () => typeof this.handlers + add: (f: any, priority?: number) => void + addOnce: (f: any, priority?: number) => void + remove: (f: any) => void + hasHandler: () => boolean + dispatch: (value?: TDispatchValue) => void + } + + export class PipelineSubscription< + TDispatchValue = any + > extends Subscription { + dispatch: (value: TDispatchValue) => any + } + + export class StoppableSubscription< + TDispatchValue = any + > extends Subscription { + dispatch: (value?: TDispatchValue) => any + } + + export class DOMSubscription< + TDispatchEvent = any + > extends Subscription { + dispatch: (event: TDispatchEvent) => boolean + } +}