diff --git a/example/src/App.tsx b/example/src/App.tsx index 9c42b87146..d862d0d9bf 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -50,10 +50,14 @@ const App = () => { const [errorMessage, setErrorMessage] = useState(''); const [showPolymerEditor, setShowPolymerEditor] = useState(false); + const togglePolymerEditor = (toggleValue: boolean) => { + setShowPolymerEditor(toggleValue); + window.isPolymerEditorTurnedOn = toggleValue; + }; return showPolymerEditor ? ( <> - + ) : ( <> @@ -75,7 +79,7 @@ const App = () => { ); }} /> - {enablePolymerEditor && } + {enablePolymerEditor && } {hasError && ( =12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "license": "BSD-2-Clause" @@ -9198,6 +9846,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -12547,6 +13203,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "1.4.0", "dev": true, @@ -19787,6 +20451,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rollup": { "version": "2.70.1", "license": "MIT", @@ -20202,6 +20871,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -24410,6 +25084,7 @@ "@babel/runtime": "^7.17.9", "ajv": "^8.10.0", "assert": "^2.0.0", + "d3": "^7.8.5", "lodash": "^4.17.21", "raphael": "^2.3.0", "svgpath": "^2.3.1" @@ -24425,6 +25100,7 @@ "@rollup/plugin-node-resolve": "^10.0.0", "@rollup/plugin-replace": "^2.3.4", "@rollup/plugin-strip": "^2.0.0", + "@types/d3": "^7.4.0", "@types/jest": "^27.0.3", "@types/node": "^16.11.12", "@zerollup/ts-transform-paths": "^1.7.18", diff --git a/packages/ketcher-core/__tests__/application/editor/Editor.test.ts b/packages/ketcher-core/__tests__/application/editor/Editor.test.ts new file mode 100644 index 0000000000..40e4cfd103 --- /dev/null +++ b/packages/ketcher-core/__tests__/application/editor/Editor.test.ts @@ -0,0 +1,17 @@ +import { CoreEditor } from 'application/editor'; +import { PeptideTool } from 'application/editor/tools/Peptide'; +import { createPolymerEditorCanvas } from '../../helpers/dom'; + +describe('CoreEditor', () => { + it('should track dom events and trigger handlers', () => { + const canvas: SVGSVGElement = createPolymerEditorCanvas(); + const editor: CoreEditor = new CoreEditor({ canvas, theme: {} }); + const onMousemove = jest.fn(); + jest + .spyOn(PeptideTool.prototype, 'mousemove') + .mockImplementation(onMousemove); + editor.selectTool('peptide'); + canvas.dispatchEvent(new Event('mousemove', { bubbles: true })); + expect(onMousemove).toHaveBeenCalled(); + }); +}); diff --git a/packages/ketcher-core/__tests__/application/editor/tools/PeptideTool.test.ts b/packages/ketcher-core/__tests__/application/editor/tools/PeptideTool.test.ts new file mode 100644 index 0000000000..40b9cf42f4 --- /dev/null +++ b/packages/ketcher-core/__tests__/application/editor/tools/PeptideTool.test.ts @@ -0,0 +1,32 @@ +import { CoreEditor } from 'application/editor'; +import { PeptideRenderer } from 'application/render/renderers/PeptideRenderer'; +import { peptideMonomerItem, polymerEditorTheme } from '../../../mock-data'; +import { createPolymerEditorCanvas } from '../../../helpers/dom'; + +describe('PeptideTool', () => { + it('should initiate the render of peptide preview on mouseover event', () => { + const canvas: SVGSVGElement = createPolymerEditorCanvas(); + const editor: CoreEditor = new CoreEditor({ + canvas, + theme: polymerEditorTheme, + }); + const onShow = jest.fn(); + jest.spyOn(PeptideRenderer.prototype, 'show').mockImplementation(onShow); + editor.events.selectPeptide.dispatch(peptideMonomerItem); + canvas.dispatchEvent(new Event('mouseover', { bubbles: true })); + expect(onShow).toHaveBeenCalled(); + }); + + it('should initiate the render of peptide mousedown', () => { + const canvas: SVGSVGElement = createPolymerEditorCanvas(); + const editor: CoreEditor = new CoreEditor({ + canvas, + theme: polymerEditorTheme, + }); + const onShow = jest.fn(); + jest.spyOn(PeptideRenderer.prototype, 'show').mockImplementation(onShow); + editor.events.selectPeptide.dispatch(peptideMonomerItem); + canvas.dispatchEvent(new Event('mouseover', { bubbles: true })); + expect(onShow).toHaveBeenCalled(); + }); +}); diff --git a/packages/ketcher-core/__tests__/application/render/renderers/PeptideRenderer.test.ts b/packages/ketcher-core/__tests__/application/render/renderers/PeptideRenderer.test.ts new file mode 100644 index 0000000000..756b1df328 --- /dev/null +++ b/packages/ketcher-core/__tests__/application/render/renderers/PeptideRenderer.test.ts @@ -0,0 +1,19 @@ +import { createPolymerEditorCanvas } from '../../../helpers/dom'; +import { PeptideRenderer } from 'application/render/renderers/PeptideRenderer'; +import { Peptide } from 'domain/entities/Peptide'; +import { peptideMonomerItem, polymerEditorTheme } from '../../../mock-data'; + +describe('PeptideRenderer', () => { + it('should render peptide', () => { + const canvas: SVGSVGElement = createPolymerEditorCanvas(); + const peptide = new Peptide(peptideMonomerItem); + const peptideRenderer = new PeptideRenderer(peptide); + global.SVGElement.prototype.getBBox = jest.fn(); + jest + .spyOn(global.SVGElement.prototype, 'getBBox') + .mockImplementation(() => ({ width: 30, height: 20 })); + peptideRenderer.show(polymerEditorTheme); + + expect(canvas).toMatchSnapshot(); + }); +}); diff --git a/packages/ketcher-core/__tests__/application/render/renderers/__snapshots__/PeptideRenderer.test.ts.snap b/packages/ketcher-core/__tests__/application/render/renderers/__snapshots__/PeptideRenderer.test.ts.snap new file mode 100644 index 0000000000..a6ccc88ab7 --- /dev/null +++ b/packages/ketcher-core/__tests__/application/render/renderers/__snapshots__/PeptideRenderer.test.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PeptideRenderer should render peptide 1`] = ` + + + + + Abc + + + +`; diff --git a/packages/ketcher-core/__tests__/domain/entities/peptide.test.ts b/packages/ketcher-core/__tests__/domain/entities/peptide.test.ts new file mode 100644 index 0000000000..e3b1328881 --- /dev/null +++ b/packages/ketcher-core/__tests__/domain/entities/peptide.test.ts @@ -0,0 +1,27 @@ +import { Peptide } from 'domain/entities/Peptide'; +import { peptideMonomerItem } from '../../mock-data'; +import { Vec2 } from 'domain/entities'; + +describe('Peptide', () => { + it('should store its position and change it properly', () => { + const peptide = new Peptide(peptideMonomerItem); + expect(peptide.position.x).toBe(0); + expect(peptide.position.y).toBe(0); + + peptide.moveAbsolute(new Vec2(10, 10)); + + expect(peptide.position.x).toBe(10); + expect(peptide.position.y).toBe(10); + + peptide.moveRelative(new Vec2(2, 2)); + peptide.moveRelative(new Vec2(3, 3)); + + expect(peptide.position.x).toBe(15); + expect(peptide.position.y).toBe(15); + }); + + it('should give monomer label', () => { + const peptide = new Peptide(peptideMonomerItem); + expect(peptide.monomerItem.label).toBe(peptideMonomerItem.label); + }); +}); diff --git a/packages/ketcher-core/__tests__/helpers/dom.ts b/packages/ketcher-core/__tests__/helpers/dom.ts new file mode 100644 index 0000000000..ec6f771a24 --- /dev/null +++ b/packages/ketcher-core/__tests__/helpers/dom.ts @@ -0,0 +1,13 @@ +export const createPolymerEditorCanvas = (): SVGSVGElement => { + const canvas: SVGSVGElement = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'svg', + ); + + canvas.setAttribute('id', 'polymer-editor-canvas'); + canvas.setAttribute('width', '500'); + canvas.setAttribute('height', '500'); + document.body.appendChild(canvas); + + return canvas; +}; diff --git a/packages/ketcher-core/__tests__/mock-data.ts b/packages/ketcher-core/__tests__/mock-data.ts index 0d8324975a..a953a12987 100644 --- a/packages/ketcher-core/__tests__/mock-data.ts +++ b/packages/ketcher-core/__tests__/mock-data.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { ReAtom, ReBond } from 'application/render'; -import { Box2Abs, Pool, Vec2 } from 'domain/entities'; +import { Box2Abs, Pool, Struct, Vec2 } from 'domain/entities'; import { mockFn } from 'jest-mock-extended'; +import { MonomerItemType } from 'domain/types'; const mockAtoms = [ { @@ -733,3 +734,22 @@ molecule.bonds.forEach((bond, bid) => { }); export const singleBond = { type: 1, stereo: 0 }; + +export const peptideMonomerItem: MonomerItemType = { + favorite: false, + label: 'Abc', + props: { + BranchMonomer: '', + MonomerCaps: '', + MonomerCode: '', + MonomerName: '', + MonomerType: '', + Name: '', + MonomerNaturalAnalogCode: 'A', + }, + struct: new Struct(), +}; + +export const polymerEditorTheme = { + monomer: { color: { A: { regular: 'yellow' } } }, +}; diff --git a/packages/ketcher-core/jest.config.js b/packages/ketcher-core/jest.config.js index 144f7e9fff..283c4199f5 100644 --- a/packages/ketcher-core/jest.config.js +++ b/packages/ketcher-core/jest.config.js @@ -10,6 +10,7 @@ module.exports = { 'domain(.*)$': '/src/domain/$1', 'infrastructure(.*)$': '/src/infrastructure/$1', 'utilities(.*)$': '/src/utilities/$1', + '^d3$': '/../../node_modules/d3/dist/d3.min.js', }, globals: { 'ts-jest': { diff --git a/packages/ketcher-core/package.json b/packages/ketcher-core/package.json index f2acb2803b..284c669d63 100644 --- a/packages/ketcher-core/package.json +++ b/packages/ketcher-core/package.json @@ -45,6 +45,7 @@ "@babel/runtime": "^7.17.9", "ajv": "^8.10.0", "assert": "^2.0.0", + "d3": "^7.8.5", "lodash": "^4.17.21", "raphael": "^2.3.0", "svgpath": "^2.3.1" @@ -60,6 +61,7 @@ "@rollup/plugin-node-resolve": "^10.0.0", "@rollup/plugin-replace": "^2.3.4", "@rollup/plugin-strip": "^2.0.0", + "@types/d3": "^7.4.0", "@types/jest": "^27.0.3", "@types/node": "^16.11.12", "@zerollup/ts-transform-paths": "^1.7.18", diff --git a/packages/ketcher-core/src/application/editor/Editor.ts b/packages/ketcher-core/src/application/editor/Editor.ts new file mode 100644 index 0000000000..67207545de --- /dev/null +++ b/packages/ketcher-core/src/application/editor/Editor.ts @@ -0,0 +1,178 @@ +import { Subscription, DOMSubscription } from 'subscription'; +import { ReStruct } from 'application/render'; +import { Struct, Vec2 } from 'domain/entities'; +import { + Tool, + ToolConstructorInterface, + ToolEventHandlerName, +} from 'application/editor/tools/Tool'; +import { toolsMap } from 'application/editor/tools'; +import { MonomerItemType } from 'domain/types'; + +interface ICoreEditorConstructorParams { + theme; + canvas: SVGSVGElement; +} + +function isMouseMainButtonPressed(event: MouseEvent) { + return event.button === 0; +} + +export class CoreEditor { + public events = { + selectPeptide: new Subscription(), + selectTool: new Subscription(), + }; + + public renderersContainer: ReStruct; + public lastCursorPosition: Vec2 = new Vec2(0, 0); + private canvas: SVGSVGElement; + public theme; + // private lastEvent: Event | undefined; + private tool?: Tool; + + constructor({ theme, canvas }: ICoreEditorConstructorParams) { + this.theme = theme; + this.canvas = canvas; + this.subscribeEvents(); + this.renderersContainer = new ReStruct(new Struct(), { + skipRaphaelInitialization: true, + theme, + }); + this.domEventSetup(); + } + + private subscribeEvents() { + this.events.selectPeptide.add((peptide) => this.onSelectPeptide(peptide)); + this.events.selectTool.add((tool) => this.onSelectTool(tool)); + } + + private onSelectPeptide(peptide: MonomerItemType) { + this.selectTool('peptide', peptide); + } + + private onSelectTool(tool: string) { + this.selectTool(tool); + } + + public selectTool(name: string, options?) { + const ToolConstructor: ToolConstructorInterface = toolsMap[name]; + + this.tool = new ToolConstructor(this, options); + } + + private domEventSetup() { + const trackedDomEvents: { + target: Node; + eventName: string; + toolEventHandler: ToolEventHandlerName; + }[] = [ + { + target: this.canvas, + eventName: 'click', + toolEventHandler: 'click', + }, + { + target: this.canvas, + eventName: 'dblclick', + toolEventHandler: 'dblclick', + }, + { + target: this.canvas, + eventName: 'mousedown', + toolEventHandler: 'mousedown', + }, + { + target: document, + eventName: 'mousemove', + toolEventHandler: 'mousemove', + }, + { + target: document, + eventName: 'mouseup', + toolEventHandler: 'mouseup', + }, + { + target: document, + eventName: 'mouseleave', + toolEventHandler: 'mouseleave', + }, + { + target: this.canvas, + eventName: 'mouseleave', + toolEventHandler: 'mouseLeaveClientArea', + }, + { + target: this.canvas, + eventName: 'mouseover', + toolEventHandler: 'mouseover', + }, + ]; + + trackedDomEvents.forEach(({ target, eventName, toolEventHandler }) => { + this.events[eventName] = new DOMSubscription(); + const subs = this.events[eventName]; + + target.addEventListener(eventName, subs.dispatch.bind(subs)); + + subs.add((event) => { + this.updateLastCursorPosition(event); + + if ( + ['mouseup', 'mousedown', 'click', 'dbclick'].includes(event.type) && + !isMouseMainButtonPressed(event) + ) { + return true; + } + + // if (eventName !== 'mouseup' && eventName !== 'mouseleave') { + // // to complete drag actions + // if (!event.target || event.target.nodeName === 'DIV') { + // // click on scroll + // this.hover(null); + // return true; + // } + // } + + const isToolUsed = this.useToolIfNeeded(toolEventHandler, event); + if (isToolUsed) { + return true; + } + + return true; + }, -1); + }); + } + + private updateLastCursorPosition(event) { + const events = ['mousemove', 'click', 'mousedown', 'mouseup', 'mouseover']; + if (events.includes(event.type)) { + const clientAreaBoundingBox = this.canvas.getBoundingClientRect(); + + this.lastCursorPosition = new Vec2({ + x: event.pageX - clientAreaBoundingBox.x, + y: event.pageY - clientAreaBoundingBox.y, + }); + } + } + + private useToolIfNeeded(eventHandlerName: ToolEventHandlerName, event) { + const editorTool = this.tool; + if (!editorTool) { + return false; + } + // this.lastEvent = event; + const conditions = [ + eventHandlerName in editorTool, + this.canvas.contains(event.target) || editorTool.isSelectionRunning?.(), + // isContextMenuClosed(editor.contextMenu), + ]; + + if (conditions.every((condition) => condition)) { + editorTool[eventHandlerName]?.(event); + return true; + } + + return false; + } +} diff --git a/packages/ketcher-core/src/application/editor/actions/index.ts b/packages/ketcher-core/src/application/editor/actions/index.ts index 0af2498330..ad81b59cf7 100644 --- a/packages/ketcher-core/src/application/editor/actions/index.ts +++ b/packages/ketcher-core/src/application/editor/actions/index.ts @@ -18,3 +18,4 @@ export * from './template'; export * from './text'; export * from './utils'; export * from './highlight'; +export * from './peptide'; diff --git a/packages/ketcher-core/src/application/editor/actions/peptide.ts b/packages/ketcher-core/src/application/editor/actions/peptide.ts new file mode 100644 index 0000000000..ece319396a --- /dev/null +++ b/packages/ketcher-core/src/application/editor/actions/peptide.ts @@ -0,0 +1,31 @@ +/**************************************************************************** + * 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 { ReStruct } from 'application/render'; +import { Action } from 'application/editor'; +import { PeptideAdd } from 'application/editor/operations/peptide'; +import { Vec2 } from 'domain/entities'; +import { MonomerItemType } from 'domain/types'; + +export function fromPeptideAddition( + renderersContainer: ReStruct, + peptide: MonomerItemType, + position: Vec2, +): Action { + const action = new Action(); + action.addOp(new PeptideAdd(peptide, position)).perform(renderersContainer); + + return action; +} diff --git a/packages/ketcher-core/src/application/editor/index.ts b/packages/ketcher-core/src/application/editor/index.ts index 536fb31153..eb22c6275e 100644 --- a/packages/ketcher-core/src/application/editor/index.ts +++ b/packages/ketcher-core/src/application/editor/index.ts @@ -22,3 +22,4 @@ export * from './operations'; export * from './actions'; export * from './shared/constants'; export * from './editor.types'; +export * from './Editor'; diff --git a/packages/ketcher-core/src/application/editor/operations/peptide/index.ts b/packages/ketcher-core/src/application/editor/operations/peptide/index.ts new file mode 100644 index 0000000000..4b58c04b5c --- /dev/null +++ b/packages/ketcher-core/src/application/editor/operations/peptide/index.ts @@ -0,0 +1,57 @@ +/**************************************************************************** + * 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. + ***************************************************************************/ +/* eslint-disable @typescript-eslint/no-use-before-define */ + +import { Vec2 } from 'domain/entities'; +import { ReStruct } from '../../../render'; + +import { BaseOperation } from '../base'; +import { OperationType } from '../OperationType'; +import { Peptide } from 'domain/entities/Peptide'; +import { PeptideRenderer } from 'application/render/renderers/PeptideRenderer'; +import { MonomerItemType } from 'domain/types'; + +type Data = { + peptide: MonomerItemType; + position: Vec2; +}; + +class PeptideAdd extends BaseOperation { + data: Data; + + constructor(peptide: MonomerItemType, position: Vec2) { + super(OperationType.ATOM_ADD); + this.data = { peptide, position }; + } + + execute(restruct: ReStruct) { + const { peptide, position } = this.data; + + const struct = restruct.molecule; + const newPeptide = new Peptide(peptide, position); + const peptideRenderer = new PeptideRenderer(newPeptide); + struct.peptides.add(newPeptide); + restruct.peptides.set(newPeptide.id, peptideRenderer); + } + + invert() { + const inverted = new PeptideAdd(this.data.peptide, this.data.position); + inverted.data = this.data; + return inverted; + } +} + +export { PeptideAdd }; diff --git a/packages/ketcher-core/src/application/editor/tools/Peptide.ts b/packages/ketcher-core/src/application/editor/tools/Peptide.ts new file mode 100644 index 0000000000..5fc946578d --- /dev/null +++ b/packages/ketcher-core/src/application/editor/tools/Peptide.ts @@ -0,0 +1,80 @@ +/**************************************************************************** + * 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 { Tool } from 'application/editor/tools/Tool'; +import { Peptide } from 'domain/entities/Peptide'; +import { Vec2 } from 'domain/entities'; +import { CoreEditor, fromPeptideAddition } from 'application/editor'; +import { PeptideRenderer } from 'application/render/renderers/PeptideRenderer'; +import { MonomerItemType } from 'domain/types'; + +class PeptideTool implements Tool { + private peptidePreview: Peptide | undefined; + private peptidePreviewRenderer: PeptideRenderer | undefined; + readonly PEPTIDE_PREVIEW_SCALE_FACTOR = 0.4; + readonly PEPTIDE_PREVIEW_OFFSET_X = 8; + readonly PEPTIDE_PREVIEW_OFFSET_Y = 12; + constructor(private editor: CoreEditor, private peptide: MonomerItemType) { + this.editor = editor; + this.peptide = peptide; + } + + mousedown() { + if (!this.peptidePreviewRenderer) { + throw new Error('peptidePreviewRenderer is not initialized'); + } + + fromPeptideAddition( + this.editor.renderersContainer, + this.peptide, + new Vec2( + this.editor.lastCursorPosition.x - + this.peptidePreviewRenderer.width / 2, + this.editor.lastCursorPosition.y - + this.peptidePreviewRenderer.height / 2, + ), + ); + this.editor.renderersContainer.update(false); + } + + mousemove() { + this.peptidePreview?.moveAbsolute( + new Vec2( + this.editor.lastCursorPosition.x + this.PEPTIDE_PREVIEW_OFFSET_X, + this.editor.lastCursorPosition.y + this.PEPTIDE_PREVIEW_OFFSET_Y, + ), + ); + this.peptidePreviewRenderer?.move(); + } + + public mouseLeaveClientArea() { + this.peptidePreviewRenderer?.remove(); + this.peptidePreviewRenderer = undefined; + this.peptidePreview = undefined; + } + + public mouseover() { + if (!this.peptidePreview) { + this.peptidePreview = new Peptide(this.peptide); + this.peptidePreviewRenderer = new PeptideRenderer( + this.peptidePreview, + this.PEPTIDE_PREVIEW_SCALE_FACTOR, + ); + this.peptidePreviewRenderer.show(this.editor.theme); + } + } +} + +export { PeptideTool }; diff --git a/packages/ketcher-core/src/application/editor/tools/SelectLasso.ts b/packages/ketcher-core/src/application/editor/tools/SelectLasso.ts new file mode 100644 index 0000000000..8065126bd8 --- /dev/null +++ b/packages/ketcher-core/src/application/editor/tools/SelectLasso.ts @@ -0,0 +1,43 @@ +/**************************************************************************** + * 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 { Tool } from 'application/editor/tools/Tool'; +import { Vec2 } from 'domain/entities'; + +class SelectLasso implements Tool { + private isMouseDown = false; + private selectedItem; + + mousedown(event) { + this.isMouseDown = true; + this.selectedItem = event.target.__data__; + } + + mousemove(event) { + if (this.isMouseDown && this.selectedItem) { + this.selectedItem.peptide.moveRelative( + new Vec2(event.movementX, event.movementY), + ); + this.selectedItem.move(); + } + } + + mouseup() { + this.isMouseDown = false; + this.selectedItem = null; + } +} + +export { SelectLasso }; diff --git a/packages/ketcher-core/src/application/editor/tools/Tool.ts b/packages/ketcher-core/src/application/editor/tools/Tool.ts new file mode 100644 index 0000000000..50efbede7e --- /dev/null +++ b/packages/ketcher-core/src/application/editor/tools/Tool.ts @@ -0,0 +1,37 @@ +import { MonomerItemType } from 'domain/types'; + +interface ToolEventHandler { + click?(event: Event): void; + + dblclick?(event: Event): void; + + mousedown?(event: Event): void; + + mousemove?(event: Event): void; + + mouseup?(event: Event): void; + + mouseleave?(event: Event): void; + + mouseLeaveClientArea?(event: Event): void; + + mouseover?(event: Event): void; +} + +export interface Tool extends ToolEventHandler { + cancel?(): void; + + isSelectionRunning?(): boolean; + + isNotActiveTool?: boolean; +} + +export type PeptideToolOptions = MonomerItemType; + +export type ToolOptions = PeptideToolOptions; + +export type ToolConstructorInterface = { + new (editor, ...args: ToolOptions[]): Tool; +}; + +export type ToolEventHandlerName = keyof ToolEventHandler; diff --git a/packages/ketcher-core/src/application/editor/tools/index.ts b/packages/ketcher-core/src/application/editor/tools/index.ts new file mode 100644 index 0000000000..971ef2cdad --- /dev/null +++ b/packages/ketcher-core/src/application/editor/tools/index.ts @@ -0,0 +1,23 @@ +/**************************************************************************** + * 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 { ToolConstructorInterface } from './Tool'; +import { PeptideTool } from 'application/editor/tools/Peptide'; +import { SelectLasso } from 'application/editor/tools/SelectLasso'; + +export const toolsMap: Record = { + peptide: PeptideTool, + 'select-lasso': SelectLasso, +}; diff --git a/packages/ketcher-core/src/application/render/raphaelRender.ts b/packages/ketcher-core/src/application/render/raphaelRender.ts index 4bd8f89bc4..e3c1173dfe 100644 --- a/packages/ketcher-core/src/application/render/raphaelRender.ts +++ b/packages/ketcher-core/src/application/render/raphaelRender.ts @@ -25,6 +25,7 @@ import draw from './draw'; import { RenderOptions } from './render.types'; export class Render { + public skipRaphaelInitialization = false; public readonly clientArea: HTMLElement; public readonly paper: RaphaelPaper; // TODO https://github.com/epam/ketcher/issues/2631 diff --git a/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts b/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts new file mode 100644 index 0000000000..12912d9160 --- /dev/null +++ b/packages/ketcher-core/src/application/render/renderers/BaseRenderer.ts @@ -0,0 +1,15 @@ +import { select, Selection } from 'd3'; + +export abstract class BaseRenderer { + public static isSelectable = () => false; + protected abstract rootElement: + | Selection + | undefined; + + protected canvas: Selection; + protected constructor() { + this.canvas = select('#polymer-editor-canvas'); + } + + public abstract show(theme): void; +} diff --git a/packages/ketcher-core/src/application/render/renderers/PeptideRenderer.ts b/packages/ketcher-core/src/application/render/renderers/PeptideRenderer.ts new file mode 100644 index 0000000000..552bb1bad4 --- /dev/null +++ b/packages/ketcher-core/src/application/render/renderers/PeptideRenderer.ts @@ -0,0 +1,118 @@ +import { BaseRenderer } from './BaseRenderer'; +import { Selection } from 'd3'; +import { Peptide } from 'domain/entities/Peptide'; + +export class PeptideRenderer extends BaseRenderer { + protected rootElement: + | Selection + | undefined; + + static isSelectable() { + return true; + } + + constructor(private peptide: Peptide, private scale?: number) { + super(); + } + + public get rootBBox() { + const rootNode = this.rootElement?.node(); + if (!rootNode) return; + + return rootNode.getBBox(); + } + + public get width() { + return this.rootBBox?.width || 0; + } + + public get height() { + return this.rootBBox?.height || 0; + } + + public get textColor() { + const WHITE = 'white'; + const colorsMap = { + D: WHITE, + F: WHITE, + K: WHITE, + Q: WHITE, + R: WHITE, + W: WHITE, + Y: WHITE, + }; + return ( + colorsMap[this.peptide.monomerItem.props.MonomerNaturalAnalogCode] || + 'black' + ); + } + + private appendRootElement( + canvas: Selection, + ) { + return canvas + .append('g') + .data([this]) + .attr('transition', 'transform 0.2s') + .attr( + 'transform', + `translate(${this.peptide.position.x}, ${ + this.peptide.position.y + }) scale(${this.scale || 1})`, + ); + } + + private appendHexagon( + rootElement: Selection, + theme, + ) { + return rootElement + .append('use') + .data([this]) + .attr('href', '#peptide') + .attr( + 'fill', + theme.monomer.color[ + this.peptide.monomerItem.props.MonomerNaturalAnalogCode + ].regular, + ); + } + + private appendLabel( + rootElement: Selection, + ) { + const textElement = rootElement + .append('text') + .text(this.peptide.label) + .attr('fill', this.textColor) + .attr('font-size', '12px') + .attr('line-height', '12px') + .attr('font-weight', '700'); + + const textBBox = (textElement.node() as SVGTextElement).getBBox(); + + textElement + .attr('x', this.width / 2 - textBBox.width / 2) + .attr('y', this.height / 2); + } + + public show(theme) { + this.rootElement = this.rootElement || this.appendRootElement(this.canvas); + this.appendHexagon(this.rootElement, theme); + this.appendLabel(this.rootElement); + } + + public move() { + this.rootElement?.attr( + 'transform', + `translate(${this.peptide.position.x}, ${ + this.peptide.position.y + }) scale(${this.scale || 1})`, + ); + } + + public remove() { + this.rootElement?.remove(); + this.rootElement = undefined; + } +} diff --git a/packages/ketcher-core/src/application/render/restruct/restruct.ts b/packages/ketcher-core/src/application/render/restruct/restruct.ts index 945d584852..ab9ba81f31 100644 --- a/packages/ketcher-core/src/application/render/restruct/restruct.ts +++ b/packages/ketcher-core/src/application/render/restruct/restruct.ts @@ -42,6 +42,7 @@ import { Render } from '../raphaelRender'; import Visel from './visel'; import util from '../util'; import { ReRGroupAttachmentPoint } from './rergroupAttachmentPoint'; +import { PeptideRenderer } from 'application/render/renderers/PeptideRenderer'; class ReStruct { public static maps = { @@ -55,6 +56,7 @@ class ReStruct { sgroupData: ReDataSGroupData, enhancedFlags: ReEnhancedFlag, sgroups: ReSGroup, + peptides: PeptideRenderer, reloops: ReLoop, simpleObjects: ReSimpleObject, texts: ReText, @@ -72,6 +74,7 @@ class ReStruct { public rgroupAttachmentPoints: Pool = new Pool(); public sgroups: Map = new Map(); + public peptides: Map = new Map(); public sgroupData: Map = new Map(); public enhancedFlags: Map = new Map(); private simpleObjects: Map = new Map(); @@ -90,11 +93,16 @@ class ReStruct { private bondsChanged: Map = new Map(); private textsChanged: Map = new Map(); private snappingBonds: number[] = []; - constructor(molecule, render: Render) { + constructor( + molecule, + render: Render | { skipRaphaelInitialization: boolean; theme }, + ) { // eslint-disable-line max-statements - this.render = render; + this.render = render as Render; this.molecule = molecule || new Struct(); - this.initLayers(); + if (!render.skipRaphaelInitialization) { + this.initLayers(); + } this.clearMarks(); // TODO: eachItem ? @@ -463,7 +471,9 @@ class ReStruct { const mapChanged = this[map + 'Changed']; mapChanged.forEach((_value, id) => { - this.clearVisel(this[map].get(id).visel); + if (this[map].get(id).visel) { + this.clearVisel(this[map].get(id).visel); + } this.structChanged = this.structChanged || mapChanged.get(id) > 0; }); }); @@ -499,6 +509,8 @@ class ReStruct { this.assignConnectedComponents(); this.initialized = true; + this.showPeptides(); + this.verifyLoops(); const updLoops = force || this.structChanged; if (updLoops) this.updateLoops(); @@ -692,6 +704,13 @@ class ReStruct { }); } + private showPeptides(): void { + this.peptides.forEach((peptideRenderer) => { + peptideRenderer.remove(); + peptideRenderer.show((this.render as any).theme); + }); + } + showEnhancedFlags(): void { const options = this.render.options; diff --git a/packages/ketcher-core/src/domain/entities/DrawingEntity.ts b/packages/ketcher-core/src/domain/entities/DrawingEntity.ts new file mode 100644 index 0000000000..93df3a98d2 --- /dev/null +++ b/packages/ketcher-core/src/domain/entities/DrawingEntity.ts @@ -0,0 +1,20 @@ +import { Vec2 } from 'domain/entities/vec2'; + +export abstract class DrawingEntity { + protected constructor(private _position: Vec2 = new Vec2(0, 0)) { + this._position = _position || new Vec2(0, 0); + } + + moveRelative(position: Vec2) { + this._position.x += position.x; + this._position.y += position.y; + } + + public moveAbsolute(position: Vec2) { + this._position = position; + } + + get position() { + return this._position; + } +} diff --git a/packages/ketcher-core/src/domain/entities/Peptide.ts b/packages/ketcher-core/src/domain/entities/Peptide.ts new file mode 100644 index 0000000000..e9f4628afc --- /dev/null +++ b/packages/ketcher-core/src/domain/entities/Peptide.ts @@ -0,0 +1,21 @@ +import { DrawingEntity } from './DrawingEntity'; +import { Vec2 } from 'domain/entities/vec2'; +import { MonomerItemType } from 'domain/types'; + +let id = 1; +export class Peptide extends DrawingEntity { + public id = 0; + constructor(private _monomerItem: MonomerItemType, _position?: Vec2) { + super(_position); + this.id = id; + id++; + } + + get monomerItem() { + return this._monomerItem; + } + + get label() { + return this.monomerItem.label; + } +} diff --git a/packages/ketcher-core/src/domain/entities/struct.ts b/packages/ketcher-core/src/domain/entities/struct.ts index 4c068abacc..3d9c6a6d93 100644 --- a/packages/ketcher-core/src/domain/entities/struct.ts +++ b/packages/ketcher-core/src/domain/entities/struct.ts @@ -36,6 +36,7 @@ import { Text } from './text'; import { Vec2 } from './vec2'; import { Highlight } from './highlight'; import { RGroupAttachmentPoint } from './rgroupAttachmentPoint'; +import { Peptide } from 'domain/entities/Peptide'; export type Neighbor = { aid: number; @@ -54,6 +55,7 @@ export class Struct { atoms: Pool; bonds: Pool; sgroups: Pool; + peptides: Pool; halfBonds: Pool; loops: Pool; isReaction: boolean; @@ -74,6 +76,7 @@ export class Struct { this.atoms = new Pool(); this.bonds = new Pool(); this.sgroups = new Pool(); + this.peptides = new Pool(); this.halfBonds = new Pool(); this.loops = new Pool(); this.isReaction = false; diff --git a/packages/ketcher-core/src/domain/types/index.ts b/packages/ketcher-core/src/domain/types/index.ts new file mode 100644 index 0000000000..684a4eb572 --- /dev/null +++ b/packages/ketcher-core/src/domain/types/index.ts @@ -0,0 +1 @@ +export * from './monomers'; diff --git a/packages/ketcher-core/src/domain/types/monomers.ts b/packages/ketcher-core/src/domain/types/monomers.ts new file mode 100644 index 0000000000..fd637b2aea --- /dev/null +++ b/packages/ketcher-core/src/domain/types/monomers.ts @@ -0,0 +1,23 @@ +import { Struct } from 'domain/entities'; + +export type MonomerColorScheme = { + regular: string; + hover: string; +}; + +export type MonomerItemType = { + label: string; + colorScheme?: MonomerColorScheme; + favorite?: boolean; + struct: Struct; + props: { + MonomerNaturalAnalogCode: string; + MonomerName: string; + Name: string; + // TODO determine whenever these props are optional or not + BranchMonomer?: string; + MonomerCaps?: string; + MonomerCode?: string; + MonomerType?: string; + }; +}; diff --git a/packages/ketcher-core/src/index.ts b/packages/ketcher-core/src/index.ts index 96b8af995c..b4b149e281 100644 --- a/packages/ketcher-core/src/index.ts +++ b/packages/ketcher-core/src/index.ts @@ -19,6 +19,7 @@ export * from 'domain/entities'; export * from 'domain/serializers'; export * from 'domain/services'; export * from 'domain/helpers'; +export * from 'domain/types'; export * from 'infrastructure/services'; diff --git a/packages/ketcher-core/src/typing.d.ts b/packages/ketcher-core/src/typing.d.ts index 3c3b174708..c7403b639c 100644 --- a/packages/ketcher-core/src/typing.d.ts +++ b/packages/ketcher-core/src/typing.d.ts @@ -3,5 +3,10 @@ import { Ketcher } from 'ketcher-core'; declare global { export interface Window { ketcher?: Ketcher; + isPolymerEditorTurnedOn: boolean; + } + + export interface SVGElement { + getBBox: () => void; } } diff --git a/packages/ketcher-polymer-editor-react/jest.config.js b/packages/ketcher-polymer-editor-react/jest.config.js index ca46183f83..3fd4fdcf2d 100644 --- a/packages/ketcher-polymer-editor-react/jest.config.js +++ b/packages/ketcher-polymer-editor-react/jest.config.js @@ -18,6 +18,7 @@ module.exports = { '^assets(.*)$': '/src/assets/$1', '^helpers(.*)$': '/src/helpers/$1', '\\.sdf$': '/textFileTransformer.js', + '^d3$': '/../../node_modules/d3/dist/d3.min.js', }, setupFilesAfterEnv: ['/src/setupTests.tsx'], }; diff --git a/packages/ketcher-polymer-editor-react/src/Editor.tsx b/packages/ketcher-polymer-editor-react/src/Editor.tsx index 197a3f7404..dc5d4a950a 100644 --- a/packages/ketcher-polymer-editor-react/src/Editor.tsx +++ b/packages/ketcher-polymer-editor-react/src/Editor.tsx @@ -32,7 +32,11 @@ import { getGlobalStyles } from 'theming/globalStyles'; import { Layout } from 'components/Layout'; import { MonomerLibrary } from 'components/monomerLibrary'; import { Menu } from 'components/menu'; -import { selectEditorActiveTool, selectTool } from 'state/common'; +import { + createEditor, + selectEditor, + selectEditorActiveTool, +} from 'state/common'; import { loadMonomerLibrary } from 'state/library'; import { useAppDispatch, useAppSelector } from 'hooks'; import { openModal } from 'state/modal'; @@ -46,12 +50,16 @@ import { EditorClassName } from './constants'; const muiTheme = createTheme(muiOverrides); -interface EditorProps { +interface EditorContainerProps { onInit?: () => void; theme?: DeepPartial; } -function EditorContainer({ onInit, theme }: EditorProps) { +interface EditorProps { + theme?: DeepPartial; +} + +function EditorContainer({ onInit, theme }: EditorContainerProps) { const rootElRef = useRef(null); const editorTheme: EditorTheme = theme ? merge(defaultTheme, theme) @@ -70,17 +78,19 @@ function EditorContainer({ onInit, theme }: EditorProps) {
- +
); } -function Editor() { +function Editor({ theme }: EditorProps) { const dispatch = useAppDispatch(); + const canvasRef = useRef(null); useEffect(() => { + dispatch(createEditor({ theme, canvas: canvasRef.current })); const serializer = new SdfSerializer(); const library = serializer.deserialize(monomersData); dispatch(loadMonomerLibrary(library)); @@ -93,7 +103,20 @@ function Editor() { - + + + + + + + + + @@ -110,12 +133,12 @@ function Editor() { function MenuComponent() { const dispatch = useAppDispatch(); const activeTool = useAppSelector(selectEditorActiveTool); - + const editor = useAppSelector(selectEditor); const menuItemChanged = (name) => { if (modalComponentList[name]) { dispatch(openModal(name)); } else { - dispatch(selectTool(name)); + editor.events.selectTool.dispatch(name); } }; diff --git a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/RnaAccordion/RnaAccordion.test.tsx b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/RnaAccordion/RnaAccordion.test.tsx index f7ffcec62c..c20015daf7 100644 --- a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/RnaAccordion/RnaAccordion.test.tsx +++ b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/RnaAccordion/RnaAccordion.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { RnaAccordion } from 'components/monomerLibrary/RnaBuilder/RnaAccordion/RnaAccordion'; import { getMonomerUniqueKey } from 'state/library'; -import { MonomerItemType } from 'components/monomerLibrary/monomerLibraryItem/types'; +import { MonomerItemType } from 'ketcher-core'; describe('Test Rna Accordion component', () => { it('should render', () => { diff --git a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/RnaAccordion/RnaAccordion.tsx b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/RnaAccordion/RnaAccordion.tsx index 21755a63f5..547f351f9e 100644 --- a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/RnaAccordion/RnaAccordion.tsx +++ b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/RnaAccordion/RnaAccordion.tsx @@ -57,7 +57,7 @@ import { GroupContainer, ItemsContainer, } from 'components/monomerLibrary/monomerLibraryGroup/styles'; -import { MonomerItemType } from 'components/monomerLibrary/monomerLibraryItem/types'; +import { MonomerItemType } from 'ketcher-core'; interface IGroupsDataItem { groupName: RnaBuilderItem; diff --git a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/types.ts b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/types.ts index 2eafc8682c..9b3d6881c6 100644 --- a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/types.ts +++ b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/RnaBuilder/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************/ -import { MonomerItemType } from 'components/monomerLibrary/monomerLibraryItem/types'; +import { MonomerItemType } from 'ketcher-core'; export interface IExpandIconProps { expanded: boolean; diff --git a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryGroup/MonomerGroup.tsx b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryGroup/MonomerGroup.tsx index 87701f27cb..c218bc893c 100644 --- a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryGroup/MonomerGroup.tsx +++ b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryGroup/MonomerGroup.tsx @@ -17,22 +17,24 @@ import { useCallback, useMemo } from 'react'; import { EmptyFunction } from 'helpers'; import { debounce } from 'lodash'; import { MonomerItem } from '../monomerLibraryItem'; -import { MonomerItemType } from '../monomerLibraryItem/types'; import { GroupContainer, GroupTitle, ItemsContainer } from './styles'; import { IMonomerGroupProps } from './types'; import { getMonomerUniqueKey } from 'state/library'; +import { MonomerItemType } from 'ketcher-core'; import { calculatePreviewPosition } from '../../../helpers'; import { useAppDispatch, useAppSelector } from 'hooks'; -import { showPreview, selectShowPreview } from 'state/common'; +import { showPreview, selectShowPreview, selectEditor } from 'state/common'; const MonomerGroup = ({ items, title, selectedMonomerUniqueKey, + libraryName, onItemClick = EmptyFunction, }: IMonomerGroupProps) => { const dispatch = useAppDispatch(); const preview = useAppSelector(selectShowPreview); + const editor = useAppSelector(selectEditor); const dispatchShowPreview = useCallback( (payload) => dispatch(showPreview(payload)), @@ -46,7 +48,7 @@ const MonomerGroup = ({ const handleItemMouseLeave = () => { debouncedShowPreview.cancel(); - dispatch(showPreview()); + dispatch(showPreview(undefined)); }; const handleItemMouseMove = ( @@ -62,6 +64,18 @@ const MonomerGroup = ({ debouncedShowPreview({ monomer, style: previewStyle }); }; + const selectMonomer = (monomer: MonomerItemType) => { + switch (libraryName) { + case 'PEPTIDE': + editor.events.selectPeptide.dispatch(monomer); + onItemClick(monomer); + break; + default: + onItemClick(monomer); + break; + } + }; + return ( {title && ( @@ -83,7 +97,7 @@ const MonomerGroup = ({ } onMouseLeave={handleItemMouseLeave} onMouseMove={(e) => handleItemMouseMove(monomer, e)} - onClick={() => onItemClick(monomer)} + onClick={() => selectMonomer(monomer)} /> ); })} diff --git a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryGroup/types.ts b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryGroup/types.ts index 783e94e89a..410107aea6 100644 --- a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryGroup/types.ts +++ b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryGroup/types.ts @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************/ -import { MonomerItemType } from '../monomerLibraryItem/types'; +import { LibraryNameType } from '../../../constants'; +import { MonomerItemType } from 'ketcher-core'; export interface IMonomerGroupProps { items: MonomerItemType[]; onItemClick?: (item: MonomerItemType) => void; title?: string; + libraryName?: LibraryNameType; selectedMonomerUniqueKey?: string; } diff --git a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryItem/MonomerItem.test.tsx b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryItem/MonomerItem.test.tsx index dd2faa7d2d..e1fcbdce86 100644 --- a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryItem/MonomerItem.test.tsx +++ b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryItem/MonomerItem.test.tsx @@ -1,7 +1,6 @@ import { render, screen, fireEvent } from '@testing-library/react'; -import { Struct } from 'ketcher-core'; +import { MonomerItemType, Struct } from 'ketcher-core'; import { MonomerItem } from './MonomerItem'; -import { MonomerItemType } from './types'; describe('Test Monomer Item component', () => { it('Test click event', () => { diff --git a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryItem/types.ts b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryItem/types.ts index 4e1ee13d45..b73fa36960 100644 --- a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryItem/types.ts +++ b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryItem/types.ts @@ -13,25 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************/ -import { Struct } from 'ketcher-core'; -import { MonomerColorScheme } from 'theming/defaultTheme'; - -export type MonomerItemType = { - label: string; - colorScheme?: MonomerColorScheme; - favorite?: boolean; - struct: Struct; - props: { - MonomerNaturalAnalogCode: string; - MonomerName: string; - Name: string; - // TODO determine whenever these props are optional or not - BranchMonomer?: string; - MonomerCaps?: string; - MonomerCode?: string; - MonomerType: string; - }; -}; +import { MonomerItemType } from 'ketcher-core'; export interface IMonomerItemProps { item: MonomerItemType; diff --git a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryList/MonomerList.tsx b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryList/MonomerList.tsx index 71477a1a01..c6f84f1727 100644 --- a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryList/MonomerList.tsx +++ b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryList/MonomerList.tsx @@ -27,7 +27,7 @@ import { getMonomerUniqueKey, } from 'state/library'; import { MONOMER_LIBRARY_FAVORITES } from '../../../constants'; -import { MonomerItemType } from '../monomerLibraryItem/types'; +import { MonomerItemType } from 'ketcher-core'; export type Group = { groupItems: Array; @@ -57,6 +57,7 @@ const MonomerList = ({ onItemClick, libraryName }: IMonomerListProps) => { key={groupTitle} title={groups.length === 1 ? undefined : groupTitle} items={groupItems} + libraryName={libraryName} onItemClick={onItemClick || selectItem} selectedMonomerUniqueKey={selectedMonomers} /> diff --git a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryList/types.ts b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryList/types.ts index 4d8ba1fd0d..968b67d809 100644 --- a/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryList/types.ts +++ b/packages/ketcher-polymer-editor-react/src/components/monomerLibrary/monomerLibraryList/types.ts @@ -15,7 +15,7 @@ ***************************************************************************/ import { LibraryNameType } from 'src/constants'; -import { MonomerItemType } from '../monomerLibraryItem/types'; +import { MonomerItemType } from 'ketcher-core'; export type Group = { groupItems: Array; diff --git a/packages/ketcher-polymer-editor-react/src/components/shared/MonomerPreview/index.tsx b/packages/ketcher-polymer-editor-react/src/components/shared/MonomerPreview/index.tsx index e8490284dc..5c75f320ad 100644 --- a/packages/ketcher-polymer-editor-react/src/components/shared/MonomerPreview/index.tsx +++ b/packages/ketcher-polymer-editor-react/src/components/shared/MonomerPreview/index.tsx @@ -23,7 +23,6 @@ import { selectShowPreview } from 'state/common'; const MonomerPreview = ({ className }: IPreviewProps) => { const preview = useAppSelector(selectShowPreview); - const ContainerDinamic = useMemo( () => styled(Container)` top: ${preview?.style || ''}; diff --git a/packages/ketcher-polymer-editor-react/src/components/shared/MonomerPreview/types.ts b/packages/ketcher-polymer-editor-react/src/components/shared/MonomerPreview/types.ts index ffe1c23685..83255788e1 100644 --- a/packages/ketcher-polymer-editor-react/src/components/shared/MonomerPreview/types.ts +++ b/packages/ketcher-polymer-editor-react/src/components/shared/MonomerPreview/types.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************/ + export interface IPreviewProps { className?: string; } diff --git a/packages/ketcher-polymer-editor-react/src/helpers/calculatePreviewPosition.ts b/packages/ketcher-polymer-editor-react/src/helpers/calculatePreviewPosition.ts index 47ecfa60a8..b769e4cdb6 100644 --- a/packages/ketcher-polymer-editor-react/src/helpers/calculatePreviewPosition.ts +++ b/packages/ketcher-polymer-editor-react/src/helpers/calculatePreviewPosition.ts @@ -14,7 +14,7 @@ * limitations under the License. ***************************************************************************/ import { EditorClassName, preview } from '../constants'; -import { MonomerItemType } from '../components/monomerLibrary/monomerLibraryItem/types'; +import { MonomerItemType } from 'ketcher-core'; export const calculatePreviewPosition = ( monomer: MonomerItemType | undefined, diff --git a/packages/ketcher-polymer-editor-react/src/helpers/getDefaultPreset.ts b/packages/ketcher-polymer-editor-react/src/helpers/getDefaultPreset.ts index 04b0d768bf..2b8e7b88b6 100644 --- a/packages/ketcher-polymer-editor-react/src/helpers/getDefaultPreset.ts +++ b/packages/ketcher-polymer-editor-react/src/helpers/getDefaultPreset.ts @@ -1,5 +1,5 @@ import { IRnaPreset } from 'components/monomerLibrary/RnaBuilder/types'; -import { MonomerItemType } from 'components/monomerLibrary/monomerLibraryItem/types'; +import { MonomerItemType } from 'ketcher-core'; const defaultPresetBases = { A: 'Adenine', diff --git a/packages/ketcher-polymer-editor-react/src/state/common/editorSaga.ts b/packages/ketcher-polymer-editor-react/src/state/common/editorSaga.ts index df6118b083..c0b24b8c7e 100644 --- a/packages/ketcher-polymer-editor-react/src/state/common/editorSaga.ts +++ b/packages/ketcher-polymer-editor-react/src/state/common/editorSaga.ts @@ -15,7 +15,7 @@ ***************************************************************************/ import { put, takeEvery, call } from 'redux-saga/effects'; -import { init, initFailure, initSuccess } from 'state/common'; +import { editorSlice, init, initFailure, initSuccess } from 'state/common'; const FETCH_DATA = 'editor/fetchData'; @@ -25,12 +25,12 @@ const fetchDataCall = () => }); function* fetchData() { - yield put(init()); + yield put(init(editorSlice)); try { yield call(fetchDataCall); - yield put(initSuccess()); + yield put(initSuccess(editorSlice)); } catch (e) { - yield put(initFailure()); + yield put(initFailure(editorSlice)); } } diff --git a/packages/ketcher-polymer-editor-react/src/state/common/editorSlice.ts b/packages/ketcher-polymer-editor-react/src/state/common/editorSlice.ts index eab8124d04..385396b136 100644 --- a/packages/ketcher-polymer-editor-react/src/state/common/editorSlice.ts +++ b/packages/ketcher-polymer-editor-react/src/state/common/editorSlice.ts @@ -14,23 +14,27 @@ * limitations under the License. ***************************************************************************/ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit'; import { RootState } from 'state'; -import { MonomerItemType } from 'components/monomerLibrary/monomerLibraryItem/types'; +import { CoreEditor, MonomerItemType } from 'ketcher-core'; +import { ThemeType } from 'theming/defaultTheme'; +import { DeepPartial } from '../../types'; interface EditorState { isReady: boolean | null; activeTool: string; + editor: CoreEditor | undefined; preview: { monomer: undefined | MonomerItemType; style: string }; } const initialState: EditorState = { isReady: null, activeTool: 'select', + editor: undefined, preview: { monomer: undefined, style: '' }, }; -export const editorSlice = createSlice({ +export const editorSlice: Slice = createSlice({ name: 'editor', initialState, reducers: { @@ -46,6 +50,18 @@ export const editorSlice = createSlice({ selectTool: (state, action: PayloadAction) => { state.activeTool = action.payload; }, + createEditor: ( + state, + action: PayloadAction<{ + theme: DeepPartial; + canvas: SVGSVGElement; + }>, + ) => { + state.editor = new CoreEditor({ + theme: action.payload.theme, + canvas: action.payload.canvas, + }); + }, showPreview: ( state, action: PayloadAction< @@ -61,12 +77,21 @@ export const editorSlice = createSlice({ }, }); -export const { init, initSuccess, initFailure, selectTool, showPreview } = - editorSlice.actions; +export const { + init, + initSuccess, + initFailure, + selectTool, + createEditor, + showPreview, +} = editorSlice.actions; export const selectEditorIsReady = (state: RootState) => state.editor.isReady; export const selectShowPreview = (state: RootState) => state.editor.preview; export const selectEditorActiveTool = (state: RootState) => state.editor.activeTool; +export const selectEditor = (state: RootState): CoreEditor => + state.editor.editor; + export const editorReducer = editorSlice.reducer; diff --git a/packages/ketcher-polymer-editor-react/src/state/library/librarySlice.ts b/packages/ketcher-polymer-editor-react/src/state/library/librarySlice.ts index ec449833d3..9a1a271a68 100644 --- a/packages/ketcher-polymer-editor-react/src/state/library/librarySlice.ts +++ b/packages/ketcher-polymer-editor-react/src/state/library/librarySlice.ts @@ -15,9 +15,8 @@ ***************************************************************************/ import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit'; -import { MonomerItemType } from 'components/monomerLibrary/monomerLibraryItem/types'; import { Group } from 'components/monomerLibrary/monomerLibraryList/types'; -import { SdfItem } from 'ketcher-core'; +import { MonomerItemType, SdfItem } from 'ketcher-core'; import { LibraryNameType } from 'src/constants'; interface LibraryState { diff --git a/packages/ketcher-polymer-editor-react/src/state/rna-builder/rnaBuilderSlice.ts b/packages/ketcher-polymer-editor-react/src/state/rna-builder/rnaBuilderSlice.ts index 9f09746a7f..c7c62eac28 100644 --- a/packages/ketcher-polymer-editor-react/src/state/rna-builder/rnaBuilderSlice.ts +++ b/packages/ketcher-polymer-editor-react/src/state/rna-builder/rnaBuilderSlice.ts @@ -18,7 +18,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { IRnaPreset } from 'components/monomerLibrary/RnaBuilder/types'; import { RootState } from 'state'; import { MonomerGroups } from '../../constants'; -import { MonomerItemType } from 'components/monomerLibrary/monomerLibraryItem/types'; +import { MonomerItemType } from 'ketcher-core'; export enum RnaBuilderPresetsItem { Presets = 'Presets', diff --git a/packages/ketcher-polymer-editor-react/src/theming/defaultTheme/defaultTheme.ts b/packages/ketcher-polymer-editor-react/src/theming/defaultTheme/defaultTheme.ts index 11eaa74365..c4f4dab346 100644 --- a/packages/ketcher-polymer-editor-react/src/theming/defaultTheme/defaultTheme.ts +++ b/packages/ketcher-polymer-editor-react/src/theming/defaultTheme/defaultTheme.ts @@ -15,7 +15,8 @@ ***************************************************************************/ import { ThemeOptions as MuiThemeOptions } from '@mui/material/styles'; -import { EditorTheme, MonomerColorScheme } from '.'; +import { EditorTheme } from '.'; +import { MonomerColorScheme } from 'ketcher-core'; const monomerColors: Record = { colorA: { regular: '#CCCBD6', hover: '#B8BBCC' }, diff --git a/packages/ketcher-polymer-editor-react/src/theming/defaultTheme/theme.types.ts b/packages/ketcher-polymer-editor-react/src/theming/defaultTheme/theme.types.ts index 6d82fcbe99..db69d94f84 100644 --- a/packages/ketcher-polymer-editor-react/src/theming/defaultTheme/theme.types.ts +++ b/packages/ketcher-polymer-editor-react/src/theming/defaultTheme/theme.types.ts @@ -17,11 +17,7 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ import '@emotion/react'; import { ThemeOptions as MuiThemeOptions } from '@mui/material/styles'; - -export type MonomerColorScheme = { - regular: string; - hover: string; -}; +import { MonomerColorScheme } from 'ketcher-core'; export type EditorTheme = { color: { diff --git a/packages/ketcher-react/jest.config.js b/packages/ketcher-react/jest.config.js index 39b6fe4b00..1fe16e0f25 100644 --- a/packages/ketcher-react/jest.config.js +++ b/packages/ketcher-react/jest.config.js @@ -12,6 +12,7 @@ module.exports = { '\\.(css|less|sdf)$': 'identity-obj-proxy', '^src(.*)$': '/src/$1', '^components$': '/src/components', + '^d3$': '/../../node_modules/d3/dist/d3.min.js', }, setupFilesAfterEnv: ['/src/setupTests.ts'], }; diff --git a/packages/ketcher-react/src/global.d.ts b/packages/ketcher-react/src/global.d.ts index 3c3b174708..72e6ea7576 100644 --- a/packages/ketcher-react/src/global.d.ts +++ b/packages/ketcher-react/src/global.d.ts @@ -3,5 +3,6 @@ import { Ketcher } from 'ketcher-core'; declare global { export interface Window { ketcher?: Ketcher; + isPolymerEditorTurnedOn: boolean; } } diff --git a/packages/ketcher-react/src/script/editor/Editor.ts b/packages/ketcher-react/src/script/editor/Editor.ts index c15262bcab..d12711f3bf 100644 --- a/packages/ketcher-react/src/script/editor/Editor.ts +++ b/packages/ketcher-react/src/script/editor/Editor.ts @@ -803,7 +803,10 @@ function domEventSetup(editor: Editor, clientArea: HTMLElement) { editor.event[eventName] = new DOMSubscription(); const subs = editor.event[eventName]; - target.addEventListener(eventName, subs.dispatch.bind(subs)); + target.addEventListener(eventName, (...args) => { + if (window.isPolymerEditorTurnedOn) return; + subs.dispatch(...args); + }); subs.add((event) => { updateLastCursorPosition(editor, event);