diff --git a/packages/ketcher-core/src/application/render/raphaelRender.js b/packages/ketcher-core/src/application/render/raphaelRender.js new file mode 100644 index 0000000000..4bff83e97e --- /dev/null +++ b/packages/ketcher-core/src/application/render/raphaelRender.js @@ -0,0 +1,252 @@ +/**************************************************************************** + * 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, Struct, Vec2 } from 'domain/entities' + +import Raphael from './raphael-ext' +import { ReStruct } from './restruct' +import { Scale } from 'domain/helpers' +import defaultOptions from './options' +import draw from './draw' + +export function Render(clientArea, opt) { + let renderWidth = opt.width || clientArea.clientWidth - 10 + let renderHeight = opt.height || clientArea.clientHeight - 10 + renderWidth = renderWidth > 0 ? renderWidth : 0 + renderHeight = renderHeight > 0 ? renderHeight : 0 + + this.userOpts = opt + this.clientArea = clientArea + this.paper = new Raphael(clientArea, renderWidth, renderHeight) + this.sz = Vec2.ZERO + this.ctab = new ReStruct(new Struct(), this) + this.options = defaultOptions(this.userOpts) +} + +Render.prototype.updateOptions = function (opts) { + try { + const passedOptions = JSON.parse(opts) + if (passedOptions && typeof passedOptions === 'object') { + this.options = { ...this.options, ...passedOptions } + return this.options + } + } catch (e) { + console.log('Not a valid settings object') + } + return false +} + +Render.prototype.selectionPolygon = function (r) { + return draw.selectionPolygon(this.paper, r, this.options) +} + +Render.prototype.selectionLine = function (p0, p1) { + return draw.selectionLine(this.paper, p0, p1, this.options) +} + +Render.prototype.selectionRectangle = function (p0, p1) { + return draw.selectionRectangle(this.paper, p0, p1, this.options) +} + +Render.prototype.view2obj = function (p, isRelative) { + let scroll = this.scrollPos() + if (!this.useOldZoom) { + p = p.scaled(1 / this.options.zoom) + scroll = scroll.scaled(1 / this.options.zoom) + } + p = isRelative ? p : p.add(scroll).sub(this.options.offset) + return Scale.scaled2obj(p, this.options) +} + +Render.prototype.obj2view = function (v, isRelative) { + let p = Scale.obj2scaled(v, this.options) + p = isRelative + ? p + : p + .add(this.options.offset) + .sub(this.scrollPos().scaled(1 / this.options.zoom)) + if (!this.useOldZoom) p = p.scaled(this.options.zoom) + return p +} + +Render.prototype.scrollPos = function () { + return new Vec2(this.clientArea.scrollLeft, this.clientArea.scrollTop) +} + +Render.prototype.page2obj = function (event) { + const clientArea = this.clientArea + + const { top: offsetTop, left: offsetLeft } = + clientArea.getBoundingClientRect() + + const pp = new Vec2(event.clientX - offsetLeft, event.clientY - offsetTop) + return this.view2obj(pp) +} + +Render.prototype.setPaperSize = function (sz) { + this.sz = sz + this.paper.setSize(sz.x * this.options.zoom, sz.y * this.options.zoom) + this.setViewBox(this.options.zoom) +} + +Render.prototype.setOffset = function (newoffset) { + const delta = new Vec2( + newoffset.x - this.options.offset.x, + newoffset.y - this.options.offset.y + ) + this.clientArea.scrollLeft += delta.x + this.clientArea.scrollTop += delta.y + this.options.offset = newoffset +} + +Render.prototype.setZoom = function (zoom) { + // when scaling the canvas down it may happen that the scaled canvas is smaller than the view window + // don't forget to call setScrollOffset after zooming (or use extendCanvas directly) + this.options.zoom = zoom + this.paper.setSize(this.sz.x * zoom, this.sz.y * zoom) + this.setViewBox(zoom) +} + +function calcExtend(sSz, x0, y0, x1, y1) { + // eslint-disable-line max-params + let ex = x0 < 0 ? -x0 : 0 + let ey = y0 < 0 ? -y0 : 0 + + if (sSz.x < x1) ex += x1 - sSz.x + if (sSz.y < y1) ey += y1 - sSz.y + return new Vec2(ex, ey) +} + +Render.prototype.setScrollOffset = function (x, y) { + const clientArea = this.clientArea + const cx = clientArea.clientWidth + const cy = clientArea.clientHeight + const e = calcExtend( + this.sz.scaled(this.options.zoom), + x, + y, + cx + x, + cy + y + ).scaled(1 / this.options.zoom) + if (e.x > 0 || e.y > 0) { + this.setPaperSize(this.sz.add(e)) + const d = new Vec2(x < 0 ? -x : 0, y < 0 ? -y : 0).scaled( + 1 / this.options.zoom + ) + if (d.x > 0 || d.y > 0) { + this.ctab.translate(d) + this.setOffset(this.options.offset.add(d)) + } + } + // clientArea.scrollLeft = x + // clientArea.scrollTop = y + clientArea.scrollLeft = x * this.options.scale + clientArea.scrollTop = y * this.options.scale + // TODO: store drag position in scaled systems + // scrollLeft = clientArea.scrollLeft; + // scrollTop = clientArea.scrollTop; + this.update(false) +} + +Render.prototype.setScale = function (z) { + if (this.options.offset) { + this.options.offset = this.options.offset.scaled(1 / z).scaled(z) + } + this.userOpts.scale *= z + this.options = null + this.update(true) +} + +Render.prototype.setViewBox = function (z) { + if (!this.useOldZoom) { + this.paper.canvas.setAttribute( + 'viewBox', + '0 0 ' + this.sz.x + ' ' + this.sz.y + ) + } else this.setScale(z) +} + +Render.prototype.setMolecule = function (ctab) { + this.paper.clear() + this.ctab = new ReStruct(ctab, this) + this.options.offset = new Vec2() + this.update(false) +} + +Render.prototype.update = function (force = false, viewSz = null) { + // eslint-disable-line max-statements + viewSz = + viewSz || + new Vec2( + this.clientArea.clientWidth || 100, + this.clientArea.clientHeight || 100 + ) + + const changes = this.ctab.update(force) + this.ctab.setSelection() // [MK] redraw the selection bits where necessary + if (changes) { + const sf = this.options.scale + const bb = this.ctab + .getVBoxObj() + .transform(Scale.obj2scaled, this.options) + .translate(this.options.offset || new Vec2()) + + if (!this.options.autoScale) { + const ext = Vec2.UNIT.scaled(sf) + const eb = bb.sz().length() > 0 ? bb.extend(ext, ext) : bb + const vb = new Box2Abs( + this.scrollPos(), + viewSz.scaled(1 / this.options.zoom).sub(Vec2.UNIT.scaled(20)) + ) + const cb = Box2Abs.union(vb, eb) + if (!this.oldCb) this.oldCb = new Box2Abs() + + const sz = cb.sz().floor() + const delta = this.oldCb.p0.sub(cb.p0).ceil() + this.oldBb = bb + if (!this.sz || sz.x !== this.sz.x || sz.y !== this.sz.y) { + this.setPaperSize(sz) + } + + this.options.offset = this.options.offset || new Vec2() + if (delta.x !== 0 || delta.y !== 0) { + this.setOffset(this.options.offset.add(delta)) + this.ctab.translate(delta) + } + } else { + const sz1 = bb.sz() + const marg = this.options.autoScaleMargin + const mv = new Vec2(marg, marg) + const csz = viewSz + if (marg && (csz.x < 2 * marg + 1 || csz.y < 2 * marg + 1)) { + throw new Error('View box too small for the given margin') + } + let rescale = + this.options.rescaleAmount || + Math.max(sz1.x / (csz.x - 2 * marg), sz1.y / (csz.y - 2 * marg)) + if (this.options.maxBondLength / rescale > 1.0) rescale = 1.0 + const sz2 = sz1.add(mv.scaled(2 * rescale)) + /* eslint-disable no-mixed-operators */ + this.paper.setViewBox( + bb.pos().x - marg * rescale - (csz.x * rescale - sz2.x) / 2, + bb.pos().y - marg * rescale - (csz.y * rescale - sz2.y) / 2, + csz.x * rescale, + csz.y * rescale + ) + /* eslint-enable no-mixed-operators */ + } + } +} diff --git a/packages/ketcher-core/src/application/render/raphaelRender.ts b/packages/ketcher-core/src/application/render/raphaelRender.ts deleted file mode 100644 index c68a1abc49..0000000000 --- a/packages/ketcher-core/src/application/render/raphaelRender.ts +++ /dev/null @@ -1,299 +0,0 @@ -/**************************************************************************** - * Copyright 2021 EPAM Systems - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ***************************************************************************/ - -import { Box2Abs, Struct, Vec2 } from 'domain/entities' - -import Raphael from './raphael-ext' -import { ReStruct } from './restruct' -import { Scale } from 'domain/helpers' -import defaultOptions from './options' -import draw from './draw' -import { RaphaelPaper } from 'raphael' -// TODO: add types for options -// import { RenderOptions } from './render.types' - -function calcExtend( - scaledSz: Vec2, - x0: number, - y0: number, - x1: number, - y1: number -) { - // eslint-disable-line max-params - let ex = x0 < 0 ? -x0 : 0 - let ey = y0 < 0 ? -y0 : 0 - - if (scaledSz.x < x1) { - ex += x1 - scaledSz.x - } - if (scaledSz.y < y1) { - ey += y1 - scaledSz.y - } - - return new Vec2(ex, ey) -} - -class Render { - public clientArea: HTMLElement - private userOpts: any - public paper: RaphaelPaper - private sz: Vec2 - public ctab: ReStruct - public options: any - private renderWidth: number - private renderHeight: number - private useOldZoom: boolean - private oldCb: Box2Abs | null - - constructor(clientArea: HTMLElement, opt: any) { - this.userOpts = opt - this.clientArea = clientArea - this.renderWidth = - clientArea.clientWidth - 10 > 0 ? clientArea.clientWidth - 10 : 0 - this.renderHeight = - clientArea.clientHeight - 10 > 0 ? clientArea.clientHeight - 10 : 0 - this.paper = new Raphael(clientArea, this.renderWidth, this.renderHeight) - this.sz = Vec2.ZERO - this.ctab = new ReStruct(new Struct(), this) - this.options = defaultOptions(this.userOpts) - this.useOldZoom = false // where it comes from? - this.oldCb = null // where it comes from? - } - - updateOptions(opts) { - try { - const passedOptions = JSON.parse(opts) - if (passedOptions && typeof passedOptions === 'object') { - this.options = { ...this.options, ...passedOptions } - return this.options - } - } catch (e) { - console.log('Not a valid settings object') - } - return false - } - - selectionPolygon(r) { - return draw.selectionPolygon(this.paper, r, this.options) - } - - selectionLine(p0: Vec2, p1: Vec2) { - return draw.selectionLine(this.paper, p0, p1, this.options) - } - - selectionRectangle(p0: Vec2, p1: Vec2) { - return draw.selectionRectangle(this.paper, p0, p1, this.options) - } - - getScrollPos() { - return new Vec2(this.clientArea.scrollLeft, this.clientArea.scrollTop) - } - - view2obj(p: Vec2, isRelative?: boolean) { - let scroll = this.getScrollPos() - if (!this.useOldZoom) { - p = p.scaled(1 / this.options.zoom) - scroll = scroll.scaled(1 / this.options.zoom) - } - p = isRelative ? p : p.add(scroll).sub(this.options.offset) - return Scale.scaled2obj(p, this.options) - } - - obj2view(v: Vec2, isRelative: boolean) { - let p = Scale.obj2scaled(v, this.options) - p = isRelative - ? p - : p - .add(this.options.offset) - .sub(this.getScrollPos().scaled(1 / this.options.zoom)) - - if (!this.useOldZoom) { - p = p.scaled(this.options.zoom) - } - - return p - } - - page2obj(event: MouseEvent) { - const clientArea = this.clientArea - const { top: offsetTop, left: offsetLeft } = - clientArea.getBoundingClientRect() - - const pp = new Vec2(event.clientX - offsetLeft, event.clientY - offsetTop) - - return this.view2obj(pp) - } - - setViewBox(z: number) { - if (!this.useOldZoom) { - this.paper.canvas.setAttribute( - 'viewBox', - '0 0 ' + this.sz.x + ' ' + this.sz.y - ) - } else this.setScale(z) - } - - setScale(z: number) { - if (this.options.offset) { - this.options.offset = this.options.offset.scaled(1 / z).scaled(z) - } - this.userOpts.scale *= z - this.options = null - this.update(true) - } - - setPaperSize(sz: Vec2) { - this.sz = sz - this.paper.setSize(sz.x * this.options.zoom, sz.y * this.options.zoom) - this.setViewBox(this.options.zoom) - } - - setZoom(zoom: number) { - // when scaling the canvas down it may happen that the scaled canvas is smaller than the view window - // don't forget to call setScrollOffset after zooming (or use extendCanvas directly) - this.options.zoom = zoom - this.paper.setSize(this.sz.x * zoom, this.sz.y * zoom) - this.setViewBox(zoom) - } - - setOffset(newoffset: Vec2) { - const delta = new Vec2( - newoffset.x - this.options.offset.x, - newoffset.y - this.options.offset.y - ) - this.clientArea.scrollLeft += delta.x - this.clientArea.scrollTop += delta.y - this.options.offset = newoffset - } - - setMolecule(struct: Struct) { - this.paper.clear() - this.ctab = new ReStruct(struct, this) - this.options.offset = new Vec2() - this.update(false) - } - - setScrollOffset(x: number, y: number) { - const clientArea = this.clientArea - const cx = clientArea.clientWidth - const cy = clientArea.clientHeight - const e = calcExtend( - this.sz.scaled(this.options.zoom), - x, - y, - cx + x, - cy + y - ).scaled(1 / this.options.zoom) - if (e.x > 0 || e.y > 0) { - this.setPaperSize(this.sz.add(e)) - const d = new Vec2(x < 0 ? -x : 0, y < 0 ? -y : 0).scaled( - 1 / this.options.zoom - ) - if (d.x > 0 || d.y > 0) { - this.ctab.translate(d) - this.setOffset(this.options.offset.add(d)) - } - } - - clientArea.scrollLeft = x * this.options.scale - clientArea.scrollTop = y * this.options.scale - } - - update(force = false, viewSz: Vec2 | null = null) { - viewSz = - viewSz || - new Vec2( - this.clientArea.clientWidth || 100, - this.clientArea.clientHeight || 100 - ) - - const changes = this.ctab.update(force) - this.ctab.setSelection() // [MK] redraw the selection bits where necessary - - if (changes) { - const boundingBox = this.ctab - ?.getVBoxObj() - ?.transform(Scale.obj2scaled, this.options) - .translate(this.options.offset || new Vec2()) - - if (!this.options.autoScale && boundingBox) { - const ext = Vec2.UNIT.scaled(this.options.scale) - const extendedBox = - boundingBox.sz().length() > 0 - ? boundingBox.extend(ext, ext) - : boundingBox - const viewBox = new Box2Abs( - this.getScrollPos(), - viewSz.scaled(1 / this.options.zoom).sub(Vec2.UNIT.scaled(20)) - ) - const cb = Box2Abs.union(viewBox, extendedBox) - if (!this.oldCb) { - this.oldCb = new Box2Abs() - } - - const sz = cb.sz().floor() - const delta = this.oldCb.p0.sub(cb.p0).ceil() - // TODO: do we need this.oldBb? Seem it's not in use - // this.oldBb = bb - if (!this.sz || sz.x !== this.sz.x || sz.y !== this.sz.y) { - this.setPaperSize(sz) - } - - this.options.offset = this.options.offset || new Vec2() - if (delta.x !== 0 || delta.y !== 0) { - this.setOffset(this.options.offset.add(delta)) - this.ctab.translate(delta) - } - } else { - if (!boundingBox) { - return - } - const sz1 = boundingBox.sz() - const { autoScaleMargin, rescaleAmount, maxBondLength } = this.options - const mv = new Vec2(autoScaleMargin, autoScaleMargin) - const csz = viewSz - if ( - autoScaleMargin && - (csz.x < 2 * autoScaleMargin + 1 || csz.y < 2 * autoScaleMargin + 1) - ) { - throw new Error('View box too small for the given margin') - } - let rescale = - rescaleAmount || - Math.max( - sz1.x / (csz.x - 2 * autoScaleMargin), - sz1.y / (csz.y - 2 * autoScaleMargin) - ) - if (maxBondLength / rescale > 1.0) rescale = 1.0 - const sz2 = sz1.add(mv.scaled(2 * rescale)) - /* eslint-disable no-mixed-operators */ - this.paper.setViewBox( - boundingBox.pos().x - - autoScaleMargin * rescale - - (csz.x * rescale - sz2.x) / 2, - boundingBox.pos().y - - autoScaleMargin * rescale - - (csz.y * rescale - sz2.y) / 2, - csz.x * rescale, - csz.y * rescale - ) - /* eslint-enable no-mixed-operators */ - } - } - } -} - -export { Render }