From 27849f611038014652604250f9a5863e1d00f7c5 Mon Sep 17 00:00:00 2001 From: Daniil Sloboda Date: Fri, 23 Aug 2024 11:42:39 +0400 Subject: [PATCH] #5107 - Implemented move and resize multitail head and tails --- .../editor/actions/multitailArrow.ts | 31 ++- .../editor/operations/OperationType.ts | 2 + .../editor/operations/multitailArrow/index.ts | 2 + .../multitailArrowMoveHeadTail.ts | 51 +++++ .../multitailArrowResizeTailHead.ts | 29 +++ .../render/restruct/remultitailArrow.ts | 187 +++++++++++++----- .../src/domain/constants/multitailArrow.ts | 32 +++ .../src/domain/entities/multitailArrow.ts | 186 +++++++++++++---- .../src/script/editor/shared/closest.ts | 1 + .../script/editor/tool/arrow/arrow.types.ts | 32 +++ .../src/script/editor/tool/arrow/arrowTool.ts | 21 ++ .../script/editor/tool/arrow/commonArrow.ts | 156 +++++++-------- .../tool/arrow/multitailArrowMoveTool.ts | 84 ++++++++ .../tool/arrow/reactionArrowMoveTool.ts | 76 +++++++ .../src/script/editor/tool/image.ts | 22 +-- .../src/script/editor/tool/select.ts | 149 ++++++++------ .../src/script/editor/utils/getItemCursor.ts | 43 ++++ .../src/script/ui/action/tools.js | 2 +- .../ContextMenu/ContextMenuTrigger.utils.ts | 3 +- 19 files changed, 853 insertions(+), 256 deletions(-) create mode 100644 packages/ketcher-core/src/application/editor/operations/multitailArrow/multitailArrowMoveHeadTail.ts create mode 100644 packages/ketcher-core/src/application/editor/operations/multitailArrow/multitailArrowResizeTailHead.ts create mode 100644 packages/ketcher-react/src/script/editor/tool/arrow/arrowTool.ts create mode 100644 packages/ketcher-react/src/script/editor/tool/arrow/multitailArrowMoveTool.ts create mode 100644 packages/ketcher-react/src/script/editor/tool/arrow/reactionArrowMoveTool.ts create mode 100644 packages/ketcher-react/src/script/editor/utils/getItemCursor.ts diff --git a/packages/ketcher-core/src/application/editor/actions/multitailArrow.ts b/packages/ketcher-core/src/application/editor/actions/multitailArrow.ts index 06f7608385..0c2207375b 100644 --- a/packages/ketcher-core/src/application/editor/actions/multitailArrow.ts +++ b/packages/ketcher-core/src/application/editor/actions/multitailArrow.ts @@ -1,4 +1,4 @@ -import { ReStruct } from 'application/render'; +import { MultitailArrowReferencePosition, ReStruct } from 'application/render'; import { Action, MultitailArrowDelete, @@ -6,6 +6,8 @@ import { MultitailArrowMove, MultitailArrowAddTail, MultitailArrowRemoveTail, + MultitailArrowResizeTailHead, + MultitailArrowMoveHeadTail, } from 'application/editor'; import { Vec2, MultitailArrow } from 'domain/entities'; @@ -54,3 +56,30 @@ export function fromMultitailArrowTailRemove( action.addOp(new MultitailArrowRemoveTail(id, tailId)); return action.perform(reStruct); } + +export function fromMultitailArrowHeadTailsResize( + reStruct: ReStruct, + id: number, + ref: MultitailArrowReferencePosition, + offset: number, +) { + const action = new Action(); + action.addOp( + new MultitailArrowResizeTailHead(id, offset, ref.name === 'head'), + ); + return action.perform(reStruct); +} + +export function fromMultitailArrowHeadTailMove( + reStruct: ReStruct, + id: number, + ref: MultitailArrowReferencePosition, + offset: number, + normalize?: true, +) { + const action = new Action(); + action.addOp( + new MultitailArrowMoveHeadTail(id, offset, ref.name, ref.tailId, normalize), + ); + return action.perform(reStruct); +} diff --git a/packages/ketcher-core/src/application/editor/operations/OperationType.ts b/packages/ketcher-core/src/application/editor/operations/OperationType.ts index 178edb790b..5a76bb3ec9 100644 --- a/packages/ketcher-core/src/application/editor/operations/OperationType.ts +++ b/packages/ketcher-core/src/application/editor/operations/OperationType.ts @@ -91,6 +91,8 @@ export const OperationType = Object.freeze({ MULTITAIL_ARROW_MOVE: 'Move multitail arrow', MULTITAIL_ARROW_ADD_TAIL: 'Add multitail arrow tail', MULTITAIL_ARROW_REMOVE_TAIL: 'Remove multitail arrow tail', + MULTITAIL_ARROW_RESIZE_HEAD_TAIL: 'Resize head tail', + MULTITAIL_ARROW_MOVE_HEAD_TAIL: 'Move head tail', }); export enum OperationPriority { diff --git a/packages/ketcher-core/src/application/editor/operations/multitailArrow/index.ts b/packages/ketcher-core/src/application/editor/operations/multitailArrow/index.ts index a170970d8d..3ade5a864d 100644 --- a/packages/ketcher-core/src/application/editor/operations/multitailArrow/index.ts +++ b/packages/ketcher-core/src/application/editor/operations/multitailArrow/index.ts @@ -1,3 +1,5 @@ export * from './multitailArrowAddRemoveTail'; export * from './multitailArrowMove'; +export * from './multitailArrowMoveHeadTail'; +export * from './multitailArrowResizeTailHead'; export * from './multitailArrowUpsertDelete'; diff --git a/packages/ketcher-core/src/application/editor/operations/multitailArrow/multitailArrowMoveHeadTail.ts b/packages/ketcher-core/src/application/editor/operations/multitailArrow/multitailArrowMoveHeadTail.ts new file mode 100644 index 0000000000..e8b8370f06 --- /dev/null +++ b/packages/ketcher-core/src/application/editor/operations/multitailArrow/multitailArrowMoveHeadTail.ts @@ -0,0 +1,51 @@ +import { BaseOperation } from 'application/editor/operations/base'; +import { OperationType } from 'application/editor'; +import { MultitailArrowRefName, ReStruct } from 'application/render'; +import { MULTITAIL_ARROW_KEY } from 'domain/constants'; + +export class MultitailArrowMoveHeadTail extends BaseOperation { + constructor( + private id: number, + private offset: number, + private name: string, + private tailId: number | null, + private normalize?: true, + ) { + super(OperationType.MULTITAIL_ARROW_MOVE_HEAD_TAIL); + } + + execute(reStruct: ReStruct) { + const reMultitailArrow = reStruct.multitailArrows.get(this.id); + const multitailArrow = reStruct.molecule.multitailArrows.get(this.id); + if (!multitailArrow || !reMultitailArrow) { + return; + } + switch (this.name) { + case MultitailArrowRefName.HEAD: + this.offset = multitailArrow.moveHead(this.offset); + break; + case MultitailArrowRefName.TOP_TAIL: + this.offset = multitailArrow.moveTail(this.offset, this.name); + break; + case MultitailArrowRefName.BOTTOM_TAIL: + this.offset = multitailArrow.moveTail(this.offset, this.name); + break; + default: + this.offset = multitailArrow.moveTail( + this.offset, + this.tailId as number, + this.normalize, + ); + } + BaseOperation.invalidateItem(reStruct, MULTITAIL_ARROW_KEY, this.id, 1); + } + + invert(): BaseOperation { + return new MultitailArrowMoveHeadTail( + this.id, + -this.offset, + this.name, + this.tailId, + ); + } +} diff --git a/packages/ketcher-core/src/application/editor/operations/multitailArrow/multitailArrowResizeTailHead.ts b/packages/ketcher-core/src/application/editor/operations/multitailArrow/multitailArrowResizeTailHead.ts new file mode 100644 index 0000000000..91a2540bde --- /dev/null +++ b/packages/ketcher-core/src/application/editor/operations/multitailArrow/multitailArrowResizeTailHead.ts @@ -0,0 +1,29 @@ +import { BaseOperation } from 'application/editor/operations/base'; +import { OperationType } from 'application/editor'; +import { ReStruct } from 'application/render'; +import { MULTITAIL_ARROW_KEY } from 'domain/constants'; + +export class MultitailArrowResizeTailHead extends BaseOperation { + constructor( + private id: number, + private offset: number, + private isHead: boolean, + ) { + super(OperationType.MULTITAIL_ARROW_RESIZE_HEAD_TAIL); + } + + execute(reStruct: ReStruct) { + const multitailArrow = reStruct.molecule.multitailArrows.get(this.id); + if (!multitailArrow) { + return; + } + this.offset = this.isHead + ? multitailArrow.resizeHead(this.offset) + : multitailArrow.resizeTails(this.offset); + BaseOperation.invalidateItem(reStruct, MULTITAIL_ARROW_KEY, this.id, 1); + } + + invert(): BaseOperation { + return new MultitailArrowResizeTailHead(this.id, -this.offset, this.isHead); + } +} diff --git a/packages/ketcher-core/src/application/render/restruct/remultitailArrow.ts b/packages/ketcher-core/src/application/render/restruct/remultitailArrow.ts index cb81b1220f..5c4b61a8a1 100644 --- a/packages/ketcher-core/src/application/render/restruct/remultitailArrow.ts +++ b/packages/ketcher-core/src/application/render/restruct/remultitailArrow.ts @@ -8,9 +8,24 @@ import { Scale } from 'domain/helpers'; import { Box2Abs, Pool, Vec2 } from 'domain/entities'; import util from 'application/render/util'; -interface ClosestReferencePosition { +export enum MultitailArrowRefName { + HEAD = 'head', + TAILS = 'tails', + TOP_TAIL = 'topTail', + BOTTOM_TAIL = 'bottomTail', + SPINE = 'spine', +} + +export interface MultitailArrowReferencePosition { + name: MultitailArrowRefName; + offset: Vec2; + isLine: boolean; + tailId: number | null; +} + +export interface MultitailArrowClosestReferencePosition { distance: number; - ref: { name: string; offset: Vec2 } | null; + ref: MultitailArrowReferencePosition | null; } export class ReMultitailArrow extends ReObject { @@ -20,11 +35,9 @@ export class ReMultitailArrow extends ReObject { return true; } - static getTailIdFromRef( - ref?: ClosestReferencePosition['ref'], - ): number | null { - if (ref && ref.name.startsWith(ReMultitailArrow.TAILS_NAME)) { - return parseInt(ref.name.replace(`${ReMultitailArrow.TAILS_NAME}-`, '')); + static getTailIdFromRefName(name: string): number | null { + if (name.startsWith(MultitailArrowRefName.TAILS)) { + return parseInt(name.replace(`${MultitailArrowRefName.TAILS}-`, '')); } return null; } @@ -33,7 +46,9 @@ export class ReMultitailArrow extends ReObject { super(MULTITAIL_ARROW_KEY); } - getReferencePositions(renderOptions: RenderOptions) { + getReferencePositions( + renderOptions: RenderOptions, + ): ReturnType { const positions = this.multitailArrow.getReferencePositions(); const tails = new Pool(); positions.tails.forEach((item, key) => { @@ -41,23 +56,11 @@ export class ReMultitailArrow extends ReObject { }); return { - headPosition: Scale.modelToCanvas(positions.headPosition, renderOptions), - topTailPosition: Scale.modelToCanvas( - positions.topTailPosition, - renderOptions, - ), - bottomTailPosition: Scale.modelToCanvas( - positions.bottomTailPosition, - renderOptions, - ), - topSpinePosition: Scale.modelToCanvas( - positions.topSpinePosition, - renderOptions, - ), - bottomSpinePosition: Scale.modelToCanvas( - positions.bottomSpinePosition, - renderOptions, - ), + head: Scale.modelToCanvas(positions.head, renderOptions), + topTail: Scale.modelToCanvas(positions.topTail, renderOptions), + bottomTail: Scale.modelToCanvas(positions.bottomTail, renderOptions), + topSpine: Scale.modelToCanvas(positions.topSpine, renderOptions), + bottomSpine: Scale.modelToCanvas(positions.bottomSpine, renderOptions), tails, }; } @@ -78,26 +81,21 @@ export class ReMultitailArrow extends ReObject { reStruct.clearVisel(this.visel); const pathBuilder = new PathBuilder(); const headPathBuilder = new PathBuilder(); - const { - topTailPosition, - topSpinePosition, - bottomSpinePosition, - headPosition, - tails, - } = this.getReferencePositions(renderOptions); - const topTailOffsetX = topSpinePosition.sub(topTailPosition).x; - const arrowStart = new Vec2(topSpinePosition.x, headPosition.y); - const arrowLength = headPosition.x - arrowStart.x; + const { topTail, topSpine, bottomSpine, head, tails } = + this.getReferencePositions(renderOptions); + const topTailOffsetX = topSpine.sub(topTail).x; + const arrowStart = new Vec2(topSpine.x, head.y); + const arrowLength = head.x - arrowStart.x; pathBuilder.addMultitailArrowBase( - topSpinePosition.y, - bottomSpinePosition.y, - topSpinePosition.x, + topSpine.y, + bottomSpine.y, + topSpine.x, topTailOffsetX, ); headPathBuilder.addFilledTriangleArrowPathParts(arrowStart, arrowLength); tails.forEach((tail) => { - pathBuilder.addLine(tail, { x: topSpinePosition.x, y: tail.y }); + pathBuilder.addLine(tail, { x: topSpine.x, y: tail.y }); }); const path = reStruct.render.paper.path(pathBuilder.build()); @@ -111,32 +109,113 @@ export class ReMultitailArrow extends ReObject { this.visel.add(header, Box2Abs.fromRelBox(util.relBox(header.getBBox()))); } + private calculateDistanceToNamedEntity( + point: Vec2, + entities: Array<[string, Vec2]>, + isLine: false, + ): MultitailArrowClosestReferencePosition; + + private calculateDistanceToNamedEntity( + point: Vec2, + entities: Array<[string, Line]>, + isLine: true, + ): MultitailArrowClosestReferencePosition; + + private calculateDistanceToNamedEntity( + point: Vec2, + entities: Array<[string, Vec2 | Line]>, + isLine: boolean, + ): MultitailArrowClosestReferencePosition { + return entities.reduce( + (acc, [name, value]) => { + const distance = isLine + ? point.calculateDistanceToLine(value as Line) + : Vec2.dist(point, value as Vec2); + const tailId = ReMultitailArrow.getTailIdFromRefName(name); + let refName: MultitailArrowRefName; + if (typeof tailId === 'number') { + refName = MultitailArrowRefName.TAILS; + } else if ( + [ + MultitailArrowRefName.HEAD, + MultitailArrowRefName.BOTTOM_TAIL, + MultitailArrowRefName.TOP_TAIL, + ].includes(name as MultitailArrowRefName) + ) { + refName = name as MultitailArrowRefName; + } else { + refName = MultitailArrowRefName.SPINE; + } + + return distance < acc.distance + ? { + distance, + ref: { + name: refName, + offset: new Vec2(0, 0), + isLine, + tailId, + }, + } + : acc; + }, + { + distance: Infinity, + ref: null, + } as MultitailArrowClosestReferencePosition, + ); + } + + private tailArrayFromPool(tails: Pool): Array<[string, T]> { + return Array.from(tails.entries()).map(([key, value]) => [ + `${ReMultitailArrow.TAILS_NAME}-${key}`, + value, + ]); + } + calculateDistanceToPoint( point: Vec2, renderOptions: RenderOptions, - ): ClosestReferencePosition { + maxDistanceToPoint: number, + ): MultitailArrowClosestReferencePosition { const referencePositions = this.getReferencePositions(renderOptions); const referenceLines = this.getReferenceLines( renderOptions, referencePositions, ); const { tails, ...rest } = referenceLines; - const tailsLines: Array<[string, Line]> = Array.from(tails.entries()).map( - ([key, value]) => [`${ReMultitailArrow.TAILS_NAME}-${key}`, value], + const lines: Array<[string, Line]> = Object.entries(rest).concat( + this.tailArrayFromPool(tails), ); - const lines: Array<[string, Line]> = - Object.entries(rest).concat(tailsLines); - const res = lines.reduce( - (acc, [name, value]): ClosestReferencePosition => { - const distance = point.calculateDistanceToLine(value); - return distance < acc.distance - ? { distance, ref: { name, offset: new Vec2(0, 0) } } - : acc; - }, - { distance: Infinity, ref: null } as ClosestReferencePosition, - ); + const lineRes = this.calculateDistanceToNamedEntity(point, lines, true); + + if (lineRes.distance < maxDistanceToPoint) { + const { + topSpine: _t, + bottomSpine: _b, + tails: tailsPoints, + ...validReferencePositions + } = referencePositions; + + const points: Array<[string, Vec2]> = Object.entries( + validReferencePositions, + ).concat(this.tailArrayFromPool(tailsPoints)); + + const pointsRes = this.calculateDistanceToNamedEntity( + point, + points, + false, + ); + if ( + pointsRes.distance < maxDistanceToPoint / 2 || + (pointsRes.distance < maxDistanceToPoint && + pointsRes.distance <= lineRes.distance) + ) { + return pointsRes; + } + } - return res; + return lineRes; } } diff --git a/packages/ketcher-core/src/domain/constants/multitailArrow.ts b/packages/ketcher-core/src/domain/constants/multitailArrow.ts index 70477a56c8..9c4c9f066b 100644 --- a/packages/ketcher-core/src/domain/constants/multitailArrow.ts +++ b/packages/ketcher-core/src/domain/constants/multitailArrow.ts @@ -1,5 +1,37 @@ +import { + MultitailArrowReferenceLinesNames, + MultitailArrowReferencePositionsNames, +} from 'domain/entities'; + export const MULTITAIL_ARROW_KEY = 'multitailArrows'; export const MULTITAIL_ARROW_TOOL_NAME = 'reaction-arrow-multitail'; export const MULTITAIL_ARROW_SERIALIZE_KEY = 'multi-tailed-arrow'; + +const MOVE = 'move'; +const CURSOR_RESIZE_VERTICAL = 'ns-resize'; +const CURSOR_RESIZE_HORIZONTAL = 'ew-resize'; + +export const multitailReferencePositionToCursor: Record< + MultitailArrowReferencePositionsNames, + string +> = { + topTail: CURSOR_RESIZE_HORIZONTAL, + tails: CURSOR_RESIZE_HORIZONTAL, + bottomTail: CURSOR_RESIZE_HORIZONTAL, + topSpine: MOVE, + bottomSpine: MOVE, + head: CURSOR_RESIZE_HORIZONTAL, +}; + +export const multitailArrowReferenceLinesToCursor: Record< + MultitailArrowReferenceLinesNames, + string +> = { + topTail: CURSOR_RESIZE_VERTICAL, + bottomTail: CURSOR_RESIZE_VERTICAL, + tails: CURSOR_RESIZE_VERTICAL, + head: CURSOR_RESIZE_VERTICAL, + spine: MOVE, +}; diff --git a/packages/ketcher-core/src/domain/entities/multitailArrow.ts b/packages/ketcher-core/src/domain/entities/multitailArrow.ts index c3520c0604..b9d9f0c310 100644 --- a/packages/ketcher-core/src/domain/entities/multitailArrow.ts +++ b/packages/ketcher-core/src/domain/entities/multitailArrow.ts @@ -7,22 +7,28 @@ import { MULTITAIL_ARROW_SERIALIZE_KEY } from 'domain/constants'; export type Line = [Vec2, Vec2]; export interface MultitailArrowsReferencePositions { - headPosition: Vec2; - topTailPosition: Vec2; - bottomTailPosition: Vec2; - topSpinePosition: Vec2; - bottomSpinePosition: Vec2; + head: Vec2; + topTail: Vec2; + bottomTail: Vec2; + topSpine: Vec2; + bottomSpine: Vec2; tails: Pool; } +export type MultitailArrowReferencePositionsNames = + keyof MultitailArrowsReferencePositions; + export interface MultitailArrowsReferenceLines { + head: Line; topTail: Line; bottomTail: Line; spine: Line; - head: Line; tails: Pool; } +export type MultitailArrowReferenceLinesNames = + keyof MultitailArrowsReferenceLines; + export interface KetFileMultitailArrowNode { head: { position: Vec2; @@ -42,10 +48,13 @@ interface TailDistance { } export class MultitailArrow extends BaseMicromoleculeEntity { - static MIN_TAIL_DISTANCE = 0.7; + static MIN_TAIL_DISTANCE = 0.35; + static MIN_HEAD_LENGTH = 0.5; + static MIN_TAIL_LENGTH = 0.4; + static MIN_TOP_BOTTOM_OFFSET = 0.15; static canAddTail(distance: TailDistance['distance']): boolean { - return distance >= MultitailArrow.MIN_TAIL_DISTANCE; + return distance >= 2 * MultitailArrow.MIN_TAIL_DISTANCE; } static fromTwoPoints(topLeft: Vec2, bottomRight: Vec2) { @@ -106,14 +115,14 @@ export class MultitailArrow extends BaseMicromoleculeEntity { }); return { - headPosition: new Vec2( + head: new Vec2( this.spineTop.x + this.headOffset.x, this.spineTop.y + this.headOffset.y, ), - topTailPosition: new Vec2(tailX, this.spineTop.y), - bottomTailPosition: new Vec2(tailX, bottomY), - topSpinePosition: new Vec2(this.spineTop), - bottomSpinePosition: new Vec2(this.spineTop.x, bottomY), + topTail: new Vec2(tailX, this.spineTop.y), + bottomTail: new Vec2(tailX, bottomY), + topSpine: new Vec2(this.spineTop), + bottomSpine: new Vec2(this.spineTop.x, bottomY), tails, }; } @@ -126,47 +135,45 @@ export class MultitailArrow extends BaseMicromoleculeEntity { getReferenceLines( referencePositions: MultitailArrowsReferencePositions, ): MultitailArrowsReferenceLines { - const spineX = referencePositions.topSpinePosition.x; - const headSpinePosition = new Vec2( - spineX, - referencePositions.headPosition.y, - ); + const spineX = referencePositions.topSpine.x; + const headSpinePosition = new Vec2(spineX, referencePositions.head.y); const tails = new Pool(); referencePositions.tails.forEach((tail, key) => { tails.set(key, [tail, new Vec2(spineX, tail.y)]); }); return { - topTail: [ - referencePositions.topTailPosition, - referencePositions.topSpinePosition, - ], + topTail: [referencePositions.topTail, referencePositions.topSpine], bottomTail: [ - referencePositions.bottomTailPosition, - referencePositions.bottomSpinePosition, + referencePositions.bottomTail, + referencePositions.bottomSpine, ], - spine: [ - referencePositions.topSpinePosition, - referencePositions.bottomSpinePosition, - ], - head: [headSpinePosition, referencePositions.headPosition], + spine: [referencePositions.topSpine, referencePositions.bottomSpine], + head: [headSpinePosition, referencePositions.head], tails, }; } - getTailsMaxDistance(): TailDistance { - const allTailsOffsets = Array.from(this.tailsYOffset.values()) + getTailsDistance(tailsYOffsets: Array): Array { + const allTailsOffsets = tailsYOffsets .concat([0, this.height]) .sort((a, b) => a - b); return allTailsOffsets.reduce( - (acc: TailDistance, item, index, array): TailDistance => { + (acc: Array, item, index, array) => { if (index === 0) { return acc; } const distance = item - array[index - 1]; - return distance > acc.distance - ? { distance, center: item - distance / 2 } - : acc; + return acc.concat({ distance, center: item - distance / 2 }); + }, + [], + ); + } + + getTailsMaxDistance(): TailDistance { + return this.getTailsDistance(Array.from(this.tailsYOffset.values())).reduce( + (acc: TailDistance, item) => { + return item.distance > acc.distance ? item : acc; }, { distance: 0, center: 0 }, ); @@ -219,6 +226,115 @@ export class MultitailArrow extends BaseMicromoleculeEntity { }); } + resizeHead(offset: number): number { + const headOffsetX = Math.max( + this.headOffset.x + offset, + MultitailArrow.MIN_HEAD_LENGTH, + ); + const realOffset = headOffsetX - this.headOffset.x; + this.headOffset = new Vec2(headOffsetX, this.headOffset.y); + return realOffset; + } + + moveHead(offset: number): number { + const headOffsetY = Math.min( + Math.max( + MultitailArrow.MIN_TOP_BOTTOM_OFFSET, + this.headOffset.y + offset, + ), + this.height - MultitailArrow.MIN_TOP_BOTTOM_OFFSET, + ); + const realOffset = headOffsetY - this.headOffset.y; + this.headOffset = new Vec2(this.headOffset.x, headOffsetY); + return realOffset; + } + + resizeTails(offset: number): number { + const updatedLength = Math.max( + this.tailLength - offset, + MultitailArrow.MIN_TAIL_LENGTH, + ); + const realOffset = this.tailLength - updatedLength; + this.tailLength = updatedLength; + return realOffset; + } + + moveTail(offset: number, id: number, normalize?: true): number; + moveTail(offset: number, name: 'topTail' | 'bottomTail'): number; + + moveTail(offset: number, second: number | string, normalize?: true): number { + const minHeight = + MultitailArrow.MIN_TAIL_DISTANCE * (this.tailsYOffset.size + 1); + const tailsOffset = Array.from(this.tailsYOffset.values()).sort( + (a, b) => a - b, + ); + const lastTail = tailsOffset.at(-1) || 0; + const firstTail = tailsOffset.at(0) || Infinity; + const closestTopElement = Math.min(firstTail, this.headOffset.y); + const closestBottomElement = Math.max(lastTail, this.headOffset.y); + + if (typeof second === 'number') { + const originalValue = this.tailsYOffset.get(second) as number; + let updatedHeight = Math.max( + MultitailArrow.MIN_TOP_BOTTOM_OFFSET, + Math.min( + originalValue + offset, + this.height - MultitailArrow.MIN_TOP_BOTTOM_OFFSET, + ), + ); + if (normalize) { + const tailsWithoutCurrent = Array.from(this.tailsYOffset.entries()) + .filter(([key]) => key !== second) + .map(([_, value]) => value); + const tailMinDistance = this.getTailsDistance(tailsWithoutCurrent) + .filter((item) => MultitailArrow.canAddTail(item.distance)) + .sort( + (a, b) => + Math.abs(a.center - updatedHeight) - + Math.abs(b.center - updatedHeight), + ) + .at(0) as TailDistance; + if ( + Math.abs(tailMinDistance.center - updatedHeight) > + tailMinDistance.distance / 2 - MultitailArrow.MIN_TAIL_DISTANCE + ) { + updatedHeight = tailMinDistance.center; + } + } + + const realOffset = updatedHeight - originalValue; + this.tailsYOffset.set(second, updatedHeight); + return realOffset; + } else if (second === 'bottomTail') { + const updatedHeight = Math.max( + minHeight, + this.height + offset, + closestBottomElement + MultitailArrow.MIN_TAIL_DISTANCE, + ); + const realOffset = updatedHeight - this.height; + this.height = updatedHeight; + return realOffset; + } else { + const realOffset = Math.min( + offset, + closestTopElement - MultitailArrow.MIN_TAIL_DISTANCE, + this.height - minHeight, + ); + if (realOffset !== 0) { + const vectorOffset = new Vec2(0, realOffset); + this.spineTop = this.spineTop.add(vectorOffset); + this.headOffset = this.headOffset.sub(vectorOffset); + this.height -= realOffset; + const updatedTails = this.tailsYOffset.clone(); + updatedTails.forEach((item, key) => { + updatedTails.set(key, item - realOffset); + }); + this.tailsYOffset = updatedTails; + } + return realOffset; + } + } + move(offset: Vec2): void { this.spineTop = this.spineTop.add(offset); } diff --git a/packages/ketcher-react/src/script/editor/shared/closest.ts b/packages/ketcher-react/src/script/editor/shared/closest.ts index 167c7f2217..e31e520870 100644 --- a/packages/ketcher-react/src/script/editor/shared/closest.ts +++ b/packages/ketcher-react/src/script/editor/shared/closest.ts @@ -757,6 +757,7 @@ function findClosestMultitailArrow(reStruct: ReStruct, cursorPosition: Vec2) { const { distance, ref } = item.calculateDistanceToPoint( canvasScaledPosition, renderOptions, + maxDistance, ); if (distance <= maxDistance && (!acc || acc.dist > distance)) { return { id, dist: distance, ref }; diff --git a/packages/ketcher-react/src/script/editor/tool/arrow/arrow.types.ts b/packages/ketcher-react/src/script/editor/tool/arrow/arrow.types.ts index 064c0dcb74..472feedfa1 100644 --- a/packages/ketcher-react/src/script/editor/tool/arrow/arrow.types.ts +++ b/packages/ketcher-react/src/script/editor/tool/arrow/arrow.types.ts @@ -1,5 +1,37 @@ import { Tool } from '../Tool'; +import { Vec2 } from 'domain/entities'; +import { Action } from 'application/editor'; +import { ClosestItemWithMap } from '../../shared/closest.types'; +import { MultitailArrowReferencePosition } from 'application/render'; +import { MULTITAIL_ARROW_KEY } from 'domain/constants'; export type ArrowAddTool = Required< Pick >; + +export type ReactionArrowClosestItem = ClosestItemWithMap; +export type MultitailArrowClosestItem = ClosestItemWithMap< + MultitailArrowReferencePosition, + typeof MULTITAIL_ARROW_KEY +>; + +export interface CommonArrowDragContext { + originalPosition: Vec2; + action: Action | null; + closestItem: CI; +} + +export interface ArrowMoveTool { + mousedown: ( + event: PointerEvent, + closestItem: CI, + ) => CommonArrowDragContext; + mousemove: ( + event: PointerEvent, + dragContext: CommonArrowDragContext, + ) => Action; + mouseup: ( + event: PointerEvent, + dragContext: CommonArrowDragContext, + ) => Action | null; +} diff --git a/packages/ketcher-react/src/script/editor/tool/arrow/arrowTool.ts b/packages/ketcher-react/src/script/editor/tool/arrow/arrowTool.ts new file mode 100644 index 0000000000..64864f8b45 --- /dev/null +++ b/packages/ketcher-react/src/script/editor/tool/arrow/arrowTool.ts @@ -0,0 +1,21 @@ +import Editor from '../../Editor'; +import { CoordinateTransformation, Vec2 } from 'ketcher-core'; + +export abstract class ArrowTool { + // eslint-disable-next-line no-useless-constructor + constructor(protected readonly editor: Editor) {} + + protected get render() { + return this.editor.render; + } + + protected get reStruct() { + return this.render.ctab; + } + + protected getOffset(event: PointerEvent, original: Vec2): Vec2 { + return CoordinateTransformation.pageToModel(event, this.render).sub( + original, + ); + } +} diff --git a/packages/ketcher-react/src/script/editor/tool/arrow/commonArrow.ts b/packages/ketcher-react/src/script/editor/tool/arrow/commonArrow.ts index a417f97dda..98998b3766 100644 --- a/packages/ketcher-react/src/script/editor/tool/arrow/commonArrow.ts +++ b/packages/ketcher-react/src/script/editor/tool/arrow/commonArrow.ts @@ -1,43 +1,52 @@ import { Tool } from '../Tool'; import Editor from '../../Editor'; import { - Vec2, - RxnArrowMode, - Action, - fromArrowResizing, - fromMultipleMove, MULTITAIL_ARROW_KEY, MULTITAIL_ARROW_TOOL_NAME, - CoordinateTransformation, - fromMultitailArrowMove, + RxnArrowMode, } from 'ketcher-core'; -import { ArrowAddTool } from './arrow.types'; +import { + ArrowAddTool, + ArrowMoveTool, + CommonArrowDragContext, + MultitailArrowClosestItem, + ReactionArrowClosestItem, +} from './arrow.types'; import { ReactionArrowAddTool } from './reactionArrowAdd'; import { MultitailArrowAddTool } from './multitailArrowAdd'; -import { ClosestItemWithMap } from '../../shared/closest.types'; -import assert from 'assert'; import { handleMovingPosibilityCursor } from '../../utils'; +import { MultitailArrowMoveTool } from './multitailArrowMoveTool'; +import { ArrowTool } from './arrowTool'; +import { ReactionArrowMoveTool } from './reactionArrowMoveTool'; +import { getItemCursor } from '../../utils/getItemCursor'; -type ReactionArrowClosestItem = ClosestItemWithMap; -type MultitailArrowClosestItem = ClosestItemWithMap< - unknown, - typeof MULTITAIL_ARROW_TOOL_NAME ->; +export class CommonArrowTool extends ArrowTool implements Tool { + static isDragContextMultitail( + dragContext: CommonArrowDragContext< + MultitailArrowClosestItem | ReactionArrowClosestItem + >, + ): dragContext is CommonArrowDragContext { + return dragContext.closestItem.map === MULTITAIL_ARROW_KEY; + } -interface CommonArrowDragContext { - originalPosition: Vec2; - action: Action | null; - closestItem: ReactionArrowClosestItem | MultitailArrowClosestItem; -} + private dragContext: + | CommonArrowDragContext< + MultitailArrowClosestItem | ReactionArrowClosestItem + > + | 'add' + | null = null; -export class CommonArrowTool implements Tool { - private dragContext: CommonArrowDragContext | 'add' | null = null; private addTool: ArrowAddTool; + private multitailMoveTool: ArrowMoveTool; + private reactionMoveTool: ArrowMoveTool; constructor( - private readonly editor: Editor, + editor: Editor, mode: RxnArrowMode | typeof MULTITAIL_ARROW_TOOL_NAME, ) { + super(editor); + this.multitailMoveTool = new MultitailArrowMoveTool(this.editor); + this.reactionMoveTool = new ReactionArrowMoveTool(this.editor); this.editor.selection(null); this.addTool = mode === MULTITAIL_ARROW_TOOL_NAME @@ -45,40 +54,18 @@ export class CommonArrowTool implements Tool { : new ReactionArrowAddTool(this.editor, mode); } - private get render() { - return this.editor.render; - } - - private get reStruct() { - return this.render.ctab; - } - - private updateResizingState( - closestItem: CommonArrowDragContext['closestItem'], - isResizing: boolean, - ) { - if (closestItem.map === 'rxnArrows') { - const reArrow = this.reStruct.rxnArrows.get(closestItem.id); - assert(reArrow != null); - reArrow.isResizing = isResizing; - } - } - - mousedown(event: MouseEvent) { + mousedown(event: PointerEvent) { const closestItem = this.editor.findItem(event, [ 'rxnArrows', MULTITAIL_ARROW_KEY, ]) as ReactionArrowClosestItem | MultitailArrowClosestItem; if (closestItem) { - this.dragContext = { - originalPosition: CoordinateTransformation.pageToModel( - event, - this.editor.render, - ), - action: null, - closestItem, - }; + this.dragContext = + closestItem.map === MULTITAIL_ARROW_KEY + ? this.multitailMoveTool.mousedown(event, closestItem) + : this.reactionMoveTool.mousedown(event, closestItem); + this.editor.hover(null); this.editor.selection({ [closestItem.map]: [closestItem.id] }); } else { @@ -90,68 +77,57 @@ export class CommonArrowTool implements Tool { mousemove(event: PointerEvent) { if (!this.dragContext) { - const items = this.editor.findItem(event, [ + const closestItem = this.editor.findItem(event, [ 'rxnArrows', MULTITAIL_ARROW_KEY, - ]); - this.editor.hover(items, null, event); + ]) as ReactionArrowClosestItem | MultitailArrowClosestItem; + this.editor.hover(closestItem, null, event); handleMovingPosibilityCursor( - items, - this.editor.render.paper.canvas, - this.editor.render.options.movingStyle.cursor as string, + closestItem, + this.render.paper.canvas, + getItemCursor(this.render, closestItem), ); return; } if (this.dragContext === 'add') { return this.addTool.mousemove(event); } - const current = CoordinateTransformation.pageToModel(event, this.render); - const offset = current.sub(this.dragContext.originalPosition); - const { action, closestItem } = this.dragContext; - if (action) { - action.perform(this.reStruct); + if (this.dragContext.action) { + this.dragContext.action.perform(this.reStruct); } - if (closestItem.map === 'rxnArrows') { - if (!closestItem.ref) { - this.dragContext.action = fromMultipleMove( - this.reStruct, - this.editor.selection() || {}, - offset, - ); - } else { - this.updateResizingState(closestItem, true); - const isSnappingEnabled = !event.ctrlKey; - this.dragContext.action = fromArrowResizing( - this.reStruct, - closestItem.id, - offset, - current, - closestItem.ref, - isSnappingEnabled, - ); - } + if (CommonArrowTool.isDragContextMultitail(this.dragContext)) { + this.dragContext.action = this.multitailMoveTool.mousemove( + event, + this.dragContext, + ); } else { - this.dragContext.action = fromMultitailArrowMove( - this.reStruct, - closestItem.id, - offset, + this.dragContext.action = this.reactionMoveTool.mousemove( + event, + this.dragContext as CommonArrowDragContext, ); } this.editor.update(this.dragContext.action, true); } - mouseup(event: MouseEvent) { + mouseup(event: PointerEvent) { try { if (!this.dragContext) return; if (this.dragContext === 'add') { return this.addTool.mouseup(event); } - - const { action, closestItem } = this.dragContext; - if (closestItem.map === 'rxnArrows' && action) { - this.updateResizingState(closestItem, false); + if (CommonArrowTool.isDragContextMultitail(this.dragContext)) { + this.dragContext.action = this.multitailMoveTool.mouseup( + event, + this.dragContext, + ); + } else { + this.dragContext.action = this.reactionMoveTool.mouseup( + event, + this.dragContext as CommonArrowDragContext, + ); } + const { action } = this.dragContext; if (action) { this.editor.update(true); this.editor.update(action); diff --git a/packages/ketcher-react/src/script/editor/tool/arrow/multitailArrowMoveTool.ts b/packages/ketcher-react/src/script/editor/tool/arrow/multitailArrowMoveTool.ts new file mode 100644 index 0000000000..ba3a250d78 --- /dev/null +++ b/packages/ketcher-react/src/script/editor/tool/arrow/multitailArrowMoveTool.ts @@ -0,0 +1,84 @@ +import { + ArrowMoveTool, + CommonArrowDragContext, + MultitailArrowClosestItem, +} from './arrow.types'; +import { + CoordinateTransformation, + MultitailArrowRefName, + fromMultitailArrowHeadTailMove, + fromMultitailArrowHeadTailsResize, + fromMultitailArrowMove, + Action, +} from 'ketcher-core'; +import { ArrowTool } from './arrowTool'; + +export class MultitailArrowMoveTool + extends ArrowTool + implements ArrowMoveTool +{ + mousedown(event: PointerEvent, closestItem: MultitailArrowClosestItem) { + return { + originalPosition: CoordinateTransformation.pageToModel( + event, + this.editor.render, + ), + action: null, + closestItem, + }; + } + + mousemove( + event: PointerEvent, + dragContext: CommonArrowDragContext, + ): Action { + const { closestItem, originalPosition } = dragContext; + const current = CoordinateTransformation.pageToModel(event, this.render); + const offset = current.sub(originalPosition); + const ref = closestItem.ref; + if (ref && ref.name !== MultitailArrowRefName.SPINE) { + if (ref.isLine) { + return fromMultitailArrowHeadTailMove( + this.reStruct, + closestItem.id, + ref, + offset.y, + ); + } else { + return fromMultitailArrowHeadTailsResize( + this.reStruct, + dragContext.closestItem.id, + ref, + offset.x, + ); + } + } else { + return fromMultitailArrowMove(this.reStruct, closestItem.id, offset); + } + } + + mouseup( + event: PointerEvent, + dragContext: CommonArrowDragContext, + ) { + const { action, closestItem } = dragContext; + if ( + action && + closestItem.ref && + closestItem.ref.isLine && + closestItem.ref.name === 'tails' + ) { + const current = CoordinateTransformation.pageToModel(event, this.render); + const offset = current.sub(dragContext.originalPosition); + action.perform(this.reStruct); + return fromMultitailArrowHeadTailMove( + this.reStruct, + closestItem.id, + closestItem.ref, + offset.y, + true, + ); + } + return action; + } +} diff --git a/packages/ketcher-react/src/script/editor/tool/arrow/reactionArrowMoveTool.ts b/packages/ketcher-react/src/script/editor/tool/arrow/reactionArrowMoveTool.ts new file mode 100644 index 0000000000..7137eb7ca0 --- /dev/null +++ b/packages/ketcher-react/src/script/editor/tool/arrow/reactionArrowMoveTool.ts @@ -0,0 +1,76 @@ +import { + ArrowMoveTool, + CommonArrowDragContext, + ReactionArrowClosestItem, +} from './arrow.types'; +import { + CoordinateTransformation, + fromArrowResizing, + fromMultipleMove, +} from 'ketcher-core'; +import assert from 'assert'; +import { ArrowTool } from './arrowTool'; + +export class ReactionArrowMoveTool + extends ArrowTool + implements ArrowMoveTool +{ + private updateResizingState( + closestItem: ReactionArrowClosestItem, + isResizing: boolean, + ) { + if (closestItem.map === 'rxnArrows') { + const reArrow = this.reStruct.rxnArrows.get(closestItem.id); + assert(reArrow != null); + reArrow.isResizing = isResizing; + } + } + + mousedown(event: PointerEvent, closestItem: ReactionArrowClosestItem) { + return { + originalPosition: CoordinateTransformation.pageToModel( + event, + this.editor.render, + ), + action: null, + closestItem, + }; + } + + mousemove( + event: PointerEvent, + dragContext: CommonArrowDragContext, + ) { + const { closestItem, originalPosition } = dragContext; + const current = CoordinateTransformation.pageToModel(event, this.render); + const offset = this.getOffset(event, originalPosition); + if (!closestItem.ref) { + return fromMultipleMove( + this.reStruct, + this.editor.selection() || {}, + offset, + ); + } else { + this.updateResizingState(closestItem, true); + const isSnappingEnabled = !event.ctrlKey; + return fromArrowResizing( + this.reStruct, + closestItem.id, + offset, + current, + closestItem.ref, + isSnappingEnabled, + ); + } + } + + mouseup( + _event: PointerEvent, + dragContext: CommonArrowDragContext, + ) { + if (dragContext.action) { + this.updateResizingState(dragContext.closestItem, false); + } + return dragContext.action; + } +} diff --git a/packages/ketcher-react/src/script/editor/tool/image.ts b/packages/ketcher-react/src/script/editor/tool/image.ts index fe2708e4af..8cbb20e523 100644 --- a/packages/ketcher-react/src/script/editor/tool/image.ts +++ b/packages/ketcher-react/src/script/editor/tool/image.ts @@ -9,12 +9,12 @@ import { fromImageMove, fromImageResize, ImageReferencePositionInfo, - imageReferencePositionToCursor, } from 'ketcher-core'; import { Tool } from './Tool'; import type Editor from '../Editor'; import { ClosestItemWithMap } from '../shared/closest.types'; import { handleMovingPosibilityCursor } from '../utils'; +import { getItemCursor } from '../utils/getItemCursor'; const TAG = 'tool/image.ts'; const supportedMimes = ['png', 'svg+xml']; @@ -98,21 +98,11 @@ export class ImageTool implements Tool { IMAGE_KEY, ]) as ClosestItemWithMap; const render = this.editor.render; - - if (item?.ref) { - handleMovingPosibilityCursor( - item, - render.paper.canvas, - imageReferencePositionToCursor[item.ref.name], - ); - } else { - handleMovingPosibilityCursor( - item, - render.paper.canvas, - render.options.movingStyle.cursor as string, - ); - } - + handleMovingPosibilityCursor( + item, + render.paper.canvas, + getItemCursor(render, item), + ); this.editor.hover(item, null, event); } } diff --git a/packages/ketcher-react/src/script/editor/tool/select.ts b/packages/ketcher-react/src/script/editor/tool/select.ts index 9ed6222405..36e6604ee2 100644 --- a/packages/ketcher-react/src/script/editor/tool/select.ts +++ b/packages/ketcher-react/src/script/editor/tool/select.ts @@ -23,7 +23,6 @@ import { getHoverToFuse, FunctionalGroup, fromSimpleObjectResizing, - fromArrowResizing, ReStruct, ReSGroup, Vec2, @@ -34,8 +33,6 @@ import { KetcherLogger, CoordinateTransformation, IMAGE_KEY, - imageReferencePositionToCursor, - ImageReferencePositionInfo, fromImageResize, MULTITAIL_ARROW_KEY, } from 'ketcher-core'; @@ -56,6 +53,14 @@ import { updateSelectedBonds } from 'src/script/ui/state/modal/bonds'; import { filterNotInContractedSGroup } from './helper/filterNotInCollapsedSGroup'; import { Tool } from './Tool'; import { handleMovingPosibilityCursor } from '../utils'; +import { getItemCursor } from '../utils/getItemCursor'; +import { + ArrowMoveTool, + MultitailArrowClosestItem, + ReactionArrowClosestItem, +} from './arrow/arrow.types'; +import { MultitailArrowMoveTool } from './arrow/multitailArrowMoveTool'; +import { ReactionArrowMoveTool } from './arrow/reactionArrowMoveTool'; type SelectMode = 'lasso' | 'fragment' | 'rectangle'; @@ -71,12 +76,16 @@ class SelectTool implements Tool { readonly #lassoHelper: LassoHelper; private readonly editor: Editor; private dragCtx: any; - private previousMouseMoveEvent?: MouseEvent; + private previousMouseMoveEvent?: PointerEvent; isMouseDown = false; readonly isMoving = false; + private multitailArrowMoveTool: ArrowMoveTool; + private reactionArrowMoveTool: ArrowMoveTool; constructor(editor: Editor, mode: SelectMode) { this.editor = editor; + this.multitailArrowMoveTool = new MultitailArrowMoveTool(editor); + this.reactionArrowMoveTool = new ReactionArrowMoveTool(editor); this.#mode = mode; this.#lassoHelper = new LassoHelper( this.#mode === 'lasso' ? 0 : 1, @@ -89,7 +98,7 @@ class SelectTool implements Tool { return this.#lassoHelper.running(); } - mousedown(event) { + mousedown(event: PointerEvent) { this.isMouseDown = true; const rnd = this.editor.render; const ctab = rnd.ctab; @@ -115,24 +124,42 @@ class SelectTool implements Tool { if (newSelected.atoms?.length || newSelected.bonds?.length) { this.editor.selection(newSelected); } - - this.dragCtx = { - item: ci, - xy0: rnd.page2obj(event), - }; - - if (!ci || ci.map === 'atoms') { - atomLongtapEvent(this, rnd); - } + const currentPosition = CoordinateTransformation.pageToModel( + event, + this.editor.render, + ); if (!ci) { // ci.type == 'Canvas' if (!event.shiftKey) this.editor.selection(null); - delete this.dragCtx.item; + this.dragCtx = { + xy0: currentPosition, + }; if (!this.#lassoHelper.fragment) this.#lassoHelper.begin(event); return true; } + if (ci.map === MULTITAIL_ARROW_KEY) { + this.dragCtx = this.multitailArrowMoveTool.mousedown( + event, + ci as MultitailArrowClosestItem, + ); + } else if (ci.map === 'rxnArrows') { + this.dragCtx = this.reactionArrowMoveTool.mousedown( + event, + ci as ReactionArrowClosestItem, + ); + } else { + this.dragCtx = { + item: ci, + xy0: currentPosition, + }; + } + + if (!ci || ci.map === 'atoms') { + atomLongtapEvent(this, rnd); + } + let sel = closestToSel(ci); const sgroups = ctab.sgroups.get(ci.id); const selection = this.editor.selection(); @@ -173,7 +200,7 @@ class SelectTool implements Tool { return true; } - mousemove(event) { + mousemove(event: PointerEvent) { this.previousMouseMoveEvent = event; const editor = this.editor; const rnd = editor.render; @@ -181,6 +208,28 @@ class SelectTool implements Tool { const dragCtx = this.dragCtx; if (dragCtx?.stopTapping) dragCtx.stopTapping(); + if (dragCtx?.closestItem) { + if (dragCtx.action) { + dragCtx.action.perform(rnd.ctab); + } + + if (dragCtx.closestItem.map === 'rxnArrows') { + this.dragCtx.action = this.reactionArrowMoveTool.mousemove( + event, + this.dragCtx, + ); + } else if (dragCtx.closestItem.map === MULTITAIL_ARROW_KEY) { + this.dragCtx.action = this.multitailArrowMoveTool.mousemove( + event, + this.dragCtx, + ); + } + if (dragCtx.action) { + editor.update(dragCtx.action, true); + return true; + } + } + if (dragCtx?.item) { const atoms = restruct.molecule.atoms; const selection = editor.selection(); @@ -225,18 +274,6 @@ class SelectTool implements Tool { } /* end + fullstop */ - /* handle rxnArrows */ - if (dragCtx.item.map === 'rxnArrows' && dragCtx.item.ref) { - if (dragCtx?.action) dragCtx.action.perform(rnd.ctab); - const props = getResizingProps(editor, dragCtx, event); - this.updateArrowResizingState(dragCtx.item.id, true); - const isSnappingEnabled = !event.ctrlKey; - dragCtx.action = fromArrowResizing(...props, isSnappingEnabled); - editor.update(dragCtx.action, true); - return true; - } - /* end + fullstop */ - /* handle functionalGroups */ if (dragCtx.item.map === 'functionalGroups' && !dragCtx.action) { editor.event.showInfo.dispatch(null); @@ -279,26 +316,16 @@ class SelectTool implements Tool { ); const item = editor.findItem(event, maps, null); editor.hover(item, null, event); - if (item?.map === IMAGE_KEY && item.ref) { - const referencePositionInfo = item.ref as ImageReferencePositionInfo; - handleMovingPosibilityCursor( - item, - this.editor.render.paper.canvas, - // Casting is safe because we've checked for item map - imageReferencePositionToCursor[referencePositionInfo.name], - ); - } else { - handleMovingPosibilityCursor( - item, - this.editor.render.paper.canvas, - this.editor.render.options.movingStyle.cursor as string, - ); - } + handleMovingPosibilityCursor( + item, + this.editor.render.paper.canvas, + getItemCursor(this.editor.render, item), + ); return true; } - mouseup(event) { + mouseup(event: PointerEvent) { if (!this.isMouseDown) { return; } @@ -337,12 +364,27 @@ class SelectTool implements Tool { delete this.dragCtx; } /* end */ - - if (this.dragCtx?.item) { - if (this.dragCtx.item.map === 'rxnArrows') { - this.updateArrowResizingState(this.dragCtx.item.id, false); + if (this.dragCtx?.closestItem) { + if (this.dragCtx.closestItem.map === 'rxnArrows') { + this.dragCtx.action = this.reactionArrowMoveTool.mouseup( + event, + this.dragCtx, + ); + } else if (this.dragCtx.closestItem.map === MULTITAIL_ARROW_KEY) { + this.dragCtx.action = this.reactionArrowMoveTool.mouseup( + event, + this.dragCtx, + ); + } + if (this.dragCtx.action) { this.editor.update(true); + this.editor.update(this.dragCtx.action); } + this.dragCtx = null; + return; + } + + if (this.dragCtx?.item) { if (!isMergingToMacroMolecule(this.editor, this.dragCtx)) { dropAndMerge(editor, this.dragCtx.mergeItems, this.dragCtx.action); } @@ -353,7 +395,7 @@ class SelectTool implements Tool { } else if (this.#lassoHelper.fragment) { if ( !event.shiftKey && - this.editor.render.clientArea.contains(event.target) + this.editor.render.clientArea.contains(event.target as Node) ) editor.selection(null); } @@ -541,14 +583,7 @@ class SelectTool implements Tool { return isDraggingOnSaltOrSolventAtom || isDraggingOnSaltOrSolventBond; } - private updateArrowResizingState(itemId: number, isResizing: boolean) { - const reArrow = this.editor.render.ctab.rxnArrows.get(itemId); - if (reArrow) { - reArrow.isResizing = isResizing; - } - } - - private isCloseToEdgeOfCanvas(event: MouseEvent) { + private isCloseToEdgeOfCanvas(event: PointerEvent) { const EDGE_OFFSET = 50; const mousePositionInCanvas = CoordinateTransformation.pageToCanvas( event, diff --git a/packages/ketcher-react/src/script/editor/utils/getItemCursor.ts b/packages/ketcher-react/src/script/editor/utils/getItemCursor.ts new file mode 100644 index 0000000000..305a8923de --- /dev/null +++ b/packages/ketcher-react/src/script/editor/utils/getItemCursor.ts @@ -0,0 +1,43 @@ +import { ClosestItemWithMap } from '../shared/closest.types'; +import { + IMAGE_KEY, + imageReferencePositionToCursor, + MULTITAIL_ARROW_KEY, + multitailArrowReferenceLinesToCursor, + multitailReferencePositionToCursor, + ImageReferencePositionInfo, + MultitailArrowReferencePosition, + Render, +} from 'ketcher-core'; + +export function getItemCursor( + render: Render, + item?: ClosestItemWithMap | null, +): string { + const defaultCursor = render.options.movingStyle.cursor as string; + if (!item) { + return defaultCursor; + } + switch (item.map) { + case MULTITAIL_ARROW_KEY: { + const closestItem = ( + item as ClosestItemWithMap + ).ref; + if (!closestItem) { + return defaultCursor; + } + return closestItem.isLine + ? multitailArrowReferenceLinesToCursor[closestItem.name] + : multitailReferencePositionToCursor[closestItem.name]; + } + case IMAGE_KEY: { + const closestItem = + item.ref as ClosestItemWithMap; + return closestItem.ref + ? imageReferencePositionToCursor[closestItem.ref.name] + : defaultCursor; + } + default: + return defaultCursor; + } +} diff --git a/packages/ketcher-react/src/script/ui/action/tools.js b/packages/ketcher-react/src/script/ui/action/tools.js index 837dd8d60e..86ad55e540 100644 --- a/packages/ketcher-react/src/script/ui/action/tools.js +++ b/packages/ketcher-react/src/script/ui/action/tools.js @@ -256,7 +256,7 @@ const toolActions = { isHidden(options, 'reaction-arrow-elliptical-arc-arrow-open-half-angle'), }, [MULTITAIL_ARROW_TOOL_NAME]: { - title: 'Multi-Tail Arrow', + title: 'Multi-Tailed Arrow', action: { tool: 'reactionarrow', opts: MULTITAIL_ARROW_TOOL_NAME, diff --git a/packages/ketcher-react/src/script/ui/views/components/ContextMenu/ContextMenuTrigger.utils.ts b/packages/ketcher-react/src/script/ui/views/components/ContextMenu/ContextMenuTrigger.utils.ts index 325ed42dab..bf2b6b1038 100644 --- a/packages/ketcher-react/src/script/ui/views/components/ContextMenu/ContextMenuTrigger.utils.ts +++ b/packages/ketcher-react/src/script/ui/views/components/ContextMenu/ContextMenuTrigger.utils.ts @@ -116,11 +116,10 @@ export function getMenuPropsForClosestItem( const closestItemTyped = closestItem as unknown as ReturnType< ReMultitailArrow['calculateDistanceToPoint'] >; - const tailId = ReMultitailArrow.getTailIdFromRef(closestItemTyped.ref); return { id: CONTEXT_MENU_ID.FOR_MULTITAIL_ARROW, itemId: closestItem.id, - tailId, + tailId: closestItemTyped?.ref?.tailId || null, }; }