diff --git a/packages/ketcher-react/src/icons/files/shape-circle.svg b/packages/ketcher-react/src/icons/files/shape-circle.svg deleted file mode 100644 index 0019f4a994..0000000000 --- a/packages/ketcher-react/src/icons/files/shape-circle.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ketcher-react/src/icons/files/shape-ellipse.svg b/packages/ketcher-react/src/icons/files/shape-ellipse.svg new file mode 100644 index 0000000000..2c848facb6 --- /dev/null +++ b/packages/ketcher-react/src/icons/files/shape-ellipse.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ketcher-react/src/icons/index.tsx b/packages/ketcher-react/src/icons/index.tsx index a701faf02e..e913b9ed12 100644 --- a/packages/ketcher-react/src/icons/index.tsx +++ b/packages/ketcher-react/src/icons/index.tsx @@ -82,7 +82,7 @@ import TransformRotateIcon from './files/transform-rotate.svg' import UndoIcon from './files/undo.svg' import ZoomInIcon from './files/zoom-in.svg' import ZoomOutIcon from './files/zoom-out.svg' -import ShapeCircleIcon from './files/shape-circle.svg' +import ShapeEllipseIcon from './files/shape-ellipse.svg' import ShapeRectangleIcon from './files/shape-rectangle.svg' import ShapePolylineIcon from './files/shape-polyline.svg' import ShapeLineIcon from './files/shape-line.svg' @@ -157,7 +157,7 @@ const icons = { undo: UndoIcon, 'zoom-in': ZoomInIcon, 'zoom-out': ZoomOutIcon, - 'shape-circle': ShapeCircleIcon, + 'shape-ellipse': ShapeEllipseIcon, 'shape-rectangle': ShapeRectangleIcon, 'shape-polyline': ShapePolylineIcon, 'shape-line': ShapeLineIcon diff --git a/packages/ketcher-react/src/script/chem/struct/index.js b/packages/ketcher-react/src/script/chem/struct/index.js index 43abc2d1ac..4508fec467 100644 --- a/packages/ketcher-react/src/script/chem/struct/index.js +++ b/packages/ketcher-react/src/script/chem/struct/index.js @@ -28,6 +28,7 @@ import Fragment from './fragment' import SGroup from './sgroup' import RGroup from './rgroup' import SGroupForest from './sgforest' +import { SimpleObject, SimpleObjectMode } from './simpleObject' function Struct() { this.atoms = new Pool() @@ -1042,31 +1043,6 @@ RxnPlus.prototype.clone = function () { return new RxnPlus(this) } -function SimpleObject(params) { - params = params || {} - this.pos = [] - - if (params.pos) - for (let i = 0; i < params.pos.length; i++) - this.pos[i] = params.pos[i] ? new Vec2(params.pos[i]) : new Vec2() - - this.mode = params.mode -} - -SimpleObject.prototype.clone = function () { - return new SimpleObject(this) -} - -SimpleObject.prototype.center = function () { - switch (this.mode) { - case 'rectangle': { - return Vec2.centre(this.pos[0], this.pos[1]) - } - default: - return this.pos[0] - } -} - function RxnArrow(params) { params = params || {} this.pp = params.pp ? new Vec2(params.pp) : new Vec2() @@ -1095,5 +1071,6 @@ export { RGroup, RxnPlus, RxnArrow, - SimpleObject + SimpleObject, + SimpleObjectMode } diff --git a/packages/ketcher-react/src/script/chem/struct/simpleObject.ts b/packages/ketcher-react/src/script/chem/struct/simpleObject.ts new file mode 100644 index 0000000000..745e016a58 --- /dev/null +++ b/packages/ketcher-react/src/script/chem/struct/simpleObject.ts @@ -0,0 +1,53 @@ +/**************************************************************************** + * 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. + ***************************************************************************/ +import Vec2 from '../../util/vec2' + +class SimpleObject { + pos: Array + mode: SimpleObjectMode + + constructor(params: { mode: SimpleObjectMode; pos?: Array }) { + params = params || {} + this.pos = [] + + if (params.pos) + for (let i = 0; i < params.pos.length; i++) + this.pos[i] = params.pos[i] ? new Vec2(params.pos[i]) : new Vec2() + + this.mode = params.mode + } + + clone(): SimpleObject { + return new SimpleObject(this) + } + + center(): Vec2 { + switch (this.mode) { + case SimpleObjectMode.rectangle: { + return Vec2.centre(this.pos[0], this.pos[1]) + } + default: + return this.pos[0] + } + } +} + +enum SimpleObjectMode { + ellipse = 'ellipse', + rectangle = 'rectangle', + line = 'line' +} +export { SimpleObject, SimpleObjectMode } diff --git a/packages/ketcher-react/src/script/editor/actions/simpleobject.js b/packages/ketcher-react/src/script/editor/actions/simpleobject.ts similarity index 77% rename from packages/ketcher-react/src/script/editor/actions/simpleobject.js rename to packages/ketcher-react/src/script/editor/actions/simpleobject.ts index 82f6bc71d1..4c13a359e6 100644 --- a/packages/ketcher-react/src/script/editor/actions/simpleobject.js +++ b/packages/ketcher-react/src/script/editor/actions/simpleobject.ts @@ -22,14 +22,23 @@ export function fromSimpleObjectDeletion(restruct, id) { return action.perform(restruct) } -export function fromSimpleObjectAddition(restruct, pos, mode) { +export function fromSimpleObjectAddition(restruct, pos, mode, toCircle) { var action = new Action() - action.addOp(new op.SimpleObjectAdd(pos, mode)) + action.addOp(new op.SimpleObjectAdd(pos, mode, toCircle)) return action.perform(restruct) } -export function fromSimpleObjectResizing(restruct, id, d, current, anchor) { +export function fromSimpleObjectResizing( + restruct, + id, + d, + current, + anchor, + toCircle +) { var action = new Action() - action.addOp(new op.SimpleObjectResize(id, d, current, anchor)) + action.addOp( + new op.SimpleObjectResize(id, d, current, anchor, false, toCircle) + ) return action.perform(restruct) } diff --git a/packages/ketcher-react/src/script/editor/operations/base.js b/packages/ketcher-react/src/script/editor/operations/base.js index 1852de2fad..53fd9b5860 100644 --- a/packages/ketcher-react/src/script/editor/operations/base.js +++ b/packages/ketcher-react/src/script/editor/operations/base.js @@ -5,7 +5,7 @@ class Base { this.type = type } - execute() { + execute(restruct) { throw new Error('Operation.execute() is not implemented') } @@ -27,6 +27,49 @@ class Base { } } +export const OperationType = { + ATOM_ADD: 'Add atom', + ATOM_DELETE: 'Delete atom', + ATOM_ATTR: 'Set atom attribute', + ATOM_MOVE: 'Move atom', + BOND_ADD: 'Add bond', + BOND_DELETE: 'Delete bond', + BOND_ATTR: 'Set bond attribute', + BOND_MOVE: 'Move bond', + LOOP_MOVE: 'Move loop', + S_GROUP_ATOM_ADD: 'Add atom to s-group', + S_GROUP_ATOM_REMOVE: 'Remove atom from s-group', + S_GROUP_ATTR: 'Set s-group attribute', + S_GROUP_CREATE: 'Create s-group', + S_GROUP_DELETE: 'Delete s-group', + S_GROUP_ADD_TO_HIERACHY: 'Add s-group to hierarchy', + S_GROUP_REMOVE_FROM_HIERACHY: 'Delete s-group from hierarchy', + R_GROUP_ATTR: 'Set r-group attribute', + R_GROUP_FRAGMENT: 'R-group fragment', + UPDATE_IF_THEN: 'Update', + RESTORE_IF_THEN: 'Restore', + RXN_ARROW_ADD: 'Add rxn arrow', + RXN_ARROW_DELETE: 'Delete rxn arrow', + RXN_ARROW_MOVE: 'Move rxn arrow', + RXN_PLUS_ADD: 'Add rxn plus', + RXN_PLUS_DELETE: 'Delete rxn plus', + RXN_PLUS_MOVE: 'Move rxn plus', + S_GROUP_DATA_MOVE: 'Move s-group data', + CANVAS_LOAD: 'Load canvas', + ALIGN_DESCRIPTORS: 'Align descriptors', + SIMPLE_OBJECT_ADD: 'Add simple object', + SIMPLE_OBJECT_DELETE: 'Delete simple object', + SIMPLE_OBJECT_MOVE: 'Move simple object', + SIMPLE_OBJECT_RESIZE: 'Resize simple object', + RESTORE_DESCRIPTORS_POSITION: 'Restore descriptors position', + FRAGMENT_ADD: 'Add fragment', + FRAGMENT_DELETE: 'Delete fragment', + FRAGMENT_STEREO_FLAG: 'Add fragment stereo flag', + FRAGMENT_ADD_STEREO_ATOM: 'Add stereo atom to fragment', + FRAGMENT_DELETE_STEREO_ATOM: 'Delete stereo atom from fragment', + ENHANCED_FLAG_MOVE: 'Move enhanced flag' +} + export function invalidateAtom(restruct, aid, level) { const atom = restruct.atoms.get(aid) diff --git a/packages/ketcher-react/src/script/editor/operations/op.js b/packages/ketcher-react/src/script/editor/operations/op.js index 1f24da1a17..20b4f64701 100644 --- a/packages/ketcher-react/src/script/editor/operations/op.js +++ b/packages/ketcher-react/src/script/editor/operations/op.js @@ -23,8 +23,7 @@ import { RGroup, RxnArrow, RxnPlus, - SGroup, - SimpleObject + SGroup } from '../../chem/struct' import { ReAtom, @@ -32,15 +31,15 @@ import { ReRxnPlus, ReRxnArrow, ReRGroup, - ReSGroup, - ReSimpleObject + ReSGroup } from '../../render/restruct' import Base, { invalidateAtom, invalidateBond, invalidateItem, - invalidateLoop + invalidateLoop, + OperationType } from './base' import { FragmentAdd, @@ -51,50 +50,14 @@ import { EnhancedFlagMove } from './op-frag' -const tfx = util.tfx +import { + SimpleObjectAdd, + SimpleObjectDelete, + SimpleObjectMove, + SimpleObjectResize +} from './simpleObject' -export const OperationType = { - ATOM_ADD: 'Add atom', - ATOM_DELETE: 'Delete atom', - ATOM_ATTR: 'Set atom attribute', - ATOM_MOVE: 'Move atom', - BOND_ADD: 'Add bond', - BOND_DELETE: 'Delete bond', - BOND_ATTR: 'Set bond attribute', - BOND_MOVE: 'Move bond', - LOOP_MOVE: 'Move loop', - S_GROUP_ATOM_ADD: 'Add atom to s-group', - S_GROUP_ATOM_REMOVE: 'Remove atom from s-group', - S_GROUP_ATTR: 'Set s-group attribute', - S_GROUP_CREATE: 'Create s-group', - S_GROUP_DELETE: 'Delete s-group', - S_GROUP_ADD_TO_HIERACHY: 'Add s-group to hierarchy', - S_GROUP_REMOVE_FROM_HIERACHY: 'Delete s-group from hierarchy', - R_GROUP_ATTR: 'Set r-group attribute', - R_GROUP_FRAGMENT: 'R-group fragment', - UPDATE_IF_THEN: 'Update', - RESTORE_IF_THEN: 'Restore', - RXN_ARROW_ADD: 'Add rxn arrow', - RXN_ARROW_DELETE: 'Delete rxn arrow', - RXN_ARROW_MOVE: 'Move rxn arrow', - RXN_PLUS_ADD: 'Add rxn plus', - RXN_PLUS_DELETE: 'Delete rxn plus', - RXN_PLUS_MOVE: 'Move rxn plus', - S_GROUP_DATA_MOVE: 'Move s-group data', - CANVAS_LOAD: 'Load canvas', - ALIGN_DESCRIPTORS: 'Align descriptors', - SIMPLE_OBJECT_ADD: 'Add simple object', - SIMPLE_OBJECT_DELETE: 'Delete simple object', - SIMPLE_OBJECT_MOVE: 'Move simple object', - SIMPLE_OBJECT_RESIZE: 'Resize simple object', - RESTORE_DESCRIPTORS_POSITION: 'Restore descriptors position', - FRAGMENT_ADD: 'Add fragment', - FRAGMENT_DELETE: 'Delete fragment', - FRAGMENT_STEREO_FLAG: 'Add fragment stereo flag', - FRAGMENT_ADD_STEREO_ATOM: 'Add stereo atom to fragment', - FRAGMENT_DELETE_STEREO_ATOM: 'Delete stereo atom from fragment', - ENHANCED_FLAG_MOVE: 'Move enhanced flag' -} +const tfx = util.tfx function AtomAdd(atom, pos) { this.data = { atom, pos, aid: null } @@ -997,159 +960,6 @@ RestoreDescriptorsPosition.prototype.invert = function () { return new AlignDescriptors() } -function SimpleObjectAdd(pos, mode) { - this.data = { id: null, pos, mode } - this.performed = false -} - -SimpleObjectAdd.prototype = new Base(OperationType.SIMPLE_OBJECT_ADD) - -SimpleObjectAdd.prototype.execute = function (restruct) { - const struct = restruct.molecule - if (!this.performed) { - this.data.id = struct.simpleObjects.add( - new SimpleObject({ mode: this.data.mode }) - ) - this.performed = true - } else { - struct.simpleObjects.set( - this.data.id, - new SimpleObject({ mode: this.data.mode }) - ) - } - - restruct.simpleObjects.set( - this.data.id, - new ReSimpleObject(struct.simpleObjects.get(this.data.id)) - ) - - struct.simpleObjectSetPos( - this.data.id, - this.data.pos.map(p => new Vec2(p)) - ) - - invalidateItem(restruct, 'simpleObjects', this.data.id, 1) -} - -SimpleObjectAdd.prototype.invert = function () { - const ret = new SimpleObjectDelete() - ret.data = this.data - return ret -} - -function SimpleObjectDelete(id) { - this.data = { id, pos: null, item: null } - this.performed = false -} - -SimpleObjectDelete.prototype = new Base(OperationType.SIMPLE_OBJECT_DELETE) - -SimpleObjectDelete.prototype.execute = function (restruct) { - const struct = restruct.molecule - if (!this.performed) { - const item = struct.simpleObjects.get(this.data.id) - this.data.pos = item.pos - this.data.mode = item.mode - this.performed = true - } - - restruct.markItemRemoved() - restruct.clearVisel(restruct.simpleObjects.get(this.data.id).visel) - restruct.simpleObjects.delete(this.data.id) - - struct.simpleObjects.delete(this.data.id) -} - -SimpleObjectDelete.prototype.invert = function () { - const ret = new SimpleObjectAdd() - ret.data = this.data - return ret -} - -function SimpleObjectMove(id, d, noinvalidate) { - this.data = { id, d, noinvalidate } -} - -SimpleObjectMove.prototype = new Base(OperationType.SIMPLE_OBJECT_MOVE) - -SimpleObjectMove.prototype.execute = function (restruct) { - const struct = restruct.molecule - const id = this.data.id - const d = this.data.d - const item = struct.simpleObjects.get(id) - item.pos.forEach(p => p.add_(d)) - restruct.simpleObjects - .get(id) - .visel.translate(scale.obj2scaled(d, restruct.render.options)) - this.data.d = d.negated() - if (!this.data.noinvalidate) invalidateItem(restruct, 'simpleObjects', id, 1) -} - -SimpleObjectMove.prototype.invert = function () { - const ret = new SimpleObjectMove() - ret.data = this.data - return ret -} - -function SimpleObjectResize(id, d, current, anchor, noinvalidate) { - this.data = { id, d, current, anchor, noinvalidate } -} - -SimpleObjectResize.prototype = new Base(OperationType.SIMPLE_OBJECT_RESIZE) - -SimpleObjectResize.prototype.execute = function (restruct) { - const struct = restruct.molecule - const id = this.data.id - const d = this.data.d - const current = this.data.current - const item = struct.simpleObjects.get(id) - const anchor = this.data.anchor - - if (item.mode === 'circle') { - const previousPos1 = item.pos[1].get_xy0() - item.pos[1].x = current.x - item.pos[1].y = current.y - this.data.current = previousPos1 - } else if (item.mode === 'line' && anchor) { - const previousPos1 = anchor.get_xy0() - anchor.x = current.x - anchor.y = current.y - this.data.current = previousPos1 - } else if (item.mode === 'rectangle' && anchor) { - const previousPos0 = item.pos[0].get_xy0() - const previousPos1 = item.pos[1].get_xy0() - - if (tfx(anchor.x) === tfx(item.pos[1].x)) { - item.pos[1].x = anchor.x = current.x - this.data.current.x = previousPos1.x - } - if (tfx(anchor.y) === tfx(item.pos[1].y)) { - item.pos[1].y = anchor.y = current.y - this.data.current.y = previousPos1.y - } - if (tfx(anchor.x) === tfx(item.pos[0].x)) { - item.pos[0].x = anchor.x = current.x - this.data.current.x = previousPos0.x - } - if (tfx(anchor.y) === tfx(item.pos[0].y)) { - item.pos[0].y = anchor.y = current.y - this.data.current.y = previousPos0.y - } - } else item.pos[1].add_(d) - - restruct.simpleObjects - .get(id) - .visel.translate(scale.obj2scaled(d, restruct.render.options)) - this.data.d = d.negated() - if (!this.data.noinvalidate) invalidateItem(restruct, 'simpleObjects', id, 1) -} - -SimpleObjectResize.prototype.invert = function () { - const ret = new SimpleObjectResize() - ret.data = this.data - return ret -} - const operations = { AtomAdd, AtomDelete, @@ -1194,3 +1004,5 @@ const operations = { } export default operations + +export { OperationType } diff --git a/packages/ketcher-react/src/script/editor/operations/simpleObject.ts b/packages/ketcher-react/src/script/editor/operations/simpleObject.ts new file mode 100644 index 0000000000..525c8cff1a --- /dev/null +++ b/packages/ketcher-react/src/script/editor/operations/simpleObject.ts @@ -0,0 +1,334 @@ +/**************************************************************************** + * 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. + ***************************************************************************/ +import Vec2 from '../../util/vec2' +import Base, { invalidateItem, OperationType } from './base' +import { ReSimpleObject } from '../../render/restruct' +import { SimpleObject, SimpleObjectMode } from 'src/script/chem/struct' +import scale from '../../util/scale' +import util from '../../render/util' + +const tfx = util.tfx + +interface SimpleObjectAddData { + id?: string + pos: Array + mode: SimpleObjectMode + toCircle: boolean +} +export class SimpleObjectAdd extends Base { + data: SimpleObjectAddData + performed: boolean + + constructor( + pos: Array = [], + mode: SimpleObjectMode = SimpleObjectMode.line, + toCircle: boolean = false + ) { + super(OperationType.SIMPLE_OBJECT_ADD) + // here is "tempValue is used + this.data = { pos, mode, toCircle } + this.performed = false + } + + execute(restruct: any): void { + const struct = restruct.molecule + if (!this.performed) { + this.data.id = struct.simpleObjects.add( + new SimpleObject({ mode: this.data.mode }) + ) + this.performed = true + } else { + struct.simpleObjects.set( + this.data.id, + new SimpleObject({ mode: this.data.mode }) + ) + } + + restruct.simpleObjects.set( + this.data.id, + new ReSimpleObject(struct.simpleObjects.get(this.data.id)) + ) + + const positions = [...this.data.pos] + if (this.data.toCircle) { + positions[1] = makeCircleFromEllipse(positions[0], positions[1]) + } + struct.simpleObjectSetPos( + this.data.id, + positions.map(p => new Vec2(p)) + ) + + invalidateItem(restruct, 'simpleObjects', this.data.id, 1) + } + invert(): Base { + //@ts-ignore + return new SimpleObjectDelete(this.data.id) + } +} + +interface SimpleObjectDeleteData { + id: string + pos?: Array + mode?: SimpleObjectMode + toCircle?: boolean +} + +export class SimpleObjectDelete extends Base { + data: SimpleObjectDeleteData + performed: boolean + + constructor(id: string) { + super(OperationType.SIMPLE_OBJECT_DELETE) + this.data = { id, pos: [], mode: SimpleObjectMode.line, toCircle: false } + this.performed = false + } + + execute(restruct: any): void { + const struct = restruct.molecule + if (!this.performed) { + const item = struct.simpleObjects.get(this.data.id) as any + //save to data current values. In future they could be used in invert for restoring simple object + this.data.pos = item.pos + this.data.mode = item.mode + this.data.toCircle = item.toCircle + this.performed = true + } + + restruct.markItemRemoved() + restruct.clearVisel(restruct.simpleObjects.get(this.data.id).visel) + restruct.simpleObjects.delete(this.data.id) + + struct.simpleObjects.delete(this.data.id) + } + + invert(): Base { + return new SimpleObjectAdd( + this.data.pos, + this.data.mode, + this.data.toCircle + ) + } +} + +interface SimpleObjectMoveData { + id: string + d: any + noinvalidate: boolean +} + +export class SimpleObjectMove extends Base { + data: SimpleObjectMoveData + + constructor(id: string, d: any, noinvalidate: boolean) { + super(OperationType.SIMPLE_OBJECT_MOVE) + this.data = { id, d, noinvalidate } + } + execute(restruct: any): void { + const struct = restruct.molecule + const id = this.data.id + const d = this.data.d + const item = struct.simpleObjects.get(id) + item.pos.forEach(p => p.add_(d)) + restruct.simpleObjects + .get(id) + .visel.translate(scale.obj2scaled(d, restruct.render.options)) + this.data.d = d.negated() + if (!this.data.noinvalidate) + invalidateItem(restruct, 'simpleObjects', id, 1) + } + + invert(): Base { + const move = new SimpleObjectMove( + this.data.id, + this.data.d, + this.data.noinvalidate + ) + //todo Need further investigation on why this is needed? + move.data = this.data + return move + } +} + +interface SimpleObjectResizeData { + id: string + d: any + current: Vec2 + anchor: Vec2 + noinvalidate: boolean + toCircle: boolean +} + +function handleEllipseChangeIfAnchorIsOnAxis(anchor, item, current) { + const previousPos0 = item.pos[0].get_xy0() + const previousPos1 = item.pos[1].get_xy0() + if (tfx(anchor.x) === tfx(item.pos[1].x)) { + item.pos[1].x = anchor.x = current.x + current.x = previousPos1.x + } + if (tfx(anchor.y) === tfx(item.pos[1].y)) { + item.pos[1].y = anchor.y = current.y + current.y = previousPos1.y + } + if (tfx(anchor.x) === tfx(item.pos[0].x)) { + item.pos[0].x = anchor.x = current.x + current.x = previousPos0.x + } + if (tfx(anchor.y) === tfx(item.pos[0].y)) { + item.pos[0].y = anchor.y = current.y + current.y = previousPos0.y + } +} + +function handleEllipseChangeIfAnchorIsOnDiagonal(item, current) { + const rad = Vec2.diff(item.pos[1], item.pos[0]) + const rx = Math.abs(rad.x / 2) + const ry = Math.abs(rad.y / 2) + const topLeftX = item.pos[0].x <= item.pos[1].x ? item.pos[0] : item.pos[1] + const topLeftY = item.pos[0].y <= item.pos[1].y ? item.pos[0] : item.pos[1] + const bottomRightX = + item.pos[0].x <= item.pos[1].x ? item.pos[1] : item.pos[0] + const bottomRightY = + item.pos[0].y <= item.pos[1].y ? item.pos[1] : item.pos[0] + //check in which quarter the anchor is placed + const firstQuarter = + current.x > topLeftX.x + rx && current.y <= topLeftY.y + ry + const secondQuarter = + current.x <= topLeftX.x + rx && current.y <= topLeftY.y + ry + const thirdQuarter = + current.x <= topLeftX.x + rx && current.y > topLeftY.y + ry + const forthQuarter = + current.x > topLeftX.x + rx && current.y > topLeftY.y + ry + + if (current.x > topLeftX.x && (firstQuarter || forthQuarter)) { + bottomRightX.x = current.x + } + + if (current.y < bottomRightY.y && (firstQuarter || secondQuarter)) { + topLeftY.y = current.y + } + if (current.x < bottomRightX.x && (secondQuarter || thirdQuarter)) { + topLeftX.x = current.x + } + if (current.y > topLeftY.y && (thirdQuarter || forthQuarter)) { + bottomRightY.y = current.y + } +} + +function handleRectangleChangeWithAnchor(item, anchor, current) { + const previousPos0 = item.pos[0].get_xy0() + const previousPos1 = item.pos[1].get_xy0() + + if (tfx(anchor.x) === tfx(item.pos[1].x)) { + item.pos[1].x = anchor.x = current.x + current.x = previousPos1.x + } + if (tfx(anchor.y) === tfx(item.pos[1].y)) { + item.pos[1].y = anchor.y = current.y + current.y = previousPos1.y + } + if (tfx(anchor.x) === tfx(item.pos[0].x)) { + item.pos[0].x = anchor.x = current.x + current.x = previousPos0.x + } + if (tfx(anchor.y) === tfx(item.pos[0].y)) { + item.pos[0].y = anchor.y = current.y + current.y = previousPos0.y + } +} + +export class SimpleObjectResize extends Base { + data: SimpleObjectResizeData + + constructor( + id: string, + d: any, + current: Vec2, + anchor: any, + noinvalidate: boolean, + toCircle: boolean + ) { + super(OperationType.SIMPLE_OBJECT_RESIZE) + this.data = { id, d, current, anchor, noinvalidate, toCircle } + } + + execute(restruct: any): void { + const struct = restruct.molecule + const id = this.data.id + const d = this.data.d + const current = this.data.current + const item = struct.simpleObjects.get(id) + const anchor = this.data.anchor + if (item.mode === SimpleObjectMode.ellipse) { + if (anchor) { + if ( + tfx(anchor.y) !== tfx(item.pos[0].y) && + tfx(anchor.x) !== tfx(item.pos[0].x) && + tfx(anchor.y) !== tfx(item.pos[1].y) && + tfx(anchor.x) !== tfx(item.pos[1].x) + ) { + handleEllipseChangeIfAnchorIsOnDiagonal(item, current) + } else { + handleEllipseChangeIfAnchorIsOnAxis(anchor, item, current) + } + } else if (this.data.toCircle) { + const previousPos1 = item.pos[1].get_xy0() + const circlePoint = makeCircleFromEllipse(item.pos[0], current) + item.pos[1].x = circlePoint.x + item.pos[1].y = circlePoint.y + this.data.current = previousPos1 + } else { + const previousPos1 = item.pos[1].get_xy0() + item.pos[1].x = current.x + item.pos[1].y = current.y + this.data.current = previousPos1 + } + } else if (item.mode === SimpleObjectMode.line && anchor) { + const previousPos1 = anchor.get_xy0() + anchor.x = current.x + anchor.y = current.y + this.data.current = previousPos1 + } else if (item.mode === SimpleObjectMode.rectangle && anchor) { + handleRectangleChangeWithAnchor(item, anchor, current) + } else item.pos[1].add_(d) + + restruct.simpleObjects + .get(id) + .visel.translate(scale.obj2scaled(d, restruct.render.options)) + this.data.d = d.negated() + if (!this.data.noinvalidate) + invalidateItem(restruct, 'simpleObjects', id, 1) + } + invert(): Base { + return new SimpleObjectResize( + this.data.id, + this.data.d, + this.data.current, + this.data.anchor, + this.data.noinvalidate, + this.data.toCircle + ) + } +} + +export function makeCircleFromEllipse(position0: Vec2, position1: Vec2): Vec2 { + const diff = Vec2.diff(position1, position0) + const min = Math.abs(diff.x) < Math.abs(diff.y) ? diff.x : diff.y + return new Vec2( + position0.x + (diff.x > 0 ? 1 : -1) * Math.abs(min), + position0.y + (diff.y > 0 ? 1 : -1) * Math.abs(min), + 0 + ) +} diff --git a/packages/ketcher-react/src/script/editor/tool/simpleobject.js b/packages/ketcher-react/src/script/editor/tool/simpleobject.js index 65f01789cc..87bae4388b 100644 --- a/packages/ketcher-react/src/script/editor/tool/simpleobject.js +++ b/packages/ketcher-react/src/script/editor/tool/simpleobject.js @@ -64,7 +64,8 @@ SimpleObjectTool.prototype.mousemove = function (event) { this.dragCtx.ci.id, diff, current, - this.dragCtx.ci.ref + this.dragCtx.ci.ref, + event.shiftKey ) } this.editor.update(this.dragCtx.action, true) @@ -90,7 +91,8 @@ SimpleObjectTool.prototype.mousemove = function (event) { this.dragCtx.itemId, diff, current, - null + null, + event.shiftKey ) this.editor.update(this.dragCtx.action, true) } @@ -113,7 +115,8 @@ SimpleObjectTool.prototype.mouseup = function (event) { this.dragCtx.action = fromSimpleObjectAddition( rnd.ctab, [this.dragCtx.p0, this.dragCtx.previous], - this.mode + this.mode, + event.shiftKey ) } this.editor.update(this.dragCtx.action) diff --git a/packages/ketcher-react/src/script/format/chemGraph/fromGraph/simpleObjectToStruct.js b/packages/ketcher-react/src/script/format/chemGraph/fromGraph/simpleObjectToStruct.js deleted file mode 100644 index aeec67da21..0000000000 --- a/packages/ketcher-react/src/script/format/chemGraph/fromGraph/simpleObjectToStruct.js +++ /dev/null @@ -1,20 +0,0 @@ -/**************************************************************************** - * Copyright 2020 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. - ***************************************************************************/ -import { SimpleObject } from '../../../chem/struct' -export function simpleObjectToStruct(graphItem, struct) { - struct.simpleObjects.add(new SimpleObject(graphItem.data)) - return struct -} diff --git a/packages/ketcher-react/src/script/format/chemGraph/fromGraph/simpleObjectToStruct.ts b/packages/ketcher-react/src/script/format/chemGraph/fromGraph/simpleObjectToStruct.ts new file mode 100644 index 0000000000..2547670293 --- /dev/null +++ b/packages/ketcher-react/src/script/format/chemGraph/fromGraph/simpleObjectToStruct.ts @@ -0,0 +1,51 @@ +/**************************************************************************** + * 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. + ***************************************************************************/ +import { SimpleObject, SimpleObjectMode } from '../../../chem/struct' +import Vec2 from '../../../util/vec2' + +export function simpleObjectToStruct(graphItem, struct) { + const object = + graphItem.data.mode === 'circle' + ? circleToEllipse(graphItem) + : graphItem.data + struct.simpleObjects.add(new SimpleObject(object)) + return struct +} + +/** + * @deprecated TODO to remove after release 2.3 + * As circle has been migrated to ellipses here is function for converting old files data with circles to ellipse type + * @param graphItem + */ +function circleToEllipse(graphItem) { + const radius = Vec2.dist(graphItem.data.pos[1], graphItem.data.pos[0]) + const pos0 = graphItem.data.pos[0] + return { + mode: SimpleObjectMode.ellipse, + pos: [ + { + x: pos0.x - Math.abs(radius), + y: pos0.y - Math.abs(radius), + z: pos0.z - Math.abs(radius) + }, + { + x: pos0.x + Math.abs(radius), + y: pos0.y + Math.abs(radius), + z: pos0.z + Math.abs(radius) + } + ] + } +} diff --git a/packages/ketcher-react/src/script/render/draw.js b/packages/ketcher-react/src/script/render/draw.js index f5033c6ad6..3a31b62fad 100644 --- a/packages/ketcher-react/src/script/render/draw.js +++ b/packages/ketcher-react/src/script/render/draw.js @@ -29,9 +29,11 @@ function rectangle(paper, pos, options) { ) } -function circle(paper, pos, options) { - const rad = Vec2.dist(pos[0], pos[1]) - return paper.circle(pos[0].x, pos[0].y, rad) +function ellipse(paper, pos, options) { + const rad = Vec2.diff(pos[1], pos[0]) + const rx = rad.x / 2 + const ry = rad.y / 2 + return paper.ellipse(pos[0].x + rx, pos[0].y + ry, Math.abs(rx), Math.abs(ry)) } function polyline(paper, pos, options) { @@ -405,7 +407,7 @@ export default { selectionRectangle, selectionPolygon, selectionLine, - circle, + ellipse, rectangle, polyline, line diff --git a/packages/ketcher-react/src/script/render/restruct/resimpleobject.js b/packages/ketcher-react/src/script/render/restruct/resimpleobject.js index 92524d960a..38f7600f8c 100644 --- a/packages/ketcher-react/src/script/render/restruct/resimpleobject.js +++ b/packages/ketcher-react/src/script/render/restruct/resimpleobject.js @@ -20,6 +20,7 @@ import draw from '../draw' import util from '../util' import scale from '../../util/scale' import Vec2 from '../../util/vec2' +import { SimpleObjectMode } from '../../chem/struct' const tfx = util.tfx @@ -41,13 +42,26 @@ ReSimpleObject.prototype.calcDistance = function (p, s) { const pos = item.pos switch (mode) { - case 'circle': { - const dist1 = Vec2.dist(point, pos[0]) - const dist2 = Vec2.dist(pos[0], pos[1]) - dist = Math.max(dist1, dist2) - Math.min(dist1, dist2) + case SimpleObjectMode.ellipse: { + const rad = Vec2.diff(pos[1], pos[0]) + const rx = rad.x / 2 + const ry = rad.y / 2 + const center = Vec2.sum(pos[0], { x: rx, y: ry }) + const pointToCenter = Vec2.diff(point, center) + if (rx !== 0 && ry !== 0) { + dist = Math.abs( + 1 - + (pointToCenter.x * pointToCenter.x) / (rx * rx) - + (pointToCenter.y * pointToCenter.y) / (ry * ry) + ) + } else { + // in case rx or ry is equal to 0 we have a line as a trivial case of ellipse + // in such case distance need to be calculated as a distance between line and current point + dist = calculateDistanceToLine(pos, point) + } break } - case 'rectangle': { + case SimpleObjectMode.rectangle: { const topX = Math.min(pos[0].x, pos[1].x) const topY = Math.min(pos[0].y, pos[1].y) const bottomX = Math.max(pos[0].x, pos[1].x) @@ -88,21 +102,8 @@ ReSimpleObject.prototype.calcDistance = function (p, s) { dist = Math.min(...distances) break } - case 'line': { - if ( - (point.x < Math.min(pos[0].x, pos[1].x) || - point.x > Math.max(pos[0].x, pos[1].x)) && - (point.y < Math.min(pos[0].y, pos[1].y) || - point.y > Math.max(pos[0].y, pos[1].y)) - ) - dist = Math.min(Vec2.dist(pos[0], point), Vec2.dist(pos[1], point)) - else { - const a = Vec2.dist(pos[0], pos[1]) - const b = Vec2.dist(pos[0], point) - const c = Vec2.dist(pos[1], point) - const per = (a + b + c) / 2 - dist = (2 / a) * Math.sqrt(per * (per - a) * (per - b) * (per - c)) - } + case SimpleObjectMode.line: { + dist = calculateDistanceToLine(pos, point) break } @@ -117,6 +118,25 @@ ReSimpleObject.prototype.calcDistance = function (p, s) { return { minDist: dist, refPoint: refPoint } } +function calculateDistanceToLine(pos, point) { + let dist + if ( + (point.x < Math.min(pos[0].x, pos[1].x) || + point.x > Math.max(pos[0].x, pos[1].x)) && + (point.y < Math.min(pos[0].y, pos[1].y) || + point.y > Math.max(pos[0].y, pos[1].y)) + ) + dist = Math.min(Vec2.dist(pos[0], point), Vec2.dist(pos[1], point)) + else { + const a = Vec2.dist(pos[0], pos[1]) + const b = Vec2.dist(pos[0], point) + const c = Vec2.dist(pos[1], point) + const per = (a + b + c) / 2 + dist = (2 / a) * Math.sqrt(per * (per - a) * (per - b) * (per - c)) + } + return dist +} + ReSimpleObject.prototype.getReferencePointDistance = function (p) { let dist = [] const refPoints = this.getReferencePoints() @@ -136,19 +156,52 @@ ReSimpleObject.prototype.getReferencePointDistance = function (p) { ReSimpleObject.prototype.getReferencePoints = function () { const refPoints = [] switch (this.item.mode) { - case 'circle': { + case SimpleObjectMode.ellipse: { const p0 = this.item.pos[0] - const rad = Vec2.dist(this.item.pos[0], this.item.pos[1]) + const rad = Vec2.diff(this.item.pos[1], p0) + const rx = rad.x / 2 + const ry = rad.y / 2 + let point = { x: rx, y: 0 } + let angle = 0, + perimeter = 0, + curPoint + //calculate approximate perimeter value for first 1/4 + while (angle <= 90) { + curPoint = new Vec2( + rx * Math.cos((angle * Math.PI) / 180), + ry * Math.sin((angle * Math.PI) / 180) + ) + perimeter += Vec2.dist(point, curPoint) + point = curPoint + angle += 0.25 + } + angle = 0 + point = { x: rx, y: 0 } + let dist = 0 + //get point value for first 1/8 of ellipse perimeter with approximation scheme + while (dist <= perimeter / 2) { + angle += 0.25 + curPoint = new Vec2( + rx * Math.cos((angle * Math.PI) / 180), + ry * Math.sin((angle * Math.PI) / 180) + ) + dist += Vec2.dist(point, curPoint) + point = curPoint + } refPoints.push( - new Vec2(p0.x - rad, p0.y), - new Vec2(p0.x, p0.y - rad), - new Vec2(p0.x + rad, p0.y), - new Vec2(p0.x, p0.y + rad) + new Vec2(p0.x + rx, p0.y), + new Vec2(p0.x, p0.y + ry), + new Vec2(p0.x + 2 * rx, p0.y + ry), + new Vec2(p0.x + rx, p0.y + 2 * ry), + new Vec2(p0.x + rx + point.x, p0.y + ry + point.y), + new Vec2(p0.x + rx + point.x, p0.y + ry - point.y), + new Vec2(p0.x + rx - point.x, p0.y + ry + point.y), + new Vec2(p0.x + rx - point.x, p0.y + ry - point.y) ) break } - case 'rectangle': { + case SimpleObjectMode.rectangle: { const p0 = new Vec2( tfx(Math.min(this.item.pos[0].x, this.item.pos[1].x)), tfx(Math.min(this.item.pos[0].y, this.item.pos[1].y)) @@ -168,7 +221,7 @@ ReSimpleObject.prototype.getReferencePoints = function () { ) break } - case 'line': { + case SimpleObjectMode.line: { this.item.pos.forEach(i => refPoints.push(i)) break } @@ -192,16 +245,31 @@ ReSimpleObject.prototype.highlightPath = function (render) { //TODO: It seems that inheritance will be the better approach here switch (this.item.mode) { - case 'circle': { - const rad = Vec2.dist(point[0], point[1]) + case SimpleObjectMode.ellipse: { + const rad = Vec2.diff(point[1], point[0]) + const rx = rad.x / 2 + const ry = rad.y / 2 path.push( - render.paper.circle(point[0].x, point[0].y, rad + s / 8), - render.paper.circle(point[0].x, point[0].y, rad - s / 8) + render.paper.ellipse( + tfx(point[0].x + rx), + tfx(point[0].y + ry), + tfx(Math.abs(rx) + s / 8), + tfx(Math.abs(ry) + s / 8) + ) ) + if (Math.abs(rx) - s / 8 > 0 && Math.abs(ry) - s / 8 > 0) + path.push( + render.paper.ellipse( + tfx(point[0].x + rx), + tfx(point[0].y + ry), + tfx(Math.abs(rx) - s / 8), + tfx(Math.abs(ry) - s / 8) + ) + ) break } - case 'rectangle': { + case SimpleObjectMode.rectangle: { path.push( render.paper.rect( tfx(Math.min(point[0].x, point[1].x) - s / 8), @@ -238,7 +306,7 @@ ReSimpleObject.prototype.highlightPath = function (render) { break } - case 'line': { + case SimpleObjectMode.line: { //TODO: reuse this code for polyline const poly = [] @@ -351,15 +419,15 @@ ReSimpleObject.prototype.show = function (restruct, id, options) { function generatePath(mode, paper, pos, options) { let path = null switch (mode) { - case 'circle': { - path = draw.circle(paper, pos, options) + case SimpleObjectMode.ellipse: { + path = draw.ellipse(paper, pos, options) break } - case 'rectangle': { + case SimpleObjectMode.rectangle: { path = draw.rectangle(paper, pos, options) break } - case 'line': { + case SimpleObjectMode.line: { path = draw.line(paper, pos, options) break } diff --git a/packages/ketcher-react/src/script/ui/action/tools.js b/packages/ketcher-react/src/script/ui/action/tools.js index 6c3e1dd68c..98f5d0c652 100644 --- a/packages/ketcher-react/src/script/ui/action/tools.js +++ b/packages/ketcher-react/src/script/ui/action/tools.js @@ -16,6 +16,7 @@ import { bond as bondSchema } from '../data/schema/struct-schema' import { toBondType } from '../data/convert/structconv' +import { SimpleObjectMode } from '../../chem/struct' const toolActions = { 'select-lasso': { @@ -113,17 +114,17 @@ const toolActions = { title: 'Attachment Point Tool', action: { tool: 'apoint' } }, - 'shape-circle': { - title: 'Shape Circle', - action: { tool: 'simpleobject', opts: 'circle' } + 'shape-ellipse': { + title: 'Shape Ellipse', + action: { tool: 'simpleobject', opts: SimpleObjectMode.ellipse } }, 'shape-rectangle': { title: 'Shape Rectangle', - action: { tool: 'simpleobject', opts: 'rectangle' } + action: { tool: 'simpleobject', opts: SimpleObjectMode.rectangle } }, 'shape-line': { title: 'Shape Line', - action: { tool: 'simpleobject', opts: 'line' } + action: { tool: 'simpleobject', opts: SimpleObjectMode.line } } } diff --git a/packages/ketcher-react/src/script/ui/views/toolbar.jsx b/packages/ketcher-react/src/script/ui/views/toolbar.jsx index e6ce6854c4..ccb5c3b8c4 100644 --- a/packages/ketcher-react/src/script/ui/views/toolbar.jsx +++ b/packages/ketcher-react/src/script/ui/views/toolbar.jsx @@ -218,7 +218,7 @@ function initToolbar() { if (!global?.ketcher?.standalone) toolboxItems.push({ id: 'shape', - menu: ['shape-circle', 'shape-rectangle', 'shape-line'] + menu: ['shape-ellipse', 'shape-rectangle', 'shape-line'] }) return [