packages/ketcher-core/src/application/render/restruct/resimpleObject.ts (381 lines of code) (raw):

/**************************************************************************** * 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 { Box2Abs, SimpleObjectMode, Vec2 } from 'domain/entities'; import { LayerMap } from './generalEnumTypes'; import ReObject from './reobject'; import ReStruct from './restruct'; import { Render } from '../raphaelRender'; import { Scale } from 'domain/helpers'; import draw from '../draw'; import util from '../util'; import { tfx } from 'utilities'; interface MinDistanceWithReferencePoint { minDist: number; refPoint: Vec2 | null; } interface StyledPath { path: any; stylesApplied: boolean; } class ReSimpleObject extends ReObject { private item: any; private selectionSet: any; private selectionPointsSet: any; constructor(simpleObject: any) { super('simpleObject'); this.item = simpleObject; } static isSelectable(): boolean { return true; } calcDistance(p: Vec2, s: any): MinDistanceWithReferencePoint { const point: Vec2 = new Vec2(p.x, p.y); const distRef: MinDistanceWithReferencePoint = this.getReferencePointDistance(p); const item = this.item; const mode = item.mode; const pos = item.pos; let dist: number; switch (mode) { 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], new Vec2(rx, 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 = point.calculateDistanceToLine([pos[0], pos[1]]); } break; } 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); const bottomY = Math.max(pos[0].y, pos[1].y); const distances: Array<number> = []; if (point.x >= topX && point.x <= bottomX) { if (point.y < topY) { distances.push(topY - point.y); } else if (point.y > bottomY) { distances.push(point.y - bottomY); } else { distances.push(point.y - topY, bottomY - point.y); } } if (point.x < topX && point.y < topY) { distances.push(Vec2.dist(new Vec2(topX, topY), point)); } if (point.x > bottomX && point.y > bottomY) { distances.push(Vec2.dist(new Vec2(bottomX, bottomY), point)); } if (point.x < topX && point.y > bottomY) { distances.push(Vec2.dist(new Vec2(topX, bottomY), point)); } if (point.x > bottomX && point.y < topY) { distances.push(Vec2.dist(new Vec2(bottomX, topY), point)); } if (point.y >= topY && point.y <= bottomY) { if (point.x < topX) { distances.push(topX - point.x); } else if (point.x > bottomX) { distances.push(point.x - bottomX); } else { distances.push(point.x - topX, bottomX - point.x); } } dist = Math.min(...distances); break; } case SimpleObjectMode.line: { dist = point.calculateDistanceToLine([pos[0], pos[1]]); break; } default: { throw new Error('Unsupported shape type'); } } const refPoint: Vec2 | null = distRef.minDist <= 8 / s ? distRef.refPoint : null; // distance is a smallest between dist to figure and it's reference points dist = Math.min(distRef.minDist, dist); return { minDist: dist, refPoint }; } getReferencePointDistance(p: Vec2): MinDistanceWithReferencePoint { const dist: any = []; const refPoints = this.getReferencePoints(); refPoints.forEach((rp) => { dist.push({ minDist: Math.abs(Vec2.dist(p, rp)), refPoint: rp }); }); const minDist: MinDistanceWithReferencePoint = dist.reduce( (acc, current) => !acc ? current : acc.minDist < current.minDist ? acc : current, null, ); return minDist; } getReferencePoints(onlyOnObject = false): Array<Vec2> { const refPoints: Array<Vec2> = []; switch (this.item.mode) { case SimpleObjectMode.ellipse: case SimpleObjectMode.rectangle: { const p0: Vec2 = new Vec2( Math.min(this.item.pos[0].x, this.item.pos[1].x), Math.min(this.item.pos[0].y, this.item.pos[1].y), ); const w = Math.abs(Vec2.diff(this.item.pos[0], this.item.pos[1]).x); const h = Math.abs(Vec2.diff(this.item.pos[0], this.item.pos[1]).y); refPoints.push( new Vec2(p0.x + 0.5 * w, p0.y), new Vec2(p0.x + w, p0.y + 0.5 * h), new Vec2(p0.x + 0.5 * w, p0.y + h), new Vec2(p0.x, p0.y + 0.5 * h), ); if (!onlyOnObject || this.item.mode === SimpleObjectMode.rectangle) { refPoints.push( p0, new Vec2(p0.x, p0.y + h), new Vec2(p0.x + w, p0.y + h), new Vec2(p0.x + w, p0.y), ); } break; } case SimpleObjectMode.line: { this.item.pos.forEach((i) => refPoints.push(new Vec2(i.x, i.y, 0))); break; } default: { throw new Error('Unsupported shape type'); } } return refPoints; } getFigureHoverPath(path: any, render: Render, isBorder = true) { if (isBorder) { return path.attr({ ...render.options.hoverStyle, fill: 'none' }); } else { return path.attr(render.options.innerHoverStyle); } } hoverPath(render: Render): Array<StyledPath> { const point: Array<Vec2> = []; this.item.pos.forEach((p, index) => { point[index] = Scale.modelToCanvas(p, render.options); }); const scaleFactor = render.options.microModeScale; const paths: Array<StyledPath> = []; // TODO: It seems that inheritance will be the better approach here const lineOffset = scaleFactor / 8; switch (this.item.mode) { case SimpleObjectMode.ellipse: { const rad = Vec2.diff(point[1], point[0]); const rx = rad.x / 2; const ry = rad.y / 2; const centerX = tfx(point[0].x + rx); const centerY = tfx(point[0].y + ry); const outerBorderEllipse = render.paper.ellipse( centerX, centerY, tfx(Math.abs(rx) + lineOffset), tfx(Math.abs(ry) + lineOffset), ); paths.push({ path: this.getFigureHoverPath(outerBorderEllipse, render), stylesApplied: true, }); const fillEllipse = render.paper.ellipse( centerX, centerY, tfx(Math.abs(rx)), tfx(Math.abs(ry)), ); paths.push({ path: this.getFigureHoverPath(fillEllipse, render, false), stylesApplied: true, }); if ( Math.abs(rx) - scaleFactor / 8 > 0 && Math.abs(ry) - scaleFactor / 8 > 0 ) { const innerBorderEllipse = render.paper.ellipse( centerX, centerY, tfx(Math.abs(rx) - lineOffset), tfx(Math.abs(ry) - lineOffset), ); paths.push({ path: this.getFigureHoverPath(innerBorderEllipse, render), stylesApplied: true, }); } break; } case SimpleObjectMode.rectangle: { const leftX = Math.min(point[0].x, point[1].x); const topY = Math.min(point[0].y, point[1].y); const rightX = Math.max(point[0].x, point[1].x) - leftX; const bottomY = Math.max(point[0].y, point[1].y) - topY; const outerBorderRect = render.paper.rect( tfx(leftX - lineOffset), tfx(topY - lineOffset), tfx(rightX + 2 * lineOffset), tfx(bottomY + 2 * lineOffset), ); paths.push({ path: this.getFigureHoverPath(outerBorderRect, render), stylesApplied: true, }); const fillRect = render.paper.rect( tfx(leftX), tfx(topY), tfx(rightX), tfx(bottomY), ); paths.push({ path: this.getFigureHoverPath(fillRect, render, false), stylesApplied: true, }); if (rightX - 2 * lineOffset > 0 && bottomY - 2 * lineOffset > 0) { const innerRect = render.paper.rect( tfx(leftX + lineOffset), tfx(topY + lineOffset), tfx(rightX - 2 * lineOffset), tfx(bottomY - 2 * lineOffset), ); paths.push({ path: this.getFigureHoverPath(innerRect, render), stylesApplied: true, }); } break; } case SimpleObjectMode.line: { // TODO: reuse this code for polyline const poly: Array<string | number> = []; const angle = Math.atan( (point[1].y - point[0].y) / (point[1].x - point[0].x), ); const p0 = { x: 0, y: 0 }; const p1 = { x: 0, y: 0 }; const k = point[0].x > point[1].x ? -1 : 1; p0.x = point[0].x - k * ((scaleFactor / 8) * Math.cos(angle)); p0.y = point[0].y - k * ((scaleFactor / 8) * Math.sin(angle)); p1.x = point[1].x + k * ((scaleFactor / 8) * Math.cos(angle)); p1.y = point[1].y + k * ((scaleFactor / 8) * Math.sin(angle)); poly.push( 'M', p0.x + ((k * scaleFactor) / 8) * Math.sin(angle), p0.y - ((k * scaleFactor) / 8) * Math.cos(angle), ); poly.push( 'L', p1.x + ((k * scaleFactor) / 8) * Math.sin(angle), p1.y - ((k * scaleFactor) / 8) * Math.cos(angle), ); poly.push( 'L', p1.x - ((k * scaleFactor) / 8) * Math.sin(angle), p1.y + ((k * scaleFactor) / 8) * Math.cos(angle), ); poly.push( 'L', p0.x - ((k * scaleFactor) / 8) * Math.sin(angle), p0.y + ((k * scaleFactor) / 8) * Math.cos(angle), ); poly.push( 'L', p0.x + ((k * scaleFactor) / 8) * Math.sin(angle), p0.y - ((k * scaleFactor) / 8) * Math.cos(angle), ); paths.push({ path: render.paper.path(poly).attr(render.options.hoverStyle), stylesApplied: true, }); break; } default: { throw new Error('Unsupported shape type'); } } return paths; } drawHover(render: Render): Array<any> { const paths: Array<any> = this.hoverPath(render).map((enhPath) => { if (!enhPath.stylesApplied) { return enhPath.path.attr(render.options.hoverStyle); } return enhPath.path; }); render.ctab.addReObjectPath(LayerMap.hovering, this.visel, paths); return paths; } makeSelectionPlate(restruct: ReStruct, paper: any, styles: any): any { const pos = this.item.pos.map((p) => { return Scale.modelToCanvas(p, restruct.render.options) || new Vec2(); }); const refPoints = this.getReferencePoints(); const scaleFactor = restruct.render.options.microModeScale; this.selectionSet = restruct.render.paper.set(); this.selectionPointsSet = restruct.render.paper.set(); this.selectionSet.push( generatePath(this.item.mode, paper, pos).attr( styles.selectionStyleSimpleObject, ), ); refPoints.forEach((rp) => { const scaledRP = Scale.modelToCanvas(rp, restruct.render.options); this.selectionPointsSet.push( restruct.render.paper .circle(scaledRP.x, scaledRP.y, scaleFactor / 8) .attr({ fill: 'black' }), ); }); restruct.addReObjectPath( LayerMap.selectionPoints, this.visel, this.selectionPointsSet, ); return this.selectionSet; } togglePoints(displayFlag: boolean) { displayFlag ? this.selectionPointsSet?.show() : this.selectionPointsSet?.hide(); } show(restruct: ReStruct, options: any): void { const render = restruct.render; const pos = this.item.pos.map((p) => { return Scale.modelToCanvas(p, options) || new Vec2(); }); const path = generatePath(this.item.mode, render.paper, pos); const offset = options.offset; if (offset != null) path.translateAbs(offset.x, offset.y); this.visel.add(path, Box2Abs.fromRelBox(util.relBox(path.getBBox()))); } } function generatePath(mode: SimpleObjectMode, paper, pos: [Vec2, Vec2]): any { let path: any; switch (mode) { case SimpleObjectMode.ellipse: { path = draw.ellipse(paper, pos); break; } case SimpleObjectMode.rectangle: { path = draw.rectangle(paper, pos); break; } case SimpleObjectMode.line: { path = draw.line(paper, pos); break; } default: { throw new Error('Unsupported shape type'); } } return path; } export default ReSimpleObject;