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`] = `
+
+`;
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);