Skip to content

Commit

Permalink
#5104 - save\load to ket and tails add\remove
Browse files Browse the repository at this point in the history
  • Loading branch information
daniil-sloboda authored and rrodionov91 committed Sep 2, 2024
1 parent 3edb26c commit 4c5e894
Show file tree
Hide file tree
Showing 44 changed files with 740 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
MultitailArrowDelete,
MultitailArrowUpsert,
MultitailArrowMove,
MultitailArrowAddTail,
MultitailArrowRemoveTail,
} from 'application/editor';
import { Vec2, MultitailArrow } from 'domain/entities';

Expand Down Expand Up @@ -34,3 +36,21 @@ export function fromMultitailArrowMove(
action.addOp(new MultitailArrowMove(id, offset));
return action.perform(reStruct);
}

export function fromMultitailArrowTailAdd(reStruct: ReStruct, id: number) {
const action = new Action();

action.addOp(new MultitailArrowAddTail(id));
return action.perform(reStruct);
}

export function fromMultitailArrowTailRemove(
reStruct: ReStruct,
id: number,
tailId: number,
) {
const action = new Action();

action.addOp(new MultitailArrowRemoveTail(id, tailId));
return action.perform(reStruct);
}
14 changes: 13 additions & 1 deletion packages/ketcher-core/src/application/editor/actions/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ import {
BondAttr,
AtomAttr,
ImageUpsert,
MultitailArrowUpsert,
} from '../operations';
import { fromRGroupAttrs, fromUpdateIfThen } from './rgroup';

import { Action } from './action';
import { SGroup, Struct, Vec2 } from 'domain/entities';
import { MultitailArrow, SGroup, Struct, Vec2 } from 'domain/entities';
import { fromSgroupAddition } from './sgroup';
import { fromRGroupAttachmentPointAddition } from './rgroupAttachmentPoint';
import { MonomerMicromolecule } from 'domain/entities/monomerMicromolecule';
Expand Down Expand Up @@ -197,6 +198,14 @@ export function fromPaste(
action.addOp(new ImageUpsert(clonedImage).perform(restruct));
});

pstruct.multitailArrows.forEach((multitailArrow: MultitailArrow) => {
const clonedMultitailArrow = multitailArrow.clone();
clonedMultitailArrow.move(offset);
action.addOp(
new MultitailArrowUpsert(clonedMultitailArrow).perform(restruct),
);
});

pstruct.rgroups.forEach((rg, rgid) => {
rg.frags.forEach((__frag, frid) => {
action.addOp(
Expand Down Expand Up @@ -248,6 +257,9 @@ function getStructCenter(struct: Struct): Vec2 {
if (struct.texts.size > 0) return struct.texts.get(0)!.position;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (struct.images.size > 0) return struct.images.get(0)!.center();
if (struct.multitailArrows.size > 0)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return struct.multitailArrows.get(0)!.center();

return new Vec2(0, 0);
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export const OperationType = Object.freeze({
MULTITAIL_ARROW_UPSERT: 'Upsert multitail arrow',
MULTITAIL_ARROW_DELETE: 'Delete multitail arrow',
MULTITAIL_ARROW_MOVE: 'Move multitail arrow',
MULTITAIL_ARROW_ADD_TAIL: 'Add multitail arrow tail',
MULTITAIL_ARROW_REMOVE_TAIL: 'Remove multitail arrow tail',
});

export enum OperationPriority {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './multitailArrowAddRemoveTail';
export * from './multitailArrowMove';
export * from './multitailArrowUpsertDelete';
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint-disable @typescript-eslint/no-use-before-define,@typescript-eslint/no-non-null-assertion */
import { BaseOperation } from 'application/editor/operations/base';
import { OperationType } from 'application/editor';
import { ReStruct } from 'application/render';

export class MultitailArrowAddTail extends BaseOperation {
constructor(private itemId: number, private tailId?: number) {
super(OperationType.MULTITAIL_ARROW_ADD_TAIL);
}

execute(reStruct: ReStruct) {
const reMultitailArrow = reStruct.multitailArrows.get(this.itemId);
const multitailArrow = reStruct.molecule.multitailArrows.get(this.itemId);

if (!reMultitailArrow || !multitailArrow) {
return;
}

this.tailId = multitailArrow.addTail(this.tailId);
BaseOperation.invalidateItem(reStruct, 'multitailArrows', this.itemId, 1);
}

invert() {
return new MultitailArrowRemoveTail(this.itemId, this.tailId!);
}
}

export class MultitailArrowRemoveTail extends BaseOperation {
constructor(private itemId: number, private tailId: number) {
super(OperationType.MULTITAIL_ARROW_REMOVE_TAIL);
}

execute(reStruct: ReStruct) {
const reMultitailArrow = reStruct.multitailArrows.get(this.itemId);
const multitailArrow = reStruct.molecule.multitailArrows.get(this.itemId);

if (!reMultitailArrow || !multitailArrow) {
return;
}

multitailArrow.removeTail(this.tailId);
BaseOperation.invalidateItem(reStruct, 'multitailArrows', this.itemId, 1);
}

invert(): BaseOperation {
return new MultitailArrowAddTail(this.itemId, this.tailId);
}
}
5 changes: 5 additions & 0 deletions packages/ketcher-core/src/application/render/pathBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export class PathBuilder {
return this;
}

addPathParts(pathParts: Array<string>): PathBuilder {
this.pathParts = this.pathParts.concat(pathParts);
return this;
}

addOpenArrowPathParts(
start: Vec2,
arrowLength: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,21 @@ interface ClosestReferencePosition {
}

export class ReMultitailArrow extends ReObject {
static TAILS_NAME = 'tails';

static isSelectable(): boolean {
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}-`, ''));
}
return null;
}

constructor(public multitailArrow: MultitailArrow) {
super(MULTITAIL_ARROW_KEY);
}
Expand Down Expand Up @@ -111,7 +122,7 @@ export class ReMultitailArrow extends ReObject {
);
const { tails, ...rest } = referenceLines;
const tailsLines: Array<[string, Line]> = Array.from(tails.entries()).map(
([key, value]) => [`tails-${key}`, value],
([key, value]) => [`${ReMultitailArrow.TAILS_NAME}-${key}`, value],
);
const lines: Array<[string, Line]> =
Object.entries(rest).concat(tailsLines);
Expand Down
2 changes: 2 additions & 0 deletions packages/ketcher-core/src/domain/constants/multitailArrow.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const MULTITAIL_ARROW_KEY = 'multitailArrows';

export const MULTITAIL_ARROW_TOOL_NAME = 'reaction-arrow-multitail';

export const MULTITAIL_ARROW_SERIALIZE_KEY = 'multi-tailed-arrow';
134 changes: 133 additions & 1 deletion packages/ketcher-core/src/domain/entities/multitailArrow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { BaseMicromoleculeEntity } from 'domain/entities/BaseMicromoleculeEntity';
import { Vec2 } from 'domain/entities/vec2';
import { Pool } from 'domain/entities/pool';
import { getNodeWithInvertedYCoord, KetFileNode } from 'domain/serializers';
import { MULTITAIL_ARROW_SERIALIZE_KEY } from 'domain/constants';

export type Line = [Vec2, Vec2];

Expand All @@ -21,7 +23,31 @@ export interface MultitailArrowsReferenceLines {
tails: Pool<Line>;
}

export interface KetFileMultitailArrowNode {
head: {
position: Vec2;
};
spine: {
pos: [Vec2, Vec2];
};
tails: {
pos: Array<Vec2>;
};
zOrder: 0;
}

interface TailDistance {
distance: number;
center: number;
}

export class MultitailArrow extends BaseMicromoleculeEntity {
static MIN_TAIL_DISTANCE = 0.7;

static canAddTail(distance: TailDistance['distance']): boolean {
return distance >= MultitailArrow.MIN_TAIL_DISTANCE;
}

static fromTwoPoints(topLeft: Vec2, bottomRight: Vec2) {
const center = Vec2.centre(topLeft, bottomRight);
const spineX = topLeft.x + (bottomRight.x - topLeft.x) * 0.33;
Expand All @@ -36,6 +62,31 @@ export class MultitailArrow extends BaseMicromoleculeEntity {
);
}

static fromKetNode(ketFileNode: KetFileNode<KetFileMultitailArrowNode>) {
const data = getNodeWithInvertedYCoord(
ketFileNode.data,
) as KetFileMultitailArrowNode;
const [spineStart, spineEnd] = data.spine.pos;
const spineTop = new Vec2(spineStart);
const headOffset = new Vec2(data.head.position).sub(spineStart);
const tails = data.tails.pos.sort((a, b) => a.y - b.y);
const tailsLength = spineTop.x - tails[0].x;
const tailsYOffset = new Pool<number>();
tails.slice(1, -1).forEach((tail) => {
tailsYOffset.add(tail.y - spineTop.y);
});

const height = spineEnd.y - spineTop.y;

return new MultitailArrow(
spineTop,
height,
headOffset,
tailsLength,
tailsYOffset,
);
}

constructor(
private spineTop: Vec2,
private height: number,
Expand Down Expand Up @@ -103,6 +154,41 @@ export class MultitailArrow extends BaseMicromoleculeEntity {
};
}

getTailsMaxDistance(): TailDistance {
const allTailsOffsets = Array.from(this.tailsYOffset.values())
.concat([0, this.height])
.sort((a, b) => a - b);
return allTailsOffsets.reduce(
(acc: TailDistance, item, index, array): TailDistance => {
if (index === 0) {
return acc;
}
const distance = item - array[index - 1];
return distance > acc.distance
? { distance, center: item - distance / 2 }
: acc;
},
{ distance: 0, center: 0 },
);
}

addTail(id?: number): number {
const { center, distance } = this.getTailsMaxDistance();
if (!MultitailArrow.canAddTail(distance)) {
throw new Error('Cannot add tail because no minimal distance found');
}
if (id) {
this.tailsYOffset.set(id, center);
return id;
} else {
return this.tailsYOffset.add(center);
}
}

removeTail(id: number) {
this.tailsYOffset.delete(id);
}

center(): Vec2 {
return Vec2.centre(
new Vec2(this.spineTop.x - this.tailLength, this.spineTop.y),
Expand All @@ -119,11 +205,57 @@ export class MultitailArrow extends BaseMicromoleculeEntity {
this.height,
new Vec2(this.headOffset),
this.tailLength,
new Pool<number>(this.tailsYOffset),
this.tailsYOffset.clone(),
);
}

rescaleSize(scale: number) {
this.spineTop = this.spineTop.scaled(scale);
this.headOffset = this.headOffset.scaled(scale);
this.height = this.height * scale;
this.tailLength = this.tailLength * scale;
this.tailsYOffset.forEach((item, index) => {
this.tailsYOffset.set(index, item * scale);
});
}

move(offset: Vec2): void {
this.spineTop = this.spineTop.add(offset);
}

toKetNode(): KetFileNode<KetFileMultitailArrowNode> {
const head = this.spineTop.add(this.headOffset);
const bottomY = this.spineTop.y + this.height;
const spine: [Vec2, Vec2] = [
this.spineTop,
new Vec2(this.spineTop.x, bottomY),
];
const tailX = this.spineTop.x - this.tailLength;
const nonBorderTails = Array.from(this.tailsYOffset.values()).map(
(yOffset) => this.spineTop.y + yOffset,
);
const convertTail = (y: number) => new Vec2(tailX, y);
const tails = [this.spineTop.y]
.concat(nonBorderTails)
.concat(bottomY)
.map(convertTail);

return {
type: MULTITAIL_ARROW_SERIALIZE_KEY,
center: this.center(),
selected: this.getInitiallySelected(),
data: getNodeWithInvertedYCoord({
head: {
position: head,
},
spine: {
pos: spine,
},
tails: {
pos: tails,
},
zOrder: 0,
}),
};
}
}
6 changes: 6 additions & 0 deletions packages/ketcher-core/src/domain/entities/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,10 @@ export class Pool<TValue = any> extends Map<number, TValue> {
}
});
}

clone(): Pool<TValue> {
const newPool = new Pool<TValue>(this);
newPool.nextId = this.nextId;
return newPool;
}
}
Loading

0 comments on commit 4c5e894

Please sign in to comment.