├── sketchem-react ├── .stylelintignore ├── src │ ├── react-app-env.d.ts │ ├── index.css │ ├── entities │ │ └── index.ts │ ├── features │ │ ├── toolbar-item │ │ │ ├── tools │ │ │ │ ├── periodic-table │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ElementCell.tsx │ │ │ │ │ ├── PeriodicTableTool.tsx │ │ │ │ │ └── PeriodicTable.tsx │ │ │ │ ├── debug_tools │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── printMolToConsole.ts │ │ │ │ │ ├── drawAllPeriodicTable.ts │ │ │ │ │ ├── drawTree.ts │ │ │ │ │ └── drawMol.ts │ │ │ │ ├── index.ts │ │ │ │ ├── ToolsMapper.helper.ts │ │ │ │ ├── Copy.ts │ │ │ │ ├── ClearCanvas.ts │ │ │ │ ├── Erase.ts │ │ │ │ ├── Unredo.ts │ │ │ │ ├── Charge.ts │ │ │ │ ├── Paste.ts │ │ │ │ ├── Export.tsx │ │ │ │ └── AtomTool.ts │ │ │ ├── bottom-toolbar-item │ │ │ │ └── ToolbarItem.ts │ │ │ ├── index.ts │ │ │ ├── right-toolbar-item │ │ │ │ └── ToolbarItem.ts │ │ │ ├── top-toolbar-item │ │ │ │ └── ToolbarItem.ts │ │ │ ├── left-toolbar-item │ │ │ │ └── ToolbarItem.ts │ │ │ ├── DialogShow.tsx │ │ │ ├── Counter.module.css │ │ │ ├── ToolsButtonMapper.helper.ts │ │ │ ├── ToolbarItem.ts │ │ │ ├── ToolbarItems.tsx │ │ │ └── toolbarItemsSlice.ts │ │ ├── counter │ │ │ ├── counterAPI.ts │ │ │ ├── Counter.module.css │ │ │ ├── Counter.tsx │ │ │ └── counterSlice.ts │ │ ├── sketchpad │ │ │ └── svgpanzoom.js.d.ts │ │ ├── chemistry │ │ │ ├── chemistrySlice.ts │ │ │ ├── issueChecker.txt │ │ │ └── kekuleHandler.js │ │ ├── editor │ │ │ └── Editor.tsx │ │ └── shared │ │ │ ├── rbushKnn.ts │ │ │ ├── oldRbushKnn.ts │ │ │ └── storage.ts │ ├── global.d.ts │ ├── styles │ │ ├── _sketchpad.scss │ │ ├── icons │ │ │ ├── chain.svg │ │ │ ├── bond_single.svg │ │ │ ├── bond_wedge_front.svg │ │ │ ├── charge_minus.svg │ │ │ ├── bond_double.svg │ │ │ ├── redo.svg │ │ │ ├── undo.svg │ │ │ ├── charge_plus.svg │ │ │ ├── copy.svg │ │ │ ├── erase.svg │ │ │ ├── bond_single_or_double.svg │ │ │ ├── bond_triple.svg │ │ │ ├── periodic_table.svg │ │ │ ├── paste.svg │ │ │ ├── clear_canvas.svg │ │ │ ├── export.svg │ │ │ ├── import.svg │ │ │ ├── select_lasso.svg │ │ │ ├── bond_wedge_back.svg │ │ │ ├── select_box.svg │ │ │ ├── debug.svg │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ ├── _dialog.scss │ │ ├── _forms.scss │ │ ├── _layout.scss │ │ ├── _toolbar.scss │ │ ├── App.scss │ │ ├── _atoms_labels.scss │ │ ├── _periodic_table.scss │ │ └── _buttons.scss │ ├── constants │ │ ├── editor.constant.ts │ │ ├── atom.constants.ts │ │ ├── user.constants.ts │ │ ├── tools.constants.ts │ │ ├── bond.constants.ts │ │ └── enum.constants.ts │ ├── app │ │ ├── hooks.ts │ │ ├── RootReducer.ts │ │ ├── store.ts │ │ ├── selectors.ts │ │ └── resizeHook.ts │ ├── utils │ │ ├── IdUtils.ts │ │ ├── AngleUtils.ts │ │ ├── LayersUtils.ts │ │ └── mathsTs │ │ │ └── maths.ts │ ├── index.tsx │ ├── logo.svg │ ├── buttons.tsx │ ├── App.tsx │ └── types │ │ └── index_example.ts ├── .prettierignore ├── build │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── kekule-js-dist │ │ └── extra │ │ │ ├── indigo.wasm │ │ │ └── openbabel.wasm │ ├── static │ │ ├── media │ │ │ ├── chain.305046b7fa70f3c4a33f50c2ef4d5997.svg │ │ │ ├── bond_single.dda061e50dc7e697738aadaf09389896.svg │ │ │ ├── bond_wedge_front.ed67a630ca2fb54df1c73157360aca4d.svg │ │ │ ├── charge_minus.0632ab36d6e27bcb12c7feb9686606af.svg │ │ │ ├── bond_double.635d3740ba4ab18c0fce6299f21008d7.svg │ │ │ ├── redo.fbb02dbce1316e1cc84385d13c33f3c6.svg │ │ │ ├── undo.7194a4d7cf82725ae4ff24e815ca5838.svg │ │ │ ├── charge_plus.6c920f742a14e4b104adcd1ed51bf8fb.svg │ │ │ ├── copy.fb49915b75a7ac7aea0c687c37acc450.svg │ │ │ ├── erase.8f72d3fd948e4ddadd07a60fcb9938b5.svg │ │ │ ├── bond_single_or_double.6331a4c08c60e6283e149fb6482fa136.svg │ │ │ ├── bond_triple.21226e758e92bac5be3df87ee145593e.svg │ │ │ ├── periodic_table.badc0dda69fc8f1b8f4aae44672f2059.svg │ │ │ ├── paste.900d40c5cdd12f305fd5e548cabc8bda.svg │ │ │ ├── clear_canvas.1963dbde7918fdfcb6d0510b44b0bd42.svg │ │ │ ├── export.6bd71e290f10bb280762ac603a908fa6.svg │ │ │ ├── import.2d111a848177ca6fac4c2f4f503a3647.svg │ │ │ ├── select_lasso.4e8ec7cd5ffa0fba5e959596b6f40b26.svg │ │ │ ├── bond_wedge_back.af72e68b5c3f59e445a44d70086f6860.svg │ │ │ └── select_box.608cfb9bdbe2f4fc38270adb7ae06404.svg │ │ └── js │ │ │ └── main.6ed8c631.js.LICENSE.txt │ ├── index.html │ ├── manifest.json │ └── asset-manifest.json ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── kekule-js-dist │ │ └── extra │ │ │ ├── indigo.wasm │ │ │ └── openbabel.wasm │ ├── manifest.json │ └── index.html ├── .babelrc ├── prettier.config.js ├── .gitignore ├── tsconfig.json.bak ├── .vscode │ ├── tasks.json │ ├── launch.json │ └── settings.json ├── craco.config.js ├── tsconfig.json ├── README.md.bak ├── README.md ├── .eslintrc.js └── package.json ├── License.md └── .gitignore /sketchem-react/.stylelintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ -------------------------------------------------------------------------------- /sketchem-react/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /sketchem-react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | -------------------------------------------------------------------------------- /sketchem-react/.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .vscode/ 5 | public/ 6 | local/ -------------------------------------------------------------------------------- /sketchem-react/build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /sketchem-react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /sketchem-react/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itayYaakov/SketChem/HEAD/sketchem-react/build/favicon.ico -------------------------------------------------------------------------------- /sketchem-react/build/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itayYaakov/SketChem/HEAD/sketchem-react/build/logo192.png -------------------------------------------------------------------------------- /sketchem-react/build/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itayYaakov/SketChem/HEAD/sketchem-react/build/logo512.png -------------------------------------------------------------------------------- /sketchem-react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itayYaakov/SketChem/HEAD/sketchem-react/public/favicon.ico -------------------------------------------------------------------------------- /sketchem-react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itayYaakov/SketChem/HEAD/sketchem-react/public/logo192.png -------------------------------------------------------------------------------- /sketchem-react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itayYaakov/SketChem/HEAD/sketchem-react/public/logo512.png -------------------------------------------------------------------------------- /sketchem-react/src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Atom"; 2 | export * from "./Bond"; 3 | export * from "./Entity"; 4 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/periodic-table/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PeriodicTableTool } from "./PeriodicTableTool"; 2 | -------------------------------------------------------------------------------- /sketchem-react/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.scss" { 2 | const styles: { [className: string]: string }; 3 | export default styles; 4 | } 5 | -------------------------------------------------------------------------------- /sketchem-react/build/kekule-js-dist/extra/indigo.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itayYaakov/SketChem/HEAD/sketchem-react/build/kekule-js-dist/extra/indigo.wasm -------------------------------------------------------------------------------- /sketchem-react/public/kekule-js-dist/extra/indigo.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itayYaakov/SketChem/HEAD/sketchem-react/public/kekule-js-dist/extra/indigo.wasm -------------------------------------------------------------------------------- /sketchem-react/build/kekule-js-dist/extra/openbabel.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itayYaakov/SketChem/HEAD/sketchem-react/build/kekule-js-dist/extra/openbabel.wasm -------------------------------------------------------------------------------- /sketchem-react/public/kekule-js-dist/extra/openbabel.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itayYaakov/SketChem/HEAD/sketchem-react/public/kekule-js-dist/extra/openbabel.wasm -------------------------------------------------------------------------------- /sketchem-react/src/styles/_sketchpad.scss: -------------------------------------------------------------------------------- 1 | .sketchpad { 2 | // height: 500px; 3 | // width: 100%; 4 | // width: 100%; 5 | // height: calc(100% - 90px - 90px); 6 | } -------------------------------------------------------------------------------- /sketchem-react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "development": true, 7 | "runtime": "automatic" 8 | } 9 | ] 10 | ] 11 | } -------------------------------------------------------------------------------- /sketchem-react/src/constants/editor.constant.ts: -------------------------------------------------------------------------------- 1 | export const EditorConstants = { 2 | atomFontFamily: "Roboto, Helvetica, Helvetica Neue, Arial", 3 | atomFontSize: "0.825em", 4 | Scale: 60, 5 | }; 6 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/chain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/bond_single.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/bond_wedge_front.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'toolbar'; 2 | @use 'sketchpad'; 3 | @use 'dialog'; 4 | @use 'atoms_labels'; 5 | @use 'layout'; 6 | @import "~bootstrap/scss/bootstrap"; 7 | @import 'periodic_table'; 8 | @import 'buttons'; 9 | // @import 'forms'; -------------------------------------------------------------------------------- /sketchem-react/src/features/counter/counterAPI.ts: -------------------------------------------------------------------------------- 1 | // A mock function to mimic making an async request for data 2 | export function fetchCount(amount = 1) { 3 | return new Promise<{ data: number }>((resolve) => setTimeout(() => resolve({ data: amount }), 500)); 4 | } 5 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/chain.305046b7fa70f3c4a33f50c2ef4d5997.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/bond_single.dda061e50dc7e697738aadaf09389896.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/bond_wedge_front.ed67a630ca2fb54df1c73157360aca4d.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/charge_minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/charge_minus.0632ab36d6e27bcb12c7feb9686606af.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/bond_double.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/_dialog.scss: -------------------------------------------------------------------------------- 1 | .dialog_window { 2 | height: 90%; 3 | width: 80%; 4 | max-width: 85% !important; 5 | max-height: 90% !important; 6 | min-width: 50em !important; 7 | min-height: 50em !important; 8 | } 9 | 10 | .dialog_content { 11 | // overflow: hidden; 12 | overflow: auto; 13 | padding: 1em; 14 | } 15 | -------------------------------------------------------------------------------- /sketchem-react/src/constants/atom.constants.ts: -------------------------------------------------------------------------------- 1 | export const AtomConstants = { 2 | HoverRadius: 18, 3 | HoverStrokeWidth: 4, 4 | SelectDistance: -1, 5 | DefaultAtomsLabel: ["H", "C", "N", "O", "S", "P", "F", "Cl", "Br", "I"], 6 | MaxAtomsListSize: 15, 7 | }; 8 | AtomConstants.SelectDistance = AtomConstants.HoverRadius + AtomConstants.HoverStrokeWidth / 2; 9 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/bond_double.635d3740ba4ab18c0fce6299f21008d7.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/redo.fbb02dbce1316e1cc84385d13c33f3c6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/undo.7194a4d7cf82725ae4ff24e815ca5838.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/debug_tools/index.ts: -------------------------------------------------------------------------------- 1 | import { DrawAllPeriodic } from "./drawAllPeriodicTable"; 2 | import drawMol from "./drawMol"; 3 | import drawTree from "./drawTree"; 4 | import { ExportMolToConsoleTool } from "./printMolToConsole"; 5 | 6 | const DebugTools = [...drawTree, ...drawMol, DrawAllPeriodic, ExportMolToConsoleTool]; 7 | 8 | export default DebugTools; 9 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/charge_plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sketchem-react/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; 2 | 3 | import type { AppDispatch, RootState } from "./store"; 4 | 5 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 6 | export const useAppDispatch = () => useDispatch(); 7 | export const useAppSelector: TypedUseSelectorHook = useSelector; 8 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/charge_plus.6c920f742a14e4b104adcd1ed51bf8fb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/erase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/bond_single_or_double.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/bond_triple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sketchem-react/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 4, 4 | semi: true, 5 | singleQuote: false, 6 | printWidth: 120, 7 | useTabs: false, 8 | proseWrap: "always", 9 | quoteProps: "as-needed", 10 | bracketSameLine: false, 11 | singleAttributePerLine: false, 12 | jsxSingleQuote: false, 13 | bracketSpacing: true, 14 | arrowParens: "always", 15 | }; 16 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/copy.fb49915b75a7ac7aea0c687c37acc450.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/erase.8f72d3fd948e4ddadd07a60fcb9938b5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/periodic_table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/bond_single_or_double.6331a4c08c60e6283e149fb6482fa136.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/bottom-toolbar-item/ToolbarItem.ts: -------------------------------------------------------------------------------- 1 | import { Direction } from "@constants/enum.constants"; 2 | 3 | import { ToolbarItemButton } from "../ToolbarItem"; 4 | import type { IToolbarItemsProps } from "../ToolbarItems"; 5 | 6 | const toolbarItemsList: ToolbarItemButton[] = []; 7 | 8 | const props: IToolbarItemsProps = { 9 | toolbarItemsList, 10 | direction: Direction.Bottom, 11 | }; 12 | 13 | export default props; 14 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/paste.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/bond_triple.21226e758e92bac5be3df87ee145593e.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/index.ts: -------------------------------------------------------------------------------- 1 | export { default as BottomToolbarProps } from "./bottom-toolbar-item/ToolbarItem"; 2 | export { DialogShow } from "./DialogShow"; 3 | export { default as LeftToolbarProps } from "./left-toolbar-item/ToolbarItem"; 4 | export { default as RightToolbarProps } from "./right-toolbar-item/ToolbarItem"; 5 | export { ToolbarItems } from "./ToolbarItems"; 6 | export { default as TopToolbarProps } from "./top-toolbar-item/ToolbarItem"; 7 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/periodic_table.badc0dda69fc8f1b8f4aae44672f2059.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/clear_canvas.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/paste.900d40c5cdd12f305fd5e548cabc8bda.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sketchem-react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | # /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # lock files 26 | yarn.lock 27 | package-lock.json 28 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/clear_canvas.1963dbde7918fdfcb6d0510b44b0bd42.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/import.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/export.6bd71e290f10bb280762ac603a908fa6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/import.2d111a848177ca6fac4c2f4f503a3647.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sketchem-react/src/app/RootReducer.ts: -------------------------------------------------------------------------------- 1 | import chemistry from "@features/chemistry/chemistrySlice"; 2 | import toolbarItem from "@features/toolbar-item/toolbarItemsSlice"; 3 | import { RootState } from "@types"; 4 | import { combineReducers, Reducer } from "redux"; 5 | 6 | const rootReducer: Reducer = combineReducers({ 7 | toolbarItem, 8 | chemistry, 9 | }); 10 | 11 | // ??? not relevant for now - read more about redux epics and observables 12 | // export type RootStoreType = ReturnType 13 | export default rootReducer; 14 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/right-toolbar-item/ToolbarItem.ts: -------------------------------------------------------------------------------- 1 | import { Direction } from "@constants/enum.constants"; 2 | 3 | import { ToolbarItemButton } from "../ToolbarItem"; 4 | import type { IToolbarItemsProps } from "../ToolbarItems"; 5 | import { Charge, PeriodicTableTool } from "../tools"; 6 | 7 | const toolbarItemsList: ToolbarItemButton[] = [Charge.ChargePlus, Charge.ChargeMinus, PeriodicTableTool]; 8 | 9 | const props: IToolbarItemsProps = { 10 | toolbarItemsList, 11 | direction: Direction.Right, 12 | }; 13 | 14 | export default props; 15 | -------------------------------------------------------------------------------- /sketchem-react/src/utils/IdUtils.ts: -------------------------------------------------------------------------------- 1 | export const IdUtils = { 2 | getBondElemId: (idNum: number) => `bond_${idNum}`, 3 | getAtomElemId: (idNum: number) => `atom_${idNum}`, 4 | getDefElemId: (name: string) => `def_${name}`, 5 | getLayerElemId: (name: string) => `layer_${name}`, 6 | getUrlId: (name: string) => `url(#${name})`, 7 | idIsOfBondElem: (id: string): number | undefined => (id.startsWith("bond_") ? Number(id.split("_")[1]) : undefined), 8 | idIsOfAtomElem: (id: string): number | undefined => (id.startsWith("atom_") ? Number(id.split("_")[1]) : undefined), 9 | }; 10 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/_forms.scss: -------------------------------------------------------------------------------- 1 | %forms { 2 | display: block; 3 | border-radius: 0.3rem; 4 | border: 1px solid $accent-gray; 5 | padding: 0.75rem; 6 | outline: none; 7 | margin-bottom: 0.5rem; 8 | font-size: 1rem; 9 | width: 100%; 10 | max-width: 100%; 11 | } 12 | 13 | %forms-focus { 14 | outline: 0; 15 | border: 1px solid lighten($primary, 15%); 16 | box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 17 | } 18 | 19 | #{$forms} { 20 | @extend %forms; 21 | 22 | &:focus, 23 | &:active { 24 | @extend %forms-focus; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/select_lasso.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/bond_wedge_back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/select_lasso.4e8ec7cd5ffa0fba5e959596b6f40b26.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/index.ts: -------------------------------------------------------------------------------- 1 | export { generateAtomsButtons } from "./AtomTool"; 2 | export * as BondTool from "./BondTool"; 3 | export { default as Chain } from "./Chain"; 4 | export * as Charge from "./Charge"; 5 | export { default as ClearCanvas } from "./ClearCanvas"; 6 | export { default as Copy } from "./Copy"; 7 | export { default as Erase } from "./Erase"; 8 | export { default as Export } from "./Export"; 9 | export { default as Import } from "./Import"; 10 | export { default as Paste } from "./Paste"; 11 | export { PeriodicTableTool } from "./periodic-table"; 12 | export * as SelectToolBarItems from "./SelectTemplate"; 13 | export * as UnReDo from "./Unredo"; 14 | -------------------------------------------------------------------------------- /sketchem-react/src/utils/AngleUtils.ts: -------------------------------------------------------------------------------- 1 | import Vector2 from "./mathsTs/Vector2"; 2 | 3 | export const AngleUtils = { 4 | getPiPositiveAngle: (Va: Vector2, Vb: Vector2) => { 5 | let theta = Va.angle(Vb); 6 | if (theta < 0) theta = Math.PI + theta; // range [0, 2 pi) 7 | return theta; 8 | }, 9 | 10 | limitInSteps(angle: number, stepSize: number): number { 11 | return Math.round(angle / stepSize) * stepSize; 12 | }, 13 | 14 | degToRad: (angle: number) => (angle * Math.PI) / 180, 15 | 16 | clampPosAngleRad: (angle: number) => ((angle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI), 17 | 18 | radToDeg: (angle: number) => (angle * 180) / Math.PI, 19 | }; 20 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/bond_wedge_back.af72e68b5c3f59e445a44d70086f6860.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sketchem-react/src/constants/user.constants.ts: -------------------------------------------------------------------------------- 1 | export const userConstants = { 2 | REGISTER_REQUEST: "USERS_REGISTER_REQUEST", 3 | REGISTER_SUCCESS: "USERS_REGISTER_SUCCESS", 4 | REGISTER_FAILURE: "USERS_REGISTER_FAILURE", 5 | 6 | LOGIN_REQUEST: "USERS_LOGIN_REQUEST", 7 | LOGIN_SUCCESS: "USERS_LOGIN_SUCCESS", 8 | LOGIN_FAILURE: "USERS_LOGIN_FAILURE", 9 | 10 | LOGOUT: "USERS_LOGOUT", 11 | 12 | GETALL_REQUEST: "USERS_GETALL_REQUEST", 13 | GETALL_SUCCESS: "USERS_GETALL_SUCCESS", 14 | GETALL_FAILURE: "USERS_GETALL_FAILURE", 15 | 16 | DELETE_REQUEST: "USERS_DELETE_REQUEST", 17 | DELETE_SUCCESS: "USERS_DELETE_SUCCESS", 18 | DELETE_FAILURE: "USERS_DELETE_FAILURE", 19 | }; 20 | -------------------------------------------------------------------------------- /sketchem-react/build/index.html: -------------------------------------------------------------------------------- 1 | SketChem
-------------------------------------------------------------------------------- /sketchem-react/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /sketchem-react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /sketchem-react/tsconfig.json.bak: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "ESNEXT", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "exclude": [ 27 | "./node_modules/" 28 | ], 29 | } -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/ToolsMapper.helper.ts: -------------------------------------------------------------------------------- 1 | import { ToolbarItem } from "../ToolbarItem"; 2 | 3 | const toolbarItemsDict = new Map(); 4 | 5 | export function RegisterToolbarWithName(name: string, tool: ToolbarItem) { 6 | if (toolbarItemsDict.has(name)) { 7 | throw new Error(`Toolbar with name ${name} already registered`); 8 | } 9 | toolbarItemsDict.set(name, tool); 10 | } 11 | 12 | export function GetToolbarByName(name: string): ToolbarItem | undefined { 13 | if (!name) return undefined; 14 | const item = toolbarItemsDict.get(name); 15 | if (!item) { 16 | throw new Error(`Toolbar with name ${name} not registered`); 17 | } 18 | return item; 19 | } 20 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/top-toolbar-item/ToolbarItem.ts: -------------------------------------------------------------------------------- 1 | import { Direction } from "@constants/enum.constants"; 2 | 3 | import { ToolbarItemButton } from "../ToolbarItem"; 4 | import type { IToolbarItemsProps } from "../ToolbarItems"; 5 | import * as Tools from "../tools"; 6 | 7 | const toolbarItemsList: ToolbarItemButton[] = [ 8 | Tools.ClearCanvas, 9 | Tools.Copy, 10 | Tools.Paste, 11 | Tools.UnReDo.undo, 12 | Tools.UnReDo.redo, 13 | Tools.Import, 14 | Tools.Export, 15 | Tools.SelectToolBarItems.simpleSelect, 16 | Tools.SelectToolBarItems.lassoSelect, 17 | ]; 18 | 19 | const props: IToolbarItemsProps = { 20 | toolbarItemsList, 21 | direction: Direction.Top, 22 | }; 23 | 24 | export default props; 25 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/select_box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sketchem-react/build/static/media/select_box.608cfb9bdbe2f4fc38270adb7ae06404.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/_layout.scss: -------------------------------------------------------------------------------- 1 | $toolbar-min-size: 80%; 2 | $toolbar-max-size: 100%; 3 | 4 | .app { 5 | max-width: 100%; 6 | max-height: 100%; 7 | position: fixed; 8 | display: grid; 9 | grid-template-areas: "top top top" 10 | "left draw right" 11 | "bottom bottom bottom"; 12 | grid-template-rows: auto minmax($toolbar-min-size, $toolbar-max-size) auto; 13 | grid-template-columns: auto minmax($toolbar-min-size, $toolbar-max-size) auto; 14 | } 15 | 16 | .draw { 17 | grid-area: draw; 18 | } 19 | 20 | .top { 21 | grid-area: top; 22 | } 23 | 24 | .left { 25 | grid-area: left; 26 | } 27 | 28 | .right { 29 | grid-area: right; 30 | } 31 | 32 | .bottom { 33 | grid-area: bottom; 34 | } -------------------------------------------------------------------------------- /sketchem-react/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | 3 | import RootReducer from "./RootReducer"; 4 | 5 | const preloadedState = {}; 6 | 7 | export const store = configureStore({ 8 | reducer: RootReducer, 9 | // !!! enable this? 10 | // devTools: process.env.NODE_ENV !== 'production', 11 | devTools: true, 12 | preloadedState, 13 | // middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(), 14 | // middleware: (getDefaultMiddleware) => 15 | // getDefaultMiddleware().concat(thunkMiddleware), 16 | }); 17 | 18 | // Infer the `RootState` and `AppDispatch` types from the store itself 19 | export type RootState = ReturnType; 20 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} 21 | export type AppDispatch = typeof store.dispatch; 22 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/_toolbar.scss: -------------------------------------------------------------------------------- 1 | @use "material_colors" as c; 2 | 3 | @mixin toolbar() { 4 | display: flex; 5 | align-items: stretch; 6 | flex-wrap: nowrap; 7 | justify-content: center; 8 | overflow: hidden; 9 | background-color: c.$gray-200 10 | } 11 | 12 | @mixin horizontal-toolbar() { 13 | @include toolbar; 14 | flex-direction: row; 15 | height: 100%; 16 | } 17 | 18 | @mixin vertical-toolbar() { 19 | @include toolbar; 20 | flex-direction: column; 21 | } 22 | 23 | .toolbar-left { 24 | // @include toolbar(); 25 | @include vertical-toolbar(); 26 | } 27 | 28 | .toolbar-top { 29 | // @include toolbar(); 30 | @include horizontal-toolbar(); 31 | } 32 | 33 | .toolbar-right { 34 | // @include toolbar(); 35 | @include vertical-toolbar(); 36 | } 37 | 38 | .toolbar-bottom { 39 | // @include toolbar(); 40 | @include horizontal-toolbar(); 41 | } 42 | -------------------------------------------------------------------------------- /sketchem-react/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "problemMatcher": [], 14 | "label": "npm: build", 15 | "detail": "npm run prettier:write && craco build" 16 | }, 17 | { 18 | "type": "npm", 19 | "script": "build", 20 | "group": { 21 | "kind": "build", 22 | "isDefault": true 23 | }, 24 | "problemMatcher": [], 25 | "label": "npm: debugchrome", 26 | "detail": "npm run debugchrome" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /sketchem-react/src/features/sketchpad/svgpanzoom.js.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | export enum MouseButton { 3 | left = 0, 4 | middle, 5 | right, 6 | back, 7 | forth, 8 | } 9 | 10 | interface marginOptions { 11 | left: number; 12 | top: number; 13 | right: number; 14 | bottom: number; 15 | } 16 | 17 | interface options { 18 | panning?: boolean; 19 | pinchZoom?: boolean; 20 | touchpadFactor?: number; 21 | wheelZoom?: boolean; 22 | panButton?: MouseButton; 23 | oneFingerPan?: boolean; 24 | margins?: boolean | marginOptions; 25 | zoomFactor?: number; 26 | zoomMin?: number; 27 | zoomMax?: number; 28 | wheelZoomDeltaModeLinePixels?: number; 29 | wheelZoomDeltaModeScreenPixels?: number; 30 | } 31 | 32 | declare module "@svgdotjs/svg.js" { 33 | interface Svg { 34 | panZoom(options?: options | false): this; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/left-toolbar-item/ToolbarItem.ts: -------------------------------------------------------------------------------- 1 | import { Direction } from "@constants/enum.constants"; 2 | 3 | import { ToolbarItemButton } from "../ToolbarItem"; 4 | import type { IToolbarItemsProps } from "../ToolbarItems"; 5 | import { BondTool, Chain, Erase } from "../tools"; 6 | import DebugTools from "../tools/debug_tools"; 7 | 8 | let currentDebugTools = DebugTools; 9 | 10 | if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") { 11 | // dev code 12 | } else { 13 | currentDebugTools = []; 14 | } 15 | 16 | const toolbarItemsList: ToolbarItemButton[] = [ 17 | Erase, 18 | BondTool.singleBond, 19 | BondTool.doubleBond, 20 | BondTool.tripleBond, 21 | BondTool.singleOrDoubleBond, 22 | BondTool.wedgeBackBond, 23 | BondTool.wedgeFrontBond, 24 | Chain, 25 | ...currentDebugTools, 26 | ]; 27 | 28 | const props: IToolbarItemsProps = { 29 | toolbarItemsList, 30 | direction: Direction.Left, 31 | }; 32 | 33 | export default props; 34 | -------------------------------------------------------------------------------- /sketchem-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | 3 | import React from "react"; 4 | import ThemeProvider from "react-bootstrap/ThemeProvider"; 5 | import ReactDOM from "react-dom"; 6 | import { Provider } from "react-redux"; 7 | 8 | import App from "./App"; 9 | import { store } from "./app/store"; 10 | import * as serviceWorker from "./serviceWorker"; 11 | 12 | ReactDOM.render( 13 | 14 | 15 | {/* breakpoints={["xxxl", "xxl", "xl", "lg", "md", "sm", "xs", "xxs"]} */} 16 | {/* !!! only xl for now */} 17 | 18 | 19 | 20 | 21 | , 22 | document.getElementById("root") 23 | ); 24 | 25 | // If you want your app to work offline and load faster, you can change 26 | // unregister() to register() below. Note this comes with some pitfalls. 27 | // Learn more about service workers: https://bit.ly/CRA-PWA 28 | serviceWorker.unregister(); 29 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/App.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap"; 2 | 3 | 4 | .App { 5 | width: 100vw; 6 | height: 100vh; 7 | display: flex; 8 | justify-content: flex-start; 9 | // justify-content: center; 10 | // align-items: center; 11 | // text-align: center; 12 | } 13 | 14 | .App-logo { 15 | height: 40vmin; 16 | pointer-events: none; 17 | } 18 | 19 | @media (prefers-reduced-motion: no-preference) { 20 | .App-logo { 21 | animation: App-logo-float infinite 3s ease-in-out; 22 | } 23 | } 24 | 25 | .App-header { 26 | min-height: 100vh; 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | justify-content: center; 31 | font-size: calc(10px + 2vmin); 32 | } 33 | 34 | .App-link { 35 | color: rgb(112, 76, 182); 36 | } 37 | 38 | @keyframes App-logo-float { 39 | 0% { 40 | transform: translateY(0); 41 | } 42 | 50% { 43 | transform: translateY(10px); 44 | } 45 | 100% { 46 | transform: translateY(0px); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sketchem-react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | SketChem 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /sketchem-react/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sketchem-react/src/app/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from "@types"; 2 | 3 | export const getToolbarFrequentAtoms = (state: RootState) => state.toolbarItem.frequentAtoms; 4 | export const getToolbarItemContext = (state: RootState) => state.toolbarItem.toolbarContext; 5 | export const getToolbarDialog = (state: RootState) => state.toolbarItem.dialogWindow; 6 | export const getFileContent = (state: RootState) => state.toolbarItem.importContext; 7 | export const getChemistryDataPresent = (state: RootState) => state.chemistry.present; 8 | export const getChemistryDataIndex = (state: RootState) => state.chemistry.index; 9 | export const isChemistryUndoEnabled = (state: RootState) => state.chemistry.past.length > 0; 10 | export const isChemistryRedoEnabled = (state: RootState) => state.chemistry.future.length > 0; 11 | export const ChemistryFutureLength = (state: RootState) => state.chemistry.future.length; 12 | // export const isChemistryStateOlder = (state: RootState) => { 13 | // if (state.chemistry.future.length > 0) { 14 | // return state.chemistry.present; 15 | // } 16 | // return undefined; 17 | // }; 18 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ItayYaakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/periodic-table/ElementCell.tsx: -------------------------------------------------------------------------------- 1 | import type { PtElement } from "@constants/elements.constants"; 2 | import styles from "@styles/index.module.scss"; 3 | import clsx from "clsx"; 4 | import React from "react"; 5 | import { Button } from "react-bootstrap"; 6 | 7 | interface IElementProps { 8 | element: PtElement; 9 | onClick: (number: number) => void; 10 | } 11 | 12 | export function ElementCell(props: IElementProps) { 13 | const { element, onClick } = props; 14 | const btnClass = element.category.includes("unknown") 15 | ? styles.periodic_table_category_unknown 16 | : styles[`periodic_table_category_${element.category.replaceAll(" ", "_")}`]; 17 | 18 | return ( 19 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /sketchem-react/src/app/resizeHook.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useWindowSize() { 4 | // Initialize state with undefined width/height so server and client renders match 5 | // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ 6 | const [windowSize, setWindowSize] = useState({ 7 | width: 0, 8 | height: 0, 9 | }); 10 | useEffect(() => { 11 | // Handler to call on window resize 12 | function handleResize() { 13 | // Set window width/height to state 14 | setWindowSize({ 15 | width: window.innerWidth, 16 | height: window.innerHeight, 17 | }); 18 | } 19 | // Add event listener 20 | window.addEventListener("resize", handleResize); 21 | // Call handler right away so state gets updated with initial window size 22 | handleResize(); 23 | // Remove event listener on cleanup 24 | return () => window.removeEventListener("resize", handleResize); 25 | }, []); // Empty array ensures that effect is only run on mount 26 | return windowSize; 27 | } 28 | -------------------------------------------------------------------------------- /sketchem-react/src/utils/LayersUtils.ts: -------------------------------------------------------------------------------- 1 | import { LayersNames } from "@constants/enum.constants"; 2 | import { IdUtils } from "@src/utils/IdUtils"; 3 | import { G, Svg } from "@svgdotjs/svg.js"; 4 | 5 | /* 6 | Each layer will hold different types of objects, and the order will be as follow: 7 | (When the each layer hide layers above her) 8 | 9 | bond 10 | atom_label_background 11 | atom_label_hover 12 | atom_label_text 13 | 14 | */ 15 | 16 | const mLayersMap = new Map(); 17 | 18 | function setLayers(canvas: Svg) { 19 | Object.values(LayersNames).forEach((layerName) => { 20 | let group; 21 | if (layerName === LayersNames.Root) { 22 | group = canvas.id(IdUtils.getLayerElemId(layerName)); 23 | } else { 24 | group = canvas.group().id(IdUtils.getLayerElemId(layerName)); 25 | } 26 | mLayersMap.set(layerName, group); 27 | }); 28 | } 29 | 30 | function getLayer(layerName: LayersNames) { 31 | const result = mLayersMap.get(layerName); 32 | if (!result) { 33 | throw new Error(`Layer ${layerName} was not found`); 34 | } 35 | return result; 36 | } 37 | 38 | export const LayersUtils = { 39 | setLayers, 40 | getLayer, 41 | }; 42 | -------------------------------------------------------------------------------- /sketchem-react/src/buttons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button from "react-bootstrap/Button"; 3 | 4 | export default function ButtonsShowcase() { 5 | return ( 6 |
7 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 31 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/DialogShow.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-pascal-case */ 2 | import { useAppDispatch } from "@app/hooks"; 3 | import { getToolbarDialog } from "@app/selectors"; 4 | import * as ToolsConstants from "@constants/tools.constants"; 5 | import { EditorHandler } from "@features/editor/EditorHandler"; 6 | import React from "react"; 7 | import { shallowEqual, useSelector } from "react-redux"; 8 | 9 | import { isDialogToolbarItem } from "./ToolbarItem"; 10 | import { GetToolbarByName } from "./tools/ToolsMapper.helper"; 11 | import { SentDispatchEventWhenToolbarItemIsChanges } from "./ToolsButtonMapper.helper"; 12 | 13 | export function DialogShow(myProps: { editorHandler: EditorHandler }) { 14 | const dispatch = useAppDispatch(); 15 | const { editorHandler: editor } = myProps; 16 | const dialogWindowName = useSelector(getToolbarDialog, shallowEqual); 17 | const props = { 18 | onHide: () => SentDispatchEventWhenToolbarItemIsChanges(dispatch, ToolsConstants.ToolsNames.SelectBox), 19 | editor, 20 | }; 21 | 22 | const Tool = GetToolbarByName(dialogWindowName); 23 | if (Tool && isDialogToolbarItem(Tool)) { 24 | return ; 25 | } 26 | return null; 27 | } 28 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/_atoms_labels.scss: -------------------------------------------------------------------------------- 1 | @use "material_colors"as c; 2 | 3 | @mixin text { 4 | font-family: 'Roboto', sans-serif; 5 | font-weight: 700; 6 | // make unselectable 7 | -webkit-touch-callout: none; 8 | -webkit-user-select: none; 9 | -khtml-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | paint-order: stroke; 14 | // line-height: 1px !important; 15 | 16 | } 17 | 18 | .atom_label_text { 19 | @include text; 20 | // stroke: #00000094; 21 | // stroke-width: 0.05em; 22 | // stroke-linecap: round; 23 | // stroke-linejoin: round; 24 | } 25 | 26 | .atom_label_hydrogen_text { 27 | @include text; 28 | font-size: smaller; 29 | // alignment-baseline: central; 30 | baseline-shift: sub; 31 | } 32 | 33 | .atom_label_charge_text { 34 | @include text; 35 | font-size: smaller; 36 | baseline-shift: super; 37 | // alignment-baseline: central; 38 | } 39 | 40 | .atom_label_valence_error_line{ 41 | stroke: c.$pink-500; 42 | stroke-width: 3px; 43 | stroke-linecap: round; 44 | stroke-linejoin: round; 45 | } 46 | 47 | .atom_label_valence_error_rect{ 48 | stroke: c.$pink-500; 49 | stroke-width: 3px; 50 | stroke-linecap: round; 51 | stroke-linejoin: round; 52 | } -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/debug.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sketchem-react/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Python: Current File", 5 | "type": "python", 6 | "request": "launch", 7 | "program": "${file}", 8 | "console": "integratedTerminal", 9 | "justMyCode": false 10 | }, 11 | { 12 | "name": "Attach to Chrome", 13 | "port": 9222, 14 | "request": "attach", 15 | // "type": "pwa-chrome", 16 | "type": "chrome", 17 | "urlFilter": "http://localhost:3000/*", // use urlFilter instead of url! 18 | "webRoot": "${workspaceFolder}" 19 | }, 20 | { 21 | "name": "Run Server and Attach to Chrome", 22 | "port": 9222, 23 | "request": "attach", 24 | // "type": "pwa-chrome", 25 | "type": "chrome", 26 | "urlFilter": "http://localhost:3000/*", // use urlFilter instead of url! 27 | "webRoot": "${workspaceFolder}", 28 | "preLaunchTask": "yarn: debugchrome" 29 | }, 30 | { 31 | "type": "pwa-chrome", 32 | "name": "debug chrome", 33 | "request": "launch", 34 | "urlFilter": "http://localhost:3000/*", 35 | "url": "http://localhost:3000/" 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/Copy.ts: -------------------------------------------------------------------------------- 1 | import { store } from "@app/store"; 2 | import * as ToolsConstants from "@constants/tools.constants"; 3 | 4 | import { ActiveToolbarItem, LaunchAttrs, SimpleToolbarItemButtonBuilder } from "../ToolbarItem"; 5 | import { actions } from "../toolbarItemsSlice"; 6 | import { RegisterToolbarButtonWithName } from "../ToolsButtonMapper.helper"; 7 | import { RegisterToolbarWithName } from "./ToolsMapper.helper"; 8 | 9 | class Copy implements ActiveToolbarItem { 10 | onActivate(attrs?: LaunchAttrs) { 11 | if (!attrs) return; 12 | const { editor } = attrs; 13 | if (!editor) { 14 | throw new Error("Copy.onActivate: missing attributes or editor"); 15 | } 16 | 17 | editor.updateCopiedContents(); 18 | editor.setHoverMode(false, true, true); 19 | editor.resetSelectedAtoms(); 20 | editor.resetSelectedBonds(); 21 | store.dispatch(actions.asyncDispatchSelect()); 22 | } 23 | } 24 | 25 | const copyTool = new Copy(); 26 | RegisterToolbarWithName(ToolsConstants.ToolsNames.Copy, copyTool); 27 | 28 | const copy = new SimpleToolbarItemButtonBuilder( 29 | "Copy", 30 | ToolsConstants.ToolsNames.Copy, 31 | ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.ToolsNames.Copy) 32 | ); 33 | 34 | RegisterToolbarButtonWithName(copy); 35 | 36 | export default copy; 37 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/ClearCanvas.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { store } from "@app/store"; 3 | import * as ToolsConstants from "@constants/tools.constants"; 4 | 5 | import { ActiveToolbarItem, LaunchAttrs, SimpleToolbarItemButtonBuilder } from "../ToolbarItem"; 6 | import { actions } from "../toolbarItemsSlice"; 7 | import { RegisterToolbarButtonWithName } from "../ToolsButtonMapper.helper"; 8 | import { RegisterToolbarWithName } from "./ToolsMapper.helper"; 9 | 10 | class ClearCanvas implements ActiveToolbarItem { 11 | onActivate(attrs?: LaunchAttrs) { 12 | if (!attrs) return; 13 | const { editor } = attrs; 14 | if (!editor) { 15 | throw new Error("ClearCanvas.onActivate: missing attributes or editor"); 16 | } 17 | const changed = editor.clear(); 18 | if (changed) editor.createHistoryUpdate(); 19 | 20 | store.dispatch(actions.asyncDispatchSelect()); 21 | } 22 | } 23 | 24 | const clearCanvasTool = new ClearCanvas(); 25 | 26 | RegisterToolbarWithName(ToolsConstants.ToolsNames.Clear, clearCanvasTool); 27 | 28 | const clearCanvas = new SimpleToolbarItemButtonBuilder( 29 | "Clear Canvas", 30 | ToolsConstants.ToolsNames.Clear, 31 | ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.ToolsNames.Clear) 32 | ); 33 | 34 | RegisterToolbarButtonWithName(clearCanvas); 35 | 36 | export default clearCanvas; 37 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/debug_tools/printMolToConsole.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import * as ToolsConstants from "@constants/tools.constants"; 4 | import { RegisterToolbarButtonWithName } from "@features/toolbar-item/ToolsButtonMapper.helper"; 5 | import { exportFileFromMolecule } from "@src/utils/KekuleUtils"; 6 | 7 | import { ActiveToolbarItem, LaunchAttrs, SimpleToolbarItemButtonBuilder } from "../../ToolbarItem"; 8 | import { RegisterToolbarWithName } from "../ToolsMapper.helper"; 9 | 10 | class ExportMolToConsole implements ActiveToolbarItem { 11 | onActivate(attrs?: LaunchAttrs) { 12 | if (!attrs) return; 13 | const { editor } = attrs; 14 | if (!editor) { 15 | throw new Error("Paste.onActivate: missing attributes or editor"); 16 | } 17 | editor.updateAllKekuleNodes(); 18 | const content = exportFileFromMolecule("mdl"); 19 | console.log(content); 20 | } 21 | } 22 | 23 | const exportMolToConsole = new ExportMolToConsole(); 24 | 25 | RegisterToolbarWithName(ToolsConstants.ToolsNames.DebugExportMolToConsole, exportMolToConsole); 26 | 27 | const ExportMolToConsoleTool = new SimpleToolbarItemButtonBuilder( 28 | "export mol to console (debug) ", 29 | ToolsConstants.ToolsNames.DebugExportMolToConsole 30 | ); 31 | 32 | RegisterToolbarButtonWithName(ExportMolToConsoleTool); 33 | 34 | export { ExportMolToConsoleTool }; 35 | -------------------------------------------------------------------------------- /sketchem-react/src/features/chemistry/chemistrySlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, current, PayloadAction } from "@reduxjs/toolkit"; 2 | import { ChemistryState } from "@types"; 3 | import hash from "object-hash"; 4 | import undoable from "redux-undo"; 5 | 6 | const initialState = {} as any as ChemistryState; 7 | 8 | function shouldUpdate(oldState: ChemistryState, currentState: ChemistryState) { 9 | if (oldState?.atoms?.length !== currentState?.atoms?.length) return true; 10 | if (oldState?.bonds?.length !== currentState?.bonds?.length) return true; 11 | return hash(current(oldState)) !== hash(currentState); 12 | } 13 | 14 | const slice = createSlice({ 15 | name: "chemistry", 16 | initialState, 17 | reducers: { 18 | update_history_state: (state: ChemistryState, action: PayloadAction) => { 19 | if (!shouldUpdate(state, action.payload)) return; 20 | console.log("update_history_state"); 21 | state.atoms = action.payload.atoms; 22 | state.bonds = action.payload.bonds; 23 | }, 24 | }, 25 | }); 26 | 27 | export const actions = { ...slice.actions }; 28 | export default undoable(slice.reducer, { 29 | limit: 20, 30 | // undoType: "CHEMISTRY_UNDO", // define a custom action type for this undo action 31 | // redoType: "CHEMISTRY_REDO", // define a custom action type for this redo action 32 | // clearHistoryType: "CLEAR_CHEMISTRY_HISTORY", // [beta only] define custom action type for this clearHistory action 33 | }); 34 | -------------------------------------------------------------------------------- /sketchem-react/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#7ed1e2", 4 | "activityBar.activeBorder": "#d649be", 5 | "activityBar.background": "#7ed1e2", 6 | "activityBar.foreground": "#15202b", 7 | "activityBar.inactiveForeground": "#15202b99", 8 | "activityBarBadge.background": "#d649be", 9 | "activityBarBadge.foreground": "#15202b", 10 | "sash.hoverBorder": "#7ed1e2", 11 | "statusBar.background": "#54c2d9", 12 | "statusBar.foreground": "#15202b", 13 | "statusBarItem.hoverBackground": "#2db1cc", 14 | "statusBarItem.remoteBackground": "#54c2d9", 15 | "statusBarItem.remoteForeground": "#15202b", 16 | "titleBar.activeBackground": "#54c2d9", 17 | "titleBar.activeForeground": "#15202b", 18 | "titleBar.inactiveBackground": "#54c2d999", 19 | "titleBar.inactiveForeground": "#15202b99" 20 | }, 21 | "peacock.color": "#54c2d9", 22 | "cSpell.words": [ 23 | "browserslist", 24 | "dmove", 25 | "kekule", 26 | "sketchem", 27 | "undoable" 28 | ], 29 | "files.exclude": { 30 | "**/*.min.js": true, 31 | "build/": true, 32 | "node_modules/": true, 33 | "public/": true 34 | }, 35 | "editor.codeActionsOnSave": { 36 | "source.fixAll.eslint": true 37 | }, 38 | "eslint.validate": ["javascript", "typescript"], 39 | "explorer.fileNesting.enabled": true 40 | } 41 | -------------------------------------------------------------------------------- /sketchem-react/craco.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | // const CopyPlugin = require("copy-webpack-plugin"); 3 | // const webpack = require("webpack"); // for development only? 4 | 5 | module.exports = { 6 | webpack: { 7 | alias: { 8 | "@app": path.resolve(__dirname, "src/app"), 9 | "@constants": path.resolve(__dirname, "src/constants"), 10 | "@entities": path.resolve(__dirname, "src/entities/index.ts"), 11 | "@features": path.resolve(__dirname, "src/features"), 12 | "@src": path.resolve(__dirname, "src"), 13 | "@styles": path.resolve(__dirname, "src/styles"), 14 | "@types": path.resolve(__dirname, "src/types/index.ts"), 15 | "@utils": path.resolve(__dirname, "src/utils"), 16 | }, 17 | // rules: [{ test: /\.wasm$/, type: "asset/inline" }], 18 | configure: { 19 | resolve: { 20 | fallback: { 21 | fs: false, 22 | path: require.resolve("path-browserify"), 23 | }, 24 | }, 25 | }, 26 | // plugins: [ 27 | // // Copy kekule extra folder 28 | // new CopyPlugin({ 29 | // patterns: [ 30 | // // { from: path.resolve(__dirname, "src", "utils", "kekule-js-dist", "extra"), to: "static/js/extra" }, 31 | // { from: path.resolve(__dirname, "src", "utils", "kekule-js-dist"), to: "static/js/kekule-js-dist" }, 32 | // ], 33 | // }), 34 | // ], 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /sketchem-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/style-prop-object */ 2 | import "@styles/App.scss"; 3 | 4 | import Editor from "@features/editor/Editor"; 5 | import { EditorHandler } from "@features/editor/EditorHandler"; 6 | import { 7 | BottomToolbarProps, 8 | DialogShow, 9 | LeftToolbarProps, 10 | RightToolbarProps, 11 | ToolbarItems, 12 | TopToolbarProps, 13 | } from "@features/toolbar-item"; 14 | import styles from "@styles/index.module.scss"; 15 | import clsx from "clsx"; 16 | import React from "react"; 17 | 18 | const editorHandler = new EditorHandler(); 19 | 20 | function App() { 21 | // if (loading) { 22 | // return ; 23 | // } 24 | 25 | return ( 26 |
27 |
28 | 29 |
30 | 31 |
32 | 33 |
34 | 35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 | 43 |
44 | 45 | {/* SVG sketchpad is placed here */} 46 |
47 | 48 | 49 |
50 | ); 51 | } 52 | 53 | export default App; 54 | -------------------------------------------------------------------------------- /sketchem-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "sourceMap": true, 19 | "baseUrl": "./", 20 | "paths": { 21 | "@app/*": ["./src/app/*"], 22 | "@constants/*": ["./src/constants/*"], 23 | "@entities": ["./src/entities"], 24 | "@features/*": ["./src/features/*"], 25 | "@src/*": ["./src/*"], 26 | "@styles/*": ["./src/styles/*"], 27 | "@types": ["./src/types"], 28 | "@utils/*": ["./src/utils/*"] 29 | } 30 | }, 31 | "include": [ 32 | "./src/**/*.ts", 33 | "./src/**/*.tsx", 34 | ".eslintrc.js", 35 | "prettier.config.js", 36 | "craco.config.js", 37 | "src/utils/KekuleUtils.js" 38 | ], 39 | "exclude": [ 40 | "./node_modules/", 41 | "./public/kekule-js-dist/*", 42 | ".vscode", 43 | "src/features/counter/", 44 | "./src/features/counter/*", 45 | "./src/features/to/", 46 | "./src/_actions/user.action.ts" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /sketchem-react/src/features/counter/Counter.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .row > button { 8 | margin-left: 4px; 9 | margin-right: 8px; 10 | } 11 | 12 | .row:not(:last-child) { 13 | margin-bottom: 16px; 14 | } 15 | 16 | .value { 17 | font-size: 78px; 18 | padding-left: 16px; 19 | padding-right: 16px; 20 | margin-top: 2px; 21 | font-family: 'Courier New', Courier, monospace; 22 | } 23 | 24 | .button { 25 | appearance: none; 26 | background: none; 27 | font-size: 32px; 28 | padding-left: 12px; 29 | padding-right: 12px; 30 | outline: none; 31 | border: 2px solid transparent; 32 | color: rgb(112, 76, 182); 33 | padding-bottom: 4px; 34 | cursor: pointer; 35 | background-color: rgba(112, 76, 182, 0.1); 36 | border-radius: 2px; 37 | transition: all 0.15s; 38 | } 39 | 40 | .textbox { 41 | font-size: 32px; 42 | padding: 2px; 43 | width: 64px; 44 | text-align: center; 45 | margin-right: 4px; 46 | } 47 | 48 | .button:hover, 49 | .button:focus { 50 | border: 2px solid rgba(112, 76, 182, 0.4); 51 | } 52 | 53 | .button:active { 54 | background-color: rgba(112, 76, 182, 0.2); 55 | } 56 | 57 | .asyncButton { 58 | composes: button; 59 | position: relative; 60 | } 61 | 62 | .asyncButton:after { 63 | content: ''; 64 | background-color: rgba(112, 76, 182, 0.15); 65 | display: block; 66 | position: absolute; 67 | width: 100%; 68 | height: 100%; 69 | left: 0; 70 | top: 0; 71 | opacity: 0; 72 | transition: width 1s linear, opacity 0.5s ease 1s; 73 | } 74 | 75 | .asyncButton:active:after { 76 | width: 0%; 77 | opacity: 1; 78 | transition: 0s; 79 | } 80 | -------------------------------------------------------------------------------- /sketchem-react/src/features/editor/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { useAppDispatch } from "@app/hooks"; 2 | import { ChemistryFutureLength, getChemistryDataPresent } from "@app/selectors"; 3 | // import { getChemistryDataIndex, isChemistryRedoEnabled, isChemistryStateOlder } from "@app/selectors"; 4 | import SketchPad from "@features/sketchpad/SketchPad"; 5 | import React, { useEffect, useState } from "react"; 6 | import { useSelector } from "react-redux"; 7 | 8 | import { EditorHandler } from "./EditorHandler"; 9 | 10 | function Editor(props: { editorHandler: EditorHandler }) { 11 | const dispatch = useAppDispatch(); 12 | 13 | const { editorHandler } = props; 14 | 15 | const futureLength = useSelector(ChemistryFutureLength); 16 | 17 | const [previousFutureLength, setPreviousFutureLength] = useState(0); 18 | // !!! find a better way 19 | const presentState = useSelector(getChemistryDataPresent); 20 | 21 | useEffect(() => { 22 | if (futureLength > previousFutureLength) { 23 | setPreviousFutureLength(futureLength); 24 | console.log("its a undo"); 25 | } else if (futureLength < previousFutureLength) { 26 | setPreviousFutureLength(futureLength); 27 | console.log("its a redo"); 28 | } else { 29 | console.log("its synced"); 30 | return; 31 | // No need to do anything here. Component state in sync with redux 32 | } 33 | editorHandler.editAtomsAndBondsBasedOnStateObject(presentState); 34 | }, [futureLength]); 35 | 36 | editorHandler.setDispatch(dispatch); 37 | 38 | return ; 39 | } 40 | 41 | export default Editor; 42 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/Counter.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .row > button { 8 | margin-left: 4px; 9 | margin-right: 8px; 10 | } 11 | 12 | .row:not(:last-child) { 13 | margin-bottom: 16px; 14 | } 15 | 16 | .value { 17 | font-size: 78px; 18 | padding-left: 16px; 19 | padding-right: 16px; 20 | margin-top: 2px; 21 | font-family: 'Courier New', Courier, monospace; 22 | } 23 | 24 | .button { 25 | appearance: none; 26 | background: none; 27 | font-size: 32px; 28 | padding-left: 12px; 29 | padding-right: 12px; 30 | outline: none; 31 | border: 2px solid transparent; 32 | color: rgb(112, 76, 182); 33 | padding-bottom: 4px; 34 | cursor: pointer; 35 | background-color: rgba(112, 76, 182, 0.1); 36 | border-radius: 2px; 37 | transition: all 0.15s; 38 | } 39 | 40 | .textbox { 41 | font-size: 32px; 42 | padding: 2px; 43 | width: 64px; 44 | text-align: center; 45 | margin-right: 4px; 46 | } 47 | 48 | .button:hover, 49 | .button:focus { 50 | border: 2px solid rgba(112, 76, 182, 0.4); 51 | } 52 | 53 | .button:active { 54 | background-color: rgba(112, 76, 182, 0.2); 55 | } 56 | 57 | .asyncButton { 58 | composes: button; 59 | position: relative; 60 | } 61 | 62 | .asyncButton:after { 63 | content: ''; 64 | background-color: rgba(112, 76, 182, 0.15); 65 | display: block; 66 | position: absolute; 67 | width: 100%; 68 | height: 100%; 69 | left: 0; 70 | top: 0; 71 | opacity: 0; 72 | transition: width 1s linear, opacity 0.5s ease 1s; 73 | } 74 | 75 | .asyncButton:active:after { 76 | width: 0%; 77 | opacity: 1; 78 | transition: 0s; 79 | } 80 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/ToolsButtonMapper.helper.ts: -------------------------------------------------------------------------------- 1 | import { ToolbarAction } from "@src/types"; 2 | 3 | import { isDialogToolbarItem, ToolbarItemButton } from "./ToolbarItem"; 4 | import { actions } from "./toolbarItemsSlice"; 5 | import { GetToolbarByName } from "./tools/ToolsMapper.helper"; 6 | 7 | const toolbarItemsButtonsDict = new Map(); 8 | export function RegisterToolbarButtonWithName(tool: ToolbarItemButton) { 9 | const name = tool.subToolName ?? tool.toolName; 10 | if (toolbarItemsButtonsDict.has(name)) { 11 | throw new Error(`Toolbar button with name ${name} already registered`); 12 | } 13 | toolbarItemsButtonsDict.set(name, tool); 14 | } 15 | 16 | export function GetToolbarButtonByName(name: string): ToolbarItemButton | undefined { 17 | if (!name) return undefined; 18 | const item = toolbarItemsButtonsDict.get(name); 19 | if (!item) { 20 | throw new Error(`Toolbar Button with name ${name} not registered`); 21 | } 22 | return item; 23 | } 24 | 25 | export function IsToolbarButtonExists(tool: ToolbarItemButton): boolean { 26 | const name = tool.subToolName ?? tool.toolName; 27 | return toolbarItemsButtonsDict.has(name); 28 | } 29 | 30 | export function SentDispatchEventWhenToolbarItemIsChanges(dispatch: any, name: string) { 31 | const toolButton = GetToolbarButtonByName(name); 32 | if (!toolButton) return; 33 | const tool = GetToolbarByName(toolButton.toolName); 34 | if (!tool) return; 35 | 36 | if (isDialogToolbarItem(tool)) { 37 | dispatch(actions.dialog(toolButton.toolName)); 38 | } else { 39 | const payload: ToolbarAction = { 40 | toolName: toolButton.toolName, 41 | }; 42 | const { attributes, subToolName } = toolButton; 43 | if (attributes) payload.attributes = attributes; 44 | if (subToolName) payload.subToolName = subToolName; 45 | 46 | dispatch(actions.asyncDispatchTool(payload)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sketchem-react/src/features/counter/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { useAppSelector, useAppDispatch } from "@app/hooks"; 4 | import { decrement, increment, incrementByAmount, incrementAsync, incrementIfOdd, selectCount } from "./counterSlice"; 5 | import styles from "./Counter.module.css"; 6 | 7 | export function Counter() { 8 | const count = useAppSelector(selectCount); 9 | const dispatch = useAppDispatch(); 10 | const [incrementAmount, setIncrementAmount] = useState("2"); 11 | 12 | const incrementValue = Number(incrementAmount) || 0; 13 | 14 | return ( 15 |
16 |
17 | 20 | {count} 21 | 24 |
25 |
26 | setIncrementAmount(e.target.value)} 31 | /> 32 | 35 | 38 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/ToolbarItem.ts: -------------------------------------------------------------------------------- 1 | import * as ToolsConstants from "@constants/tools.constants"; 2 | import { EditorHandler } from "@features/editor/EditorHandler"; 3 | import { MouseEventCallBackProperties, ToolbarAction, ToolbarItemButtonAttributes } from "@types"; 4 | 5 | export interface LaunchAttrs { 6 | toolAttributes?: ToolbarItemButtonAttributes; 7 | editor?: EditorHandler; 8 | previousToolContext?: ToolbarAction; 9 | } 10 | 11 | export interface DialogProps { 12 | editor: EditorHandler; 13 | onHide: () => void; 14 | } 15 | export interface ActiveToolbarItem { 16 | readonly onActivate?: (props?: LaunchAttrs) => void; 17 | 18 | readonly onMouseDown?: (e: MouseEventCallBackProperties) => void; 19 | 20 | readonly onMouseMove?: (e: MouseEventCallBackProperties) => void; 21 | 22 | readonly onMouseUp?: (e: MouseEventCallBackProperties) => void; 23 | 24 | readonly onMouseClick?: (e: MouseEventCallBackProperties) => void; 25 | 26 | readonly onMouseLeave?: (e: MouseEventCallBackProperties) => void; 27 | 28 | readonly onDeactivate?: () => void; 29 | } 30 | 31 | export interface DialogToolbarItem { 32 | readonly DialogRender: (props: DialogProps) => JSX.Element; 33 | } 34 | 35 | export type ToolbarItem = ActiveToolbarItem | DialogToolbarItem; 36 | export interface SimpleToolbarItemButton { 37 | name: string; 38 | 39 | toolName: ToolsConstants.ToolsNames; 40 | 41 | subToolName?: ToolsConstants.SubToolsNames | string; 42 | 43 | keyboardKeys?: string; 44 | } 45 | 46 | export interface ToolbarItemButton extends SimpleToolbarItemButton { 47 | attributes?: ToolbarItemButtonAttributes; 48 | } 49 | 50 | export class SimpleToolbarItemButtonBuilder implements SimpleToolbarItemButton { 51 | name: string; 52 | 53 | toolName: ToolsConstants.ToolsNames; 54 | 55 | keyboardKeys?: string; 56 | 57 | constructor(name: string, toolName: ToolsConstants.ToolsNames, keyboardKeys?: string) { 58 | this.name = name; 59 | this.toolName = toolName; 60 | this.keyboardKeys = keyboardKeys; 61 | } 62 | } 63 | 64 | export const isDialogToolbarItem = (o: ToolbarItem): o is DialogToolbarItem => 65 | (o as DialogToolbarItem).DialogRender !== undefined; 66 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/debug_tools/drawAllPeriodicTable.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import { ElementsData } from "@constants/elements.constants"; 4 | import * as ToolsConstants from "@constants/tools.constants"; 5 | import { Atom } from "@entities"; 6 | import { RegisterToolbarButtonWithName } from "@features/toolbar-item/ToolsButtonMapper.helper"; 7 | import { IAtom } from "@src/types"; 8 | import Vector2 from "@src/utils/mathsTs/Vector2"; 9 | 10 | import { ActiveToolbarItem, SimpleToolbarItemButtonBuilder } from "../../ToolbarItem"; 11 | import { RegisterToolbarWithName } from "../ToolsMapper.helper"; 12 | 13 | class DrawAllPeriodicTable implements ActiveToolbarItem { 14 | onActivate() { 15 | // const { editor } = attrs; 16 | // if (!editor) { 17 | // throw new Error("SelectTemplate.onActivate: missing attributes or editor"); 18 | // } 19 | const padding = 80; 20 | const startX = 1000; 21 | const atomCenter = new Vector2(startX, 500); 22 | // for (let index = 1; index < 119; index += 1) { 23 | for (let index = 1; index < 112; index += 1) { 24 | const elem = ElementsData.elementsByAtomicNumberMap.get(index); 25 | if (!elem) throw new Error(`Element with atomic number ${index} not found`); 26 | if ((index - 1) % 14 === 0) { 27 | atomCenter.x = startX - padding; 28 | atomCenter.x = startX; 29 | atomCenter.y += padding; 30 | } 31 | 32 | atomCenter.addValuesSelf(padding, 0); 33 | 34 | const atom = new Atom({ props: { symbol: elem.symbol, center: atomCenter.get() } } as IAtom); 35 | atom.execOuterDrawCommand(); 36 | } 37 | } 38 | } 39 | 40 | const drawAllPeriodic = new DrawAllPeriodicTable(); 41 | 42 | RegisterToolbarWithName(ToolsConstants.ToolsNames.DebugDrawAllPeriodic, drawAllPeriodic); 43 | 44 | const DrawAllPeriodic = new SimpleToolbarItemButtonBuilder( 45 | "draw all periodic (debug) ", 46 | ToolsConstants.ToolsNames.DebugDrawAllPeriodic 47 | ); 48 | 49 | RegisterToolbarButtonWithName(DrawAllPeriodic); 50 | 51 | export { DrawAllPeriodic }; 52 | -------------------------------------------------------------------------------- /sketchem-react/README.md.bak: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | sketchem-react/local/** 106 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/Erase.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import * as ToolsConstants from "@constants/tools.constants"; 3 | import type { Atom, Bond } from "@entities"; 4 | import { EditorHandler } from "@features/editor/EditorHandler"; 5 | import { MouseEventCallBackProperties } from "@src/types"; 6 | 7 | import { LaunchAttrs, SimpleToolbarItemButtonBuilder } from "../ToolbarItem"; 8 | import { RegisterToolbarButtonWithName } from "../ToolsButtonMapper.helper"; 9 | import { BoxSelect } from "./SelectTemplate"; 10 | import { RegisterToolbarWithName } from "./ToolsMapper.helper"; 11 | 12 | class EraseBox extends BoxSelect { 13 | selectColor: string = "#ff9a9a"; 14 | 15 | shapeFillColor: string = "#df5c83"; 16 | // delete all selectedAtoms 17 | 18 | onActivate(attrs?: LaunchAttrs) { 19 | if (!attrs) return; 20 | const { editor } = attrs; 21 | if (!editor) { 22 | throw new Error("EraseBox.onActivate: missing attributes or editor"); 23 | } 24 | this.doAction(editor); 25 | editor.setHoverMode(true, true, true, this.selectColor); 26 | } 27 | 28 | onMouseMove(eventHolder: MouseEventCallBackProperties): void { 29 | if (this.selectionMode === 1) return; 30 | super.onMouseMove(eventHolder); 31 | } 32 | 33 | doAction(editor: EditorHandler): void { 34 | let changed = 0; 35 | changed += editor.applyFunctionToAtoms((atom: Atom) => { 36 | atom.destroy(); 37 | }, true); 38 | changed += editor.applyFunctionToBonds((bond: Bond) => { 39 | bond.destroy(); 40 | }, true); 41 | editor.resetSelectedAtoms(); 42 | editor.resetSelectedBonds(); 43 | if (changed > 0) editor.createHistoryUpdate(); 44 | editor.setHoverMode(true, true, true, this.selectColor); 45 | } 46 | } 47 | 48 | const eraseBoxTool = new EraseBox(); 49 | RegisterToolbarWithName(ToolsConstants.ToolsNames.Erase, eraseBoxTool); 50 | 51 | const eraseBox = new SimpleToolbarItemButtonBuilder( 52 | "Erase Box", 53 | ToolsConstants.ToolsNames.Erase, 54 | ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.ToolsNames.Erase) 55 | ); 56 | 57 | RegisterToolbarButtonWithName(eraseBox); 58 | 59 | export default eraseBox; 60 | -------------------------------------------------------------------------------- /sketchem-react/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template. 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /sketchem-react/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "./static/css/main.fe47ac76.css", 4 | "main.js": "./static/js/main.6ed8c631.js", 5 | "index.html": "./index.html", 6 | "static/media/select_box.svg": "./static/media/select_box.608cfb9bdbe2f4fc38270adb7ae06404.svg", 7 | "static/media/bond_wedge_back.svg": "./static/media/bond_wedge_back.af72e68b5c3f59e445a44d70086f6860.svg", 8 | "static/media/select_lasso.svg": "./static/media/select_lasso.4e8ec7cd5ffa0fba5e959596b6f40b26.svg", 9 | "static/media/export.svg": "./static/media/export.6bd71e290f10bb280762ac603a908fa6.svg", 10 | "static/media/import.svg": "./static/media/import.2d111a848177ca6fac4c2f4f503a3647.svg", 11 | "static/media/clear_canvas.svg": "./static/media/clear_canvas.1963dbde7918fdfcb6d0510b44b0bd42.svg", 12 | "static/media/periodic_table.svg": "./static/media/periodic_table.badc0dda69fc8f1b8f4aae44672f2059.svg", 13 | "static/media/paste.svg": "./static/media/paste.900d40c5cdd12f305fd5e548cabc8bda.svg", 14 | "static/media/bond_triple.svg": "./static/media/bond_triple.21226e758e92bac5be3df87ee145593e.svg", 15 | "static/media/copy.svg": "./static/media/copy.fb49915b75a7ac7aea0c687c37acc450.svg", 16 | "static/media/erase.svg": "./static/media/erase.8f72d3fd948e4ddadd07a60fcb9938b5.svg", 17 | "static/media/bond_single_or_double.svg": "./static/media/bond_single_or_double.6331a4c08c60e6283e149fb6482fa136.svg", 18 | "static/media/charge_plus.svg": "./static/media/charge_plus.6c920f742a14e4b104adcd1ed51bf8fb.svg", 19 | "static/media/redo.svg": "./static/media/redo.fbb02dbce1316e1cc84385d13c33f3c6.svg", 20 | "static/media/undo.svg": "./static/media/undo.7194a4d7cf82725ae4ff24e815ca5838.svg", 21 | "static/media/bond_double.svg": "./static/media/bond_double.635d3740ba4ab18c0fce6299f21008d7.svg", 22 | "static/media/charge_minus.svg": "./static/media/charge_minus.0632ab36d6e27bcb12c7feb9686606af.svg", 23 | "static/media/chain.svg": "./static/media/chain.305046b7fa70f3c4a33f50c2ef4d5997.svg", 24 | "static/media/bond_single.svg": "./static/media/bond_single.dda061e50dc7e697738aadaf09389896.svg", 25 | "static/media/bond_wedge_front.svg": "./static/media/bond_wedge_front.ed67a630ca2fb54df1c73157360aca4d.svg", 26 | "main.fe47ac76.css.map": "./static/css/main.fe47ac76.css.map", 27 | "main.6ed8c631.js.map": "./static/js/main.6ed8c631.js.map" 28 | }, 29 | "entrypoints": [ 30 | "static/css/main.fe47ac76.css", 31 | "static/js/main.6ed8c631.js" 32 | ] 33 | } -------------------------------------------------------------------------------- /sketchem-react/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | }, 5 | extends: ["plugin:react/recommended", "airbnb", "airbnb-typescript", "prettier"], 6 | // extends: ["plugin:react/recommended"], 7 | parser: "@typescript-eslint/parser", 8 | parserOptions: { 9 | ecmaFeatures: { 10 | jsx: true, 11 | }, 12 | project: "./tsconfig.json", 13 | ecmaVersion: "latest", 14 | sourceType: "module", 15 | }, 16 | // !!! temp 17 | ignorePatterns: [ 18 | "build/*", 19 | "public/*", 20 | "local/*", 21 | "src/features/counter/*", 22 | "src/features/to/", 23 | "src/_actions/user.action.ts", 24 | ], 25 | plugins: ["react", "@typescript-eslint", "prettier", "simple-import-sort"], 26 | rules: { 27 | "simple-import-sort/imports": "error", 28 | "simple-import-sort/exports": "error", 29 | "arrow-parens": "off", 30 | "no-console": "off", 31 | "import/prefer-default-export": "off", 32 | "no-underscore-dangle": "off", 33 | "class-methods-use-this": "off", 34 | "no-param-reassign": ["error", { props: true, ignorePropertyModificationsFor: ["state"] }], 35 | // "array-element-newline": ["error", { multiline: true, minItems: 3 }], 36 | "react/jsx-filename-extension": ["warn", { extensions: [".js", ".jsx", ".ts", ".tsx"] }], 37 | "prettier/prettier": "error", 38 | "no-unused-vars": "warn", 39 | "@typescript-eslint/no-unused-vars": "warn", 40 | "react/no-unused-prop-types": "warn", 41 | "prefer-const": "warn", //! !! can be enable when deploying 42 | "react/jsx-props-no-spreading": "off", //! !! can be enable when deploying 43 | }, 44 | overrides: [ 45 | { 46 | files: ["**/index.ts"], 47 | rules: { 48 | "import/export": "off", 49 | }, 50 | }, 51 | ], 52 | settings: { 53 | react: { 54 | version: "detect", 55 | }, 56 | "import/parsers": { 57 | "@typescript-eslint/parser": [".ts", ".tsx"], 58 | }, 59 | "import/resolver": { 60 | typescript: { 61 | // always try to resolve types under `@types` directory even it doesn't contain any source code, like `@types/unist` 62 | alwaysTryTypes: true, 63 | // use /path/to/folder/tsconfig.json 64 | project: "./tsconfig.json", 65 | }, 66 | }, 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /sketchem-react/build/static/js/main.6ed8c631.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2018 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 14 | 15 | /** 16 | * @license 17 | * Lodash 18 | * Copyright OpenJS Foundation and other contributors 19 | * Released under MIT license 20 | * Based on Underscore.js 1.8.3 21 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 22 | */ 23 | 24 | /** @license React v0.20.2 25 | * scheduler.production.min.js 26 | * 27 | * Copyright (c) Facebook, Inc. and its affiliates. 28 | * 29 | * This source code is licensed under the MIT license found in the 30 | * LICENSE file in the root directory of this source tree. 31 | */ 32 | 33 | /** @license React v16.13.1 34 | * react-is.production.min.js 35 | * 36 | * Copyright (c) Facebook, Inc. and its affiliates. 37 | * 38 | * This source code is licensed under the MIT license found in the 39 | * LICENSE file in the root directory of this source tree. 40 | */ 41 | 42 | /** @license React v17.0.2 43 | * react-dom.production.min.js 44 | * 45 | * Copyright (c) Facebook, Inc. and its affiliates. 46 | * 47 | * This source code is licensed under the MIT license found in the 48 | * LICENSE file in the root directory of this source tree. 49 | */ 50 | 51 | /** @license React v17.0.2 52 | * react-is.production.min.js 53 | * 54 | * Copyright (c) Facebook, Inc. and its affiliates. 55 | * 56 | * This source code is licensed under the MIT license found in the 57 | * LICENSE file in the root directory of this source tree. 58 | */ 59 | 60 | /** @license React v17.0.2 61 | * react-jsx-runtime.production.min.js 62 | * 63 | * Copyright (c) Facebook, Inc. and its affiliates. 64 | * 65 | * This source code is licensed under the MIT license found in the 66 | * LICENSE file in the root directory of this source tree. 67 | */ 68 | 69 | /** @license React v17.0.2 70 | * react.production.min.js 71 | * 72 | * Copyright (c) Facebook, Inc. and its affiliates. 73 | * 74 | * This source code is licensed under the MIT license found in the 75 | * LICENSE file in the root directory of this source tree. 76 | */ 77 | 78 | /**! 79 | * hotkeys-js v3.9.4 80 | * A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies. 81 | * 82 | * Copyright (c) 2022 kenny wong 83 | * http://jaywcjlove.github.io/hotkeys 84 | * Licensed under the MIT license 85 | */ 86 | -------------------------------------------------------------------------------- /sketchem-react/src/features/shared/rbushKnn.ts: -------------------------------------------------------------------------------- 1 | import type RBush from "rbush"; 2 | import type { BBox } from "rbush"; 3 | import Queue from "tinyqueue"; 4 | 5 | function axisDist(k: number, min: number, max: number) { 6 | if (k < min) { 7 | return min - k; 8 | } 9 | if (k <= max) { 10 | return 0; 11 | } 12 | return k - max; 13 | } 14 | 15 | function boxDist(x: number, y: number, box: BBox) { 16 | const dx = axisDist(x, box.minX, box.maxX); 17 | const dy = axisDist(y, box.minY, box.maxY); 18 | return dx * dx + dy * dy; 19 | } 20 | 21 | // children: (3) [{…}, {…}, {…}] 22 | // height: 2 23 | // leaf: false 24 | // maxX: 716.5869432530515 25 | // maxY: 441.7202243483998 26 | // minX: -223.9130567469483 27 | // minY: 53.85318376773341 28 | 29 | interface IData { 30 | children: any; 31 | height: number; 32 | leaf: boolean; 33 | maxX: number; 34 | maxY: number; 35 | minX: number; 36 | minY: number; 37 | } 38 | 39 | export interface INode { 40 | node: any; 41 | isItem: boolean; 42 | dist: number; 43 | } 44 | 45 | function knn( 46 | tree: RBush, 47 | x: number, 48 | y: number, 49 | n?: number, 50 | maxDistance?: number, 51 | predicate?: (c: any) => boolean 52 | ) { 53 | function compareDist(a: INode, b: INode) { 54 | return a.dist - b.dist; 55 | } 56 | 57 | // @ts-ignore 58 | let node: IData | undefined = tree.data; 59 | const result: INode[] = []; 60 | const { toBBox } = tree; 61 | let i: number; 62 | let child: any; 63 | let dist: number; 64 | 65 | const queue = new Queue(undefined, compareDist); 66 | 67 | while (node) { 68 | for (i = 0; i < node.children.length; i += 1) { 69 | child = node.children[i]; 70 | dist = boxDist(x, y, node.leaf ? toBBox(child) : child); 71 | if (!maxDistance || dist <= maxDistance * maxDistance) { 72 | queue.push({ 73 | node: child, 74 | isItem: node.leaf, 75 | dist, 76 | } as INode); 77 | } 78 | } 79 | while (queue.length && queue.peek()!.isItem) { 80 | const candidate = queue.pop(); 81 | if (candidate && (!predicate || predicate(candidate.node))) { 82 | result.push(candidate); 83 | } 84 | if (n && result.length === n) { 85 | return result; 86 | } 87 | } 88 | 89 | const queueNode = queue.pop(); 90 | if (queueNode) { 91 | node = queueNode.node; 92 | } else { 93 | node = undefined; 94 | } 95 | } 96 | 97 | return result; 98 | } 99 | 100 | export default knn; 101 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/Unredo.ts: -------------------------------------------------------------------------------- 1 | import { store } from "@app/store"; 2 | import * as ToolsConstants from "@constants/tools.constants"; 3 | import { ToolbarAction } from "@src/types"; 4 | import { ActionCreators } from "redux-undo"; 5 | 6 | import { ActiveToolbarItem, LaunchAttrs, SimpleToolbarItemButtonBuilder } from "../ToolbarItem"; 7 | import { actions } from "../toolbarItemsSlice"; 8 | import { RegisterToolbarButtonWithName } from "../ToolsButtonMapper.helper"; 9 | import { RegisterToolbarWithName } from "./ToolsMapper.helper"; 10 | 11 | class Unredo implements ActiveToolbarItem { 12 | private forward: boolean; 13 | 14 | constructor(forward: boolean) { 15 | this.forward = forward; 16 | } 17 | 18 | shouldIRestorePreviousTool(previousToolContext: ToolbarAction): boolean { 19 | switch (previousToolContext.toolName) { 20 | case ToolsConstants.ToolsNames.Clear: 21 | case ToolsConstants.ToolsNames.Copy: 22 | case ToolsConstants.ToolsNames.Empty: 23 | case ToolsConstants.ToolsNames.Export: 24 | case ToolsConstants.ToolsNames.Import: 25 | case ToolsConstants.ToolsNames.Paste: 26 | case ToolsConstants.ToolsNames.PeriodicTable: 27 | case ToolsConstants.ToolsNames.Redo: 28 | case ToolsConstants.ToolsNames.Undo: 29 | return false; 30 | default: 31 | return true; 32 | } 33 | } 34 | 35 | onActivate(attrs?: LaunchAttrs) { 36 | if (!attrs) return; 37 | const { editor, previousToolContext } = attrs; 38 | if (!editor) { 39 | throw new Error("Copy.onActivate: missing attributes or editor"); 40 | } 41 | if (this.forward) { 42 | store.dispatch(ActionCreators.redo()); 43 | } else { 44 | store.dispatch(ActionCreators.undo()); 45 | } 46 | 47 | if (previousToolContext && this.shouldIRestorePreviousTool(previousToolContext)) { 48 | store.dispatch(actions.asyncDispatchTool(previousToolContext)); 49 | } else { 50 | store.dispatch(actions.asyncDispatchNone()); 51 | } 52 | } 53 | } 54 | 55 | const undoTool = new Unredo(false); 56 | const redoTool = new Unredo(true); 57 | RegisterToolbarWithName(ToolsConstants.ToolsNames.Undo, undoTool); 58 | RegisterToolbarWithName(ToolsConstants.ToolsNames.Redo, redoTool); 59 | 60 | const undo = new SimpleToolbarItemButtonBuilder( 61 | "Undo", 62 | ToolsConstants.ToolsNames.Undo, 63 | ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.ToolsNames.Undo) 64 | ); 65 | const redo = new SimpleToolbarItemButtonBuilder( 66 | "Redo", 67 | ToolsConstants.ToolsNames.Redo, 68 | ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.ToolsNames.Redo) 69 | ); 70 | 71 | RegisterToolbarButtonWithName(undo); 72 | RegisterToolbarButtonWithName(redo); 73 | 74 | export { redo, undo }; 75 | -------------------------------------------------------------------------------- /sketchem-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketchem-react", 3 | "version": "1.0.0", 4 | "private": true, 5 | "homepage": ".", 6 | "dependencies": { 7 | "@craco/craco": "^6.4.3", 8 | "@reduxjs/toolkit": "^1.8.0", 9 | "@svgdotjs/svg.js": "^3.1.2", 10 | "@types/object-hash": "^2.2.1", 11 | "@types/rbush": "^3.0.0", 12 | "@types/react": "^17.0.39", 13 | "@types/react-dom": "^17.0.11", 14 | "bootstrap": "^5.1.3", 15 | "clsx": "^1.1.1", 16 | "hotkeys-js": "^3.9.4", 17 | "lodash": "^4.17.21", 18 | "object-hash": "^3.0.0", 19 | "path-browserify": "^1.0.1", 20 | "rbush": "^3.0.1", 21 | "react": "^17.0.2", 22 | "react-bootstrap": "^2.2.3", 23 | "react-dom": "^17.0.2", 24 | "react-redux": "^7.2.6", 25 | "react-scripts": "5.0.0", 26 | "redux": "^4.1.2", 27 | "redux-undo": "^1.0.1", 28 | "tinyqueue": "^2.0.3", 29 | "typescript": "^4.6.2" 30 | }, 31 | "scripts": { 32 | "start": "craco start", 33 | "pretty": "yarn run lint_fix && yarn run prettier:write", 34 | "build": "yarn run lint_fix && yarn run prettier:write && craco build", 35 | "test": "craco test", 36 | "tsc": "tsc", 37 | "debugchrome": "cross-env BROWSER='firefox' BROWSER_ARGS='--remote-debugging-port=9222' craco start", 38 | "eject": "craco eject", 39 | "test:prettier": "prettier --check \"./**/*.{js,ts,jsx,tsx,json}\"", 40 | "test:prettier_verbose": "prettier -l \"./**/*.{js,ts,jsx,tsx,json}\"", 41 | "prettier:write": "prettier --write \"./**/*.{js,jsx,json,ts,tsx}\"", 42 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 43 | "lint_fix": "eslint . --fix --ext .js,.jsx,.ts,.tsx" 44 | }, 45 | "browserslist": { 46 | "production": [ 47 | ">0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | }, 57 | "devDependencies": { 58 | "@babel/core": "^7.17.5", 59 | "@babel/preset-react": "^7.16.7", 60 | "@types/lodash": "^4.14.182", 61 | "@typescript-eslint/eslint-plugin": "^5.19.0", 62 | "@typescript-eslint/parser": "^5.19.0", 63 | "cross-env": "^7.0.3", 64 | "eslint": "^8.13.0", 65 | "eslint-config-airbnb": "^19.0.4", 66 | "eslint-config-airbnb-typescript": "^17.0.0", 67 | "eslint-config-prettier": "^8.5.0", 68 | "eslint-import-resolver-typescript": "^2.7.1", 69 | "eslint-plugin-import": "^2.26.0", 70 | "eslint-plugin-jsx-a11y": "^6.5.1", 71 | "eslint-plugin-prettier": "^4.0.0", 72 | "eslint-plugin-react": "^7.28.0", 73 | "eslint-plugin-react-hooks": "^4.3.0", 74 | "eslint-plugin-simple-import-sort": "^7.0.0", 75 | "prettier": "^2.6.2", 76 | "sass": "^1.52.3" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sketchem-react/src/features/chemistry/issueChecker.txt: -------------------------------------------------------------------------------- 1 | checkValence = function(atom) 2 | { 3 | let currValence = atom.getValence(); 4 | let charge = atom.getCharge() || 0; 5 | let possibleValences = _getPossibleValences(atom.getAtomicNumber(), charge); 6 | if (possibleValences.length && possibleValences.indexOf(currValence) < 0) // current is abnormal 7 | { 8 | return {'currValence': currValence, 'possibleValences': possibleValences, atom} 9 | } 10 | else // no error 11 | return null; 12 | } 13 | 14 | _getPossibleValences = function(atomicNum, charge) 15 | { 16 | let result = []; 17 | let info = Kekule.ValenceUtils.getPossibleMdlValenceInfo(atomicNum, charge); 18 | if (info && info.valences && !info.unexpectedCharge) // if abnormal charge is meet, we can not determinate the valence precisely, just ignore here 19 | { 20 | result = [].concat(info.valences); 21 | } 22 | return result; 23 | } 24 | 25 | // check weather this bond valence doesn't exceed the max order of one of it's connected atoms 26 | checkBondOrder = function(bond) 27 | { 28 | let bondValence = bond.getBondValence && bond.getBondValence(); 29 | if (bondValence) 30 | { 31 | let connectedNodes = bond.getConnectedChemNodes(); 32 | let maxOrder = 0; 33 | for (let i = 0, l = connectedNodes.length; i < l; ++i) 34 | { 35 | let m = _getAllowedMaxBondOrder(connectedNodes[i]); 36 | if (m > 0 && (!maxOrder || m < maxOrder)) 37 | maxOrder = m; 38 | } 39 | if (maxOrder) 40 | { 41 | if (bondValence > maxOrder) // error 42 | { 43 | return {'currOrder': bondValence, 'maxOrder': maxOrder, bond}; 44 | } 45 | } 46 | } 47 | return null; 48 | } 49 | 50 | // Return atom max properties (src/data/kekule.structGenAtomTypesData.js), e.g.: 51 | // { 52 | // elementSymbol: "N", 53 | // atomicNumber: 7, 54 | // atomTypes: [ 55 | // { id: "N3", maxBondOrder: 3, bondOrderSum: 3 }, 56 | // { id: "N5", maxBondOrder: 2, bondOrderSum: 5 }, 57 | // ] 58 | // } 59 | _getAllowedMaxBondOrder = function(atom) 60 | { 61 | let result = 0; 62 | if (atom instanceof Kekule.Atom && atom.isNormalAtom()) 63 | { 64 | let currValence = atom.getValence(); 65 | let atomTypes = Kekule.AtomTypeDataUtil.getAllAtomTypes(atom.getAtomicNumber()); 66 | if (atomTypes) 67 | { 68 | for (let i = 0, l = atomTypes.length; i < l; ++i) 69 | { 70 | if (currValence <= atomTypes[i].bondOrderSum) 71 | { 72 | result = atomTypes[i].maxBondOrder; 73 | break; 74 | } 75 | } 76 | } 77 | } 78 | return result; 79 | } 80 | 81 | In kekule: 82 | this.valence = valence 83 | this.implicitH = hyd 84 | if (this.implicitH < 0) { 85 | this.valence = conn 86 | this.implicitH = 0 87 | this.badConn = true 88 | return false 89 | } 90 | return true 91 | } -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/periodic-table/PeriodicTableTool.tsx: -------------------------------------------------------------------------------- 1 | import { useAppDispatch } from "@app/hooks"; 2 | import * as ToolsConstants from "@constants/tools.constants"; 3 | import { actions } from "@features/toolbar-item/toolbarItemsSlice"; 4 | import { RegisterToolbarButtonWithName } from "@features/toolbar-item/ToolsButtonMapper.helper"; 5 | import { IAtomAttributes, ToolbarAction } from "@src/types"; 6 | import styles from "@styles/index.module.scss"; 7 | import clsx from "clsx"; 8 | import React, { useState } from "react"; 9 | import { Button, Card, Modal } from "react-bootstrap"; 10 | 11 | import { ToolbarItemButton } from "../../ToolbarItem"; 12 | import { RegisterToolbarWithName } from "../ToolsMapper.helper"; 13 | // import { actions } from "../toolbarItemsSlice"; 14 | import PeriodicTable from "./PeriodicTable"; 15 | 16 | export function PeriodicTableWindow(props: any) { 17 | const [modalShow, setModalShow] = useState(true); 18 | const dispatch = useAppDispatch(); 19 | const { onHide } = props; 20 | 21 | console.log("preiodc table windows "); 22 | const hideMe = () => { 23 | setModalShow(false); 24 | onHide(); 25 | }; 26 | 27 | const onAtomClick = (atomLabel: string) => { 28 | const attributes: IAtomAttributes = { 29 | label: atomLabel, 30 | color: "red", 31 | }; 32 | const payload: ToolbarAction = { 33 | toolName: ToolsConstants.ToolsNames.Atom, 34 | subToolName: ToolsConstants.createAtomSubToolName(atomLabel), 35 | attributes, 36 | }; 37 | dispatch(actions.asyncDispatchTool(payload)); 38 | }; 39 | 40 | return ( 41 | 49 | 50 | Periodic Table 51 | 52 |
53 | 54 |
55 |
56 | 59 | {/* */} 62 |
63 |
64 |
65 |
66 | ); 67 | } 68 | 69 | RegisterToolbarWithName(ToolsConstants.ToolsNames.PeriodicTable, { 70 | DialogRender: PeriodicTableWindow, 71 | }); 72 | 73 | const PeriodicTableTool: ToolbarItemButton = { 74 | name: "Periodic Table", 75 | toolName: ToolsConstants.ToolsNames.PeriodicTable, 76 | keyboardKeys: ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.ToolsNames.PeriodicTable), 77 | }; 78 | 79 | RegisterToolbarButtonWithName(PeriodicTableTool); 80 | 81 | export default PeriodicTableTool; 82 | -------------------------------------------------------------------------------- /sketchem-react/src/features/chemistry/kekuleHandler.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unreachable */ 2 | import { EditorConstants } from "@constants/editor.constant"; 3 | import { LayersNames } from "@constants/enum.constants"; 4 | import * as KekuleUtils from "@src/utils/KekuleUtils"; 5 | import { LayersUtils } from "@src/utils/LayersUtils"; 6 | import Vector2 from "@utils/mathsTs/Vector2"; 7 | import _ from "lodash"; 8 | 9 | import { Atom, Bond } from "../../entities"; 10 | 11 | const getBoundingBox = (mol) => { 12 | // {x2: maxX, x1: minX, y2: maxY, y1: minY} 13 | let box = mol.getContainerBox2D(); 14 | if (!box || !box.x1 || !box.x2 || !box.y1 || !box.y2) box = mol.getContainerBox3D(); 15 | const { x1: minX, y1: minY, x2: maxX, y2: maxY } = box; 16 | return { minX, minY, maxX, maxY }; 17 | }; 18 | 19 | export const drawMol = (mol) => { 20 | const canvas = LayersUtils.getLayer(LayersNames.Root); 21 | 22 | // canvas.zoom(1); 23 | // Bo{x: 304.0000000000001, y: 304.0000000000001, w: 1392, width: 1392, h: 1392,…} 24 | const viewBox = canvas.viewbox(); 25 | const targetCenterPoint = new Vector2(viewBox.x + 0.5 * viewBox.width, viewBox.y + 0.5 * viewBox.height); 26 | 27 | const molBoundingBox = getBoundingBox(mol); 28 | const molWidth = molBoundingBox.maxX - molBoundingBox.minX; 29 | const molHeight = molBoundingBox.maxY - molBoundingBox.minY; 30 | 31 | const molScale = EditorConstants.Scale; 32 | 33 | const sourceCenterPoint = new Vector2( 34 | molBoundingBox.minX + 0.5 * molWidth, 35 | -(molBoundingBox.minY + 0.5 * molHeight) 36 | ).scaleNew(molScale); 37 | 38 | const pointsDelta = targetCenterPoint.subNew(sourceCenterPoint); 39 | for (let i = 0, l = mol.getNodeCount(); i < l; i += 1) { 40 | const node = mol.getNodeAt(i); 41 | const { x, y } = _.isEmpty(node.absCoord2D) ? node.absCoord3D : node.absCoord2D; 42 | if (x === undefined || y === undefined || Number.isNaN(x) || Number.isNaN(y)) 43 | throw new Error("Invalid atom coord, please check molecule file"); 44 | const pos = new Vector2(x, -y).scaleSelf(molScale).addSelf(pointsDelta); 45 | 46 | console.log(`${i}. old: ${x},${y} new: ${pos.x},${pos.y}`); 47 | // const pos = new Vector2(x, -y); 48 | // pos.scaleNew(molScale); 49 | // pos.add(pointsDelta); 50 | const id = Atom.generateNewId(); 51 | node.id = id; 52 | node.setCoord2D({ x: pos.x, y: pos.y }); 53 | 54 | const atom = new Atom({ nodeObj: node }); 55 | atom.execOuterDrawCommand(); 56 | } 57 | // iterate all connectors(bonds) 58 | for (let i = 0, l = mol.getConnectorCount(); i < l; i += 1) { 59 | const connector = mol.getConnectorAt(i); 60 | const id = Bond.generateNewId(); 61 | connector.id = id; 62 | const bond = new Bond({ connectorObj: connector }); 63 | bond.draw(canvas); 64 | } 65 | 66 | // merge fragments 67 | const realMol = KekuleUtils.getMolObject(); 68 | KekuleUtils.MergeStructFragment(mol, realMol); 69 | }; 70 | 71 | export const drawMolFromFile = (fileContext) => { 72 | if (!fileContext.format || !fileContext.content) return; 73 | const mol = KekuleUtils.importMoleculeFromFile(fileContext.content, fileContext.format); 74 | if (mol === undefined) return; 75 | drawMol(mol); 76 | }; 77 | -------------------------------------------------------------------------------- /sketchem-react/src/features/shared/oldRbushKnn.ts: -------------------------------------------------------------------------------- 1 | import type RBush from "rbush"; 2 | import type { BBox } from "rbush"; 3 | import Queue from "tinyqueue"; 4 | 5 | function compareDist(a: INode, b: INode) { 6 | return a.dist - b.dist; 7 | } 8 | 9 | function axisDist(k: number, min: number, max: number) { 10 | if (k < min) { 11 | return min - k; 12 | } 13 | if (k <= max) { 14 | return 0; 15 | } 16 | return k - max; 17 | } 18 | 19 | function boxDist(x: number, y: number, box: BBox) { 20 | const dx = axisDist(x, box.minX, box.maxX); 21 | const dy = axisDist(y, box.minY, box.maxY); 22 | return dx * dx + dy * dy; 23 | } 24 | 25 | // children: (3) [{…}, {…}, {…}] 26 | // height: 2 27 | // leaf: false 28 | // maxX: 716.5869432530515 29 | // maxY: 441.7202243483998 30 | // minX: -223.9130567469483 31 | // minY: 53.85318376773341 32 | 33 | interface INode { 34 | node: any; 35 | isItem: boolean; 36 | dist: number; 37 | } 38 | 39 | interface IData { 40 | children: any; 41 | height: number; 42 | leaf: boolean; 43 | maxX: number; 44 | maxY: number; 45 | minX: number; 46 | minY: number; 47 | } 48 | 49 | function knn( 50 | tree: RBush, 51 | x: number, 52 | y: number, 53 | n?: number, 54 | maxDistance?: number, 55 | predicate?: (c: any) => boolean 56 | ) { 57 | // @ts-ignore 58 | let node: IData | undefined = tree.data; 59 | const result: INode[] = []; 60 | const { toBBox } = tree; 61 | let i: number; 62 | let child: any; 63 | let dist: number; 64 | let candidate; 65 | 66 | const queue = new Queue(undefined, compareDist); 67 | 68 | while (node) { 69 | for (i = 0; i < node.children.length; i += 1) { 70 | child = node.children[i]; 71 | dist = boxDist(x, y, node.leaf ? toBBox(child) : child); 72 | if (!maxDistance || dist <= maxDistance * maxDistance) { 73 | queue.push({ 74 | node: child, 75 | isItem: node.leaf, 76 | dist, 77 | } as INode); 78 | } 79 | } 80 | while (queue.length && queue.peek()!.isItem) { 81 | candidate = queue.pop()!.node; 82 | if (!predicate || predicate(candidate)) { 83 | result.push(candidate); 84 | } 85 | if (n && result.length === n) { 86 | return result; 87 | } 88 | } 89 | 90 | const queueNode = queue.pop(); 91 | if (queueNode) { 92 | node = queueNode.node; 93 | } else { 94 | node = undefined; 95 | } 96 | } 97 | 98 | return result; 99 | } 100 | 101 | export function mergeArrays(trees: INode[][], n?: number) { 102 | const result: INode[] = []; 103 | const queue = new Queue(undefined, compareDist); 104 | 105 | trees.forEach((tree) => { 106 | tree.forEach((node) => { 107 | queue.push(node); 108 | }); 109 | }); 110 | 111 | while (queue.length) { 112 | const node = queue.pop(); 113 | if (node) { 114 | result.push(node); 115 | } 116 | if (n && result.length === n) { 117 | return result; 118 | } 119 | } 120 | 121 | return result; 122 | } 123 | 124 | export default knn; 125 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/Charge.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { EntityVisualState } from "@constants/enum.constants"; 3 | import * as ToolsConstants from "@constants/tools.constants"; 4 | import { Atom } from "@entities"; 5 | import { EditorHandler } from "@features/editor/EditorHandler"; 6 | import { IChargeAttributes, MouseEventCallBackProperties } from "@src/types"; 7 | 8 | import { ActiveToolbarItem, LaunchAttrs, ToolbarItemButton } from "../ToolbarItem"; 9 | import { RegisterToolbarButtonWithName } from "../ToolsButtonMapper.helper"; 10 | import { RegisterToolbarWithName } from "./ToolsMapper.helper"; 11 | 12 | export interface ChargeToolbarItemButton extends ToolbarItemButton { 13 | attributes: IChargeAttributes; 14 | } 15 | 16 | class Charge implements ActiveToolbarItem { 17 | private charge: number = 0; 18 | 19 | protected changeSelectionCharge(editor: EditorHandler) { 20 | // This function is called when the user clicks on the charge button, 21 | // and it changes the charge of the selected atoms. 22 | const setAtomCharge = (atom: Atom) => { 23 | this.updateAtomCharge(atom); 24 | }; 25 | 26 | const changed = editor.applyFunctionToAtoms(setAtomCharge, true); 27 | editor.resetSelectedAtoms(); 28 | editor.resetSelectedBonds(); 29 | if (changed > 0) editor.createHistoryUpdate(); 30 | } 31 | 32 | onActivate(attrs?: LaunchAttrs) { 33 | if (!attrs) return; 34 | const { toolAttributes, editor } = attrs; 35 | if (!toolAttributes || !editor) { 36 | throw new Error("Charge.onActivate: missing attributes or editor"); 37 | } 38 | const attributes = toolAttributes as IChargeAttributes; 39 | this.charge = attributes.charge; 40 | this.changeSelectionCharge(editor); 41 | editor.setHoverMode(true, true, false); 42 | } 43 | 44 | updateAtomCharge(atom: Atom) { 45 | const attrs = atom.getAttributes(); 46 | attrs.charge += this.charge; 47 | atom.updateAttributes({ charge: attrs.charge }); 48 | } 49 | 50 | onMouseClick(eventHolder: MouseEventCallBackProperties) { 51 | const { editor } = eventHolder; 52 | const atom = editor.getHoveredAtom(); 53 | if (!atom) return; 54 | atom.setVisualState(EntityVisualState.AnimatedClick); 55 | 56 | this.updateAtomCharge(atom); 57 | editor.createHistoryUpdate(); 58 | } 59 | } 60 | 61 | const charge = new Charge(); 62 | 63 | RegisterToolbarWithName(ToolsConstants.ToolsNames.Charge, charge); 64 | 65 | const ChargeMinus: ChargeToolbarItemButton = { 66 | name: "Charge Minus", 67 | subToolName: ToolsConstants.SubToolsNames.ChargeMinus, 68 | toolName: ToolsConstants.ToolsNames.Charge, 69 | attributes: { charge: -1 }, 70 | keyboardKeys: ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.SubToolsNames.ChargeMinus), 71 | }; 72 | 73 | const ChargePlus: ChargeToolbarItemButton = { 74 | name: "Charge Plus", 75 | subToolName: ToolsConstants.SubToolsNames.ChargePlus, 76 | toolName: ToolsConstants.ToolsNames.Charge, 77 | attributes: { charge: 1 }, 78 | keyboardKeys: ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.SubToolsNames.ChargePlus), 79 | }; 80 | 81 | RegisterToolbarButtonWithName(ChargeMinus); 82 | RegisterToolbarButtonWithName(ChargePlus); 83 | 84 | export { ChargeMinus, ChargePlus }; 85 | -------------------------------------------------------------------------------- /sketchem-react/src/types/index_example.ts: -------------------------------------------------------------------------------- 1 | //= ============================================================================= 2 | // Items 3 | //= ============================================================================= 4 | 5 | export interface NoteItem { 6 | id: string; 7 | text: string; 8 | created: string; 9 | lastUpdated: string; 10 | /** 11 | * Refers to the category UUID and not the actual name. 12 | */ 13 | category?: string; 14 | scratchpad?: boolean; 15 | trash?: boolean; 16 | favorite?: boolean; 17 | } 18 | 19 | export interface CategoryItem { 20 | id: string; 21 | name: string; 22 | draggedOver: boolean; 23 | } 24 | 25 | export interface GithubUser { 26 | [anyProp: string]: any; 27 | } 28 | 29 | //= ============================================================================= 30 | // State 31 | //= ============================================================================= 32 | 33 | export interface AuthState { 34 | loading: boolean; 35 | currentUser: GithubUser; 36 | isAuthenticated: boolean; 37 | error?: string; 38 | } 39 | 40 | export interface CategoryState { 41 | categories: CategoryItem[]; 42 | error: string; 43 | loading: boolean; 44 | editingCategory: { 45 | id: string; 46 | tempName: string; 47 | }; 48 | } 49 | 50 | export interface NoteState { 51 | notes: NoteItem[]; 52 | activeNoteId: string; 53 | selectedNotesIds: string[]; 54 | activeCategoryId: string; 55 | error: string; 56 | loading: boolean; 57 | searchValue: string; 58 | } 59 | 60 | export interface SettingsState { 61 | isOpen: boolean; 62 | previewMarkdown: boolean; 63 | loading: boolean; 64 | darkTheme: boolean; 65 | sidebarVisible: boolean; 66 | codeMirrorOptions: { [key: string]: any }; 67 | } 68 | 69 | export interface SyncState { 70 | syncing: boolean; 71 | lastSynced: string; 72 | error: string; 73 | pendingSync: boolean; 74 | } 75 | 76 | export interface RootState { 77 | authState: AuthState; 78 | categoryState: CategoryState; 79 | noteState: NoteState; 80 | settingsState: SettingsState; 81 | syncState: SyncState; 82 | } 83 | 84 | //= ============================================================================= 85 | // API 86 | //= ============================================================================= 87 | 88 | export interface SyncPayload { 89 | categories: CategoryItem[]; 90 | notes: NoteItem[]; 91 | } 92 | 93 | //= ============================================================================= 94 | // Events 95 | //= ============================================================================= 96 | 97 | // export type ReactDragEvent = React.DragEvent 98 | 99 | // export type ReactMouseEvent = 100 | // | MouseEvent 101 | // | React.MouseEvent 102 | // | React.ChangeEvent 103 | 104 | // export type ReactSubmitEvent = React.FormEvent | React.FocusEvent 105 | 106 | //= ============================================================================= 107 | // Default Types 108 | //= ============================================================================= 109 | 110 | // Taken from TypeScript private declared type within Actions 111 | export type WithPayload = T & { 112 | payload: P; 113 | }; 114 | -------------------------------------------------------------------------------- /sketchem-react/src/features/counter/counterSlice.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { RootState, AppThunk } from "app/store"; 3 | import { fetchCount } from "./counterAPI"; 4 | 5 | export interface CounterState { 6 | value: number; 7 | status: "idle" | "loading" | "failed"; 8 | } 9 | 10 | const initialState: CounterState = { 11 | value: 0, 12 | status: "idle", 13 | }; 14 | 15 | // The function below is called a thunk and allows us to perform async logic. It 16 | // can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This 17 | // will call the thunk with the `dispatch` function as the first argument. Async 18 | // code can then be executed and other actions can be dispatched. Thunks are 19 | // typically used to make async requests. 20 | export const incrementAsync = createAsyncThunk("counter/fetchCount", async (amount: number) => { 21 | const response = await fetchCount(amount); 22 | // The value we return becomes the `fulfilled` action payload 23 | return response.data; 24 | }); 25 | 26 | export const counterSlice = createSlice({ 27 | name: "counter", 28 | initialState, 29 | // The `reducers` field lets us define reducers and generate associated actions 30 | reducers: { 31 | increment: (state) => { 32 | // Redux Toolkit allows us to write "mutating" logic in reducers. It 33 | // doesn't actually mutate the state because it uses the Immer library, 34 | // which detects changes to a "draft state" and produces a brand new 35 | // immutable state based off those changes 36 | state.value += 1; 37 | }, 38 | decrement: (state) => { 39 | state.value -= 1; 40 | }, 41 | // Use the PayloadAction type to declare the contents of `action.payload` 42 | incrementByAmount: (state, action: PayloadAction) => { 43 | state.value += action.payload; 44 | }, 45 | }, 46 | // The `extraReducers` field lets the slice handle actions defined elsewhere, 47 | // including actions generated by createAsyncThunk or in other slices. 48 | extraReducers: (builder) => { 49 | builder 50 | .addCase(incrementAsync.pending, (state) => { 51 | state.status = "loading"; 52 | }) 53 | .addCase(incrementAsync.fulfilled, (state, action) => { 54 | state.status = "idle"; 55 | state.value += action.payload; 56 | }); 57 | }, 58 | }); 59 | 60 | export const { increment, decrement, incrementByAmount } = counterSlice.actions; 61 | 62 | // The function below is called a selector and allows us to select a value from 63 | // the state. Selectors can also be defined inline where they're used instead of 64 | // in the slice file. For example: `useSelector((state: RootState) => state.counter.value)` 65 | export const selectCount = (state: RootState) => state.counter.value; 66 | 67 | // We can also write thunks by hand, which may contain both sync and async logic. 68 | // Here's an example of conditionally dispatching actions based on current state. 69 | export const incrementIfOdd = 70 | (amount: number): AppThunk => 71 | (dispatch, getState) => { 72 | const currentValue = selectCount(getState()); 73 | if (currentValue % 2 === 1) { 74 | dispatch(incrementByAmount(amount)); 75 | } 76 | }; 77 | 78 | export default counterSlice.reducer; 79 | -------------------------------------------------------------------------------- /sketchem-react/src/constants/tools.constants.ts: -------------------------------------------------------------------------------- 1 | export enum ToolsNames { 2 | Empty = "", 3 | Atom = "atom", 4 | Bond = "bond", 5 | Chain = "chain", 6 | Charge = "charge", 7 | Clear = "clear", 8 | Copy = "copy", 9 | Erase = "erase", 10 | Export = "export", 11 | Import = "import", 12 | Paste = "paste", 13 | PeriodicTable = "periodic_table", 14 | SelectBox = "select_box", 15 | SelectLasso = "select_lasso", 16 | Undo = "undo", 17 | Redo = "redo", 18 | DebugLoadExampleMol = "debug_load_example_mol", 19 | DebugDrawAtomTree = "debug_draw_atom_tree", 20 | DebugDrawBondTree = "debug_draw_bond_tree", 21 | DebugDrawAllPeriodic = "debug_draw_all_periodic", 22 | DebugExportMolToConsole = "debug_export_mol_to_console", 23 | } 24 | 25 | export enum SubToolsNames { 26 | BondSingle = "Single Bond", 27 | BondDouble = "Double Bond", 28 | BondTriple = "Triple Bond", 29 | BondAromatic = "Aromatic Bond", 30 | BondSingleOrDouble = "Single Or Double Bond", 31 | BondSingleOrAromatic = "Single Or Aromatic Bond", 32 | BondDoubleOrAromatic = "Double Or Aromatic Bond", 33 | BondAny = "Any Bond", 34 | BondWedgeFront = "Wedge Bond (Front)", 35 | BondWedgeBack = "Wedge Bond (Back)", 36 | BondUpOrDown = "Up Or Down Bond", 37 | BondCis = "Cis Bond", 38 | BondTrans = "Trans Bond", 39 | ChargeMinus = "Minus Charge", 40 | ChargePlus = "Plus Charge", 41 | } 42 | 43 | export function createAtomSubToolName(label: string) { 44 | return `${label} Atom`; 45 | } 46 | 47 | export const ToolsShortcutsMapByShortcut = new Map([ 48 | ["1", [SubToolsNames.BondSingle]], 49 | ["2", [SubToolsNames.BondDouble]], 50 | ["3", [SubToolsNames.BondTriple]], 51 | ["4", [SubToolsNames.BondSingleOrDouble]], 52 | ["5", [SubToolsNames.BondWedgeFront]], 53 | ["6", [SubToolsNames.BondWedgeBack]], 54 | ["Q", [ToolsNames.SelectBox]], 55 | ["W", [ToolsNames.SelectLasso]], 56 | ["E", [ToolsNames.Chain]], 57 | ["R", [ToolsNames.PeriodicTable]], 58 | ["Z", [SubToolsNames.ChargeMinus]], 59 | ["X", [SubToolsNames.ChargePlus]], 60 | ["Del, Backspace, Clear", [ToolsNames.Erase]], 61 | ["Ctrl+S", [ToolsNames.Export]], 62 | ["Ctrl+O", [ToolsNames.Import]], 63 | ["Shift+N", [ToolsNames.Clear]], 64 | ["Ctrl+C", [ToolsNames.Copy]], 65 | ["Ctrl+V", [ToolsNames.Paste]], 66 | ["Ctrl+Z", [ToolsNames.Undo]], 67 | ["Ctrl+Y", [ToolsNames.Redo]], 68 | ["H", [createAtomSubToolName("H")]], 69 | ["C", [createAtomSubToolName("C")]], 70 | ["N", [createAtomSubToolName("N")]], 71 | ["O", [createAtomSubToolName("O")]], 72 | ["S", [createAtomSubToolName("S")]], 73 | ["P", [createAtomSubToolName("P")]], 74 | ["F", [createAtomSubToolName("F")]], 75 | ["I", [createAtomSubToolName("I")]], 76 | ]); 77 | 78 | export const ToolsShortcutsMapByToolName: Map = new Map(); 79 | 80 | ToolsShortcutsMapByShortcut.forEach((toolsNames, shortcut) => { 81 | toolsNames.forEach((toolName) => { 82 | ToolsShortcutsMapByToolName.set(toolName, shortcut); 83 | }); 84 | }); 85 | 86 | export const getNextToolByShortcut = (shortcut: string, currentTool: string): string => { 87 | const tools = ToolsShortcutsMapByShortcut.get(shortcut); 88 | if (!tools) { 89 | return ToolsNames.Empty; 90 | } 91 | const index = tools.indexOf(currentTool); 92 | if (index === -1) { 93 | return tools[0]; 94 | } 95 | return tools[(index + 1) % tools.length]; 96 | }; 97 | 98 | export const ValidMouseMoveDistance = 15; 99 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/_periodic_table.scss: -------------------------------------------------------------------------------- 1 | @use "material_colors" as c; 2 | 3 | $toolbar-min-size: 80%; 4 | $toolbar-max-size: 96%; 5 | 6 | .periodic_table { 7 | width: 100%; 8 | height: 100%; 9 | table-layout: fixed; 10 | // display: table; 11 | color: #252525; 12 | background-color: #fff; 13 | 14 | tr { 15 | } 16 | 17 | td { 18 | padding: 0; 19 | min-height: 8em; 20 | height: "auto"; 21 | } 22 | 23 | th { 24 | color: rgba(c.$gray-700, .9); 25 | background-color: #fafafa; 26 | font-size: 1em; 27 | font-weight: 400; 28 | } 29 | 30 | th[scope="col"] { 31 | height: 1em; 32 | } 33 | 34 | th[scope="row"] { 35 | width: auto; 36 | } 37 | } 38 | 39 | .periodic_table_row { 40 | // display: table-row; 41 | } 42 | 43 | .periodic_table_cell { 44 | // display: table-cell; 45 | // padding: 0.1em; 46 | } 47 | 48 | .periodic_table_cell_button { 49 | // color: rgb(6, 118, 135) 50 | width: 100%; 51 | height: 100%; 52 | min-width: 0.5em; 53 | // padding: 0.1em; 54 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 55 | // border-radius: 0.1em !important; 56 | // color:#252525; 57 | // background-color: #c0c1c1 !important; 58 | // border-style: none !important; 59 | // min-width: 6em; 60 | // min-height: 5em; 61 | } 62 | 63 | .periodic_table_number { 64 | text-align: left; 65 | font-size: 0.8em; 66 | font-weight: 600; 67 | } 68 | 69 | .periodic_table_symbol { 70 | text-align: center; 71 | font-size: 1.2em; 72 | font-weight: 700; 73 | } 74 | 75 | .periodic_table_name { 76 | text-align: center; 77 | font-size: 0.7em; 78 | } 79 | 80 | $btnText: #ffffff; 81 | 82 | $btnBorderColor: #0f0f0f10; 83 | $border-radius: 0em; 84 | 85 | // @mixin button-variant( 86 | // $background, 87 | // $border, 88 | // $color: color-contrast($background), 89 | // } 90 | 91 | .unclickable_div{ 92 | pointer-events: none; 93 | } 94 | 95 | @mixin periodic_table_category_base($baseColor) { 96 | @include button-variant($baseColor, $btnBorderColor, $btnText); 97 | @include border-radius($border-radius, 0) 98 | } 99 | 100 | .periodic_table_category_diatomic_nonmetal { 101 | @include periodic_table_category_base(c.$green-500); 102 | } 103 | 104 | .periodic_table_category_unknown { 105 | @include periodic_table_category_base(c.$gray-500); 106 | } 107 | 108 | .periodic_table_category_noble_gas { 109 | @include periodic_table_category_base(c.$light-blue-400); 110 | } 111 | 112 | .periodic_table_category_alkali_metal { 113 | @include periodic_table_category_base(c.$red-400); 114 | } 115 | 116 | .periodic_table_category_alkaline_earth_metal { 117 | @include periodic_table_category_base(#dfb566); 118 | } 119 | 120 | .periodic_table_category_metalloid { 121 | @include periodic_table_category_base(c.$amber-300); 122 | } 123 | 124 | .periodic_table_category_polyatomic_nonmetal { 125 | @include periodic_table_category_base(#868760); 126 | } 127 | 128 | .periodic_table_category_post-transition_metal { 129 | @include periodic_table_category_base(c.$blue-gray-400); 130 | } 131 | 132 | .periodic_table_category_transition_metal { 133 | @include periodic_table_category_base(#df6c80); 134 | } 135 | 136 | .periodic_table_category_lanthanide { 137 | @include periodic_table_category_base(c.$deep-purple-300); 138 | } 139 | 140 | .periodic_table_category_actinide { 141 | // @include periodic_table_category_base(#ba43cf); 142 | @include periodic_table_category_base(c.$purple-500); 143 | } -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/periodic-table/PeriodicTable.tsx: -------------------------------------------------------------------------------- 1 | import { ElementsData } from "@constants/elements.constants"; 2 | import styles from "@styles/index.module.scss"; 3 | import clsx from "clsx"; 4 | import React from "react"; 5 | 6 | import { ElementCell } from "./ElementCell"; 7 | 8 | const MaxRow = 10; 9 | const MaxColumn = 18; 10 | 11 | interface PeriodicTableProps { 12 | className?: string | undefined; 13 | // eslint-disable-next-line react/no-unused-prop-types 14 | onAtomClick?: ((atomLabel: string) => void) | undefined; 15 | } 16 | 17 | function buildCell(props: PeriodicTableProps, j: number, i: number) { 18 | const { onAtomClick } = props; 19 | const key = `periodic_table_cell-${i}-${j}`; 20 | 21 | let cell; 22 | let scope; 23 | let uniqueStyle; 24 | if (i === 0 && j === 0) { 25 | cell = null; 26 | scope = "col"; 27 | uniqueStyle = { width: "1em", height: "auto" }; 28 | } else if (i === 0) { 29 | cell = j; 30 | scope = "col"; 31 | } else if (j === 0) { 32 | if (i > 7) { 33 | cell = null; 34 | } else { 35 | cell = i; 36 | } 37 | scope = "row"; 38 | } 39 | 40 | // handle labels header and first column 41 | if (scope) { 42 | return ( 43 | 44 | {cell} 45 | 46 | ); 47 | } 48 | 49 | let group: string[] = []; 50 | if ((i === 6 && j === 3) || (i === 9 && j === 2)) { 51 | cell = "*"; 52 | group = [styles.periodic_table_category_lanthanide, styles.periodic_table_symbol, styles.unclickable_div]; 53 | } else if ((i === 7 && j === 3) || (i === 10 && j === 2)) { 54 | cell = "**"; 55 | group = [styles.periodic_table_category_actinide, styles.periodic_table_symbol, styles.unclickable_div]; 56 | } else if (i === 8) { 57 | uniqueStyle = { height: "3em" }; 58 | } 59 | 60 | if (!cell) { 61 | const element = ElementsData.elementsByXYMap.get(`${i}|${j}`); 62 | if (!element || element.number > ElementsData.MaxAtomicNumber) { 63 | cell = null; 64 | } else { 65 | cell = ElementCell({ 66 | element, 67 | onClick: () => { 68 | onAtomClick?.(element.symbol); 69 | }, 70 | }); 71 | } 72 | } 73 | 74 | //
75 | return ( 76 | 77 | {cell} 78 | 79 | ); 80 | } 81 | 82 | function buildRow(props: PeriodicTableProps, i: number) { 83 | const cells: JSX.Element[] = []; 84 | for (let j = 0; j <= MaxColumn; j += 1) { 85 | cells.push(buildCell(props, j, i)); 86 | } 87 | const key = `periodic_table_row-${i}`; 88 | return ( 89 | 90 | {cells} 91 | 92 | ); 93 | } 94 | 95 | function buildTable(props: PeriodicTableProps) { 96 | const rows: JSX.Element[] = []; 97 | 98 | for (let i = 0; i <= MaxRow; i += 1) { 99 | rows.push(buildRow(props, i)); 100 | } 101 | 102 | return rows; 103 | } 104 | 105 | function PeriodicTable(props: PeriodicTableProps) { 106 | const table = buildTable(props); 107 | const { className } = props; 108 | const fullClassName = clsx(styles.periodic_table, className ?? ""); 109 | return ( 110 | 111 | {table} 112 |
113 | ); 114 | } 115 | 116 | PeriodicTable.defaultProps = { 117 | className: "", 118 | onAtomClick: () => { 119 | // currently not used, in the future it will be used to show the atom details, or select multiple atoms 120 | }, 121 | }; 122 | 123 | export default PeriodicTable; 124 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/Paste.ts: -------------------------------------------------------------------------------- 1 | import { store } from "@app/store"; 2 | import * as ToolsConstants from "@constants/tools.constants"; 3 | import { Atom, Bond } from "@entities"; 4 | import { IAtom, IBond } from "@src/types"; 5 | import Vector2 from "@src/utils/mathsTs/Vector2"; 6 | 7 | import { ActiveToolbarItem, LaunchAttrs, SimpleToolbarItemButtonBuilder } from "../ToolbarItem"; 8 | import { actions } from "../toolbarItemsSlice"; 9 | import { RegisterToolbarButtonWithName } from "../ToolsButtonMapper.helper"; 10 | import { RegisterToolbarWithName } from "./ToolsMapper.helper"; 11 | 12 | class Paste implements ActiveToolbarItem { 13 | onActivate(attrs?: LaunchAttrs) { 14 | if (!attrs) return; 15 | const { editor } = attrs; 16 | if (!editor) { 17 | throw new Error("Paste.onActivate: missing attributes or editor"); 18 | } 19 | const { atoms: ca, bonds: cb } = editor.copied; 20 | 21 | if (!ca || ca.length === 0 || !cb || cb.length === 0) { 22 | store.dispatch(actions.asyncDispatchSelect()); 23 | return; 24 | } 25 | 26 | const createdAtoms = new Map(); 27 | const createdBonds = new Map(); 28 | 29 | const editorBoundingBox = editor.getBoundingBox(); 30 | 31 | const mappedAtomsIds = new Map(); 32 | 33 | let pastedMinX = Number.MAX_SAFE_INTEGER; 34 | let pastedMaxX = Number.MIN_SAFE_INTEGER; 35 | let pastedMinY = Number.MAX_SAFE_INTEGER; 36 | let pastedMaxY = Number.MIN_SAFE_INTEGER; 37 | 38 | ca.forEach((a) => { 39 | const { attributes, id: oldId } = a; 40 | const newId = Atom.generateNewId(); 41 | mappedAtomsIds.set(oldId, newId); 42 | attributes.id = newId; 43 | const args: IAtom = { 44 | props: attributes, 45 | }; 46 | 47 | pastedMinX = Math.min(pastedMinX, attributes.center.x); 48 | pastedMaxX = Math.max(pastedMaxX, attributes.center.x); 49 | pastedMinY = Math.min(pastedMinY, attributes.center.y); 50 | pastedMaxY = Math.max(pastedMaxY, attributes.center.y); 51 | 52 | const newAtom = new Atom(args); 53 | createdAtoms.set(newAtom.getId(), newAtom); 54 | }); 55 | 56 | cb.forEach((b) => { 57 | const { attributes } = b; 58 | attributes.id = Bond.generateNewId(); 59 | const newAtomStartId = mappedAtomsIds.get(attributes.atomStartId); 60 | const newAtomEndId = mappedAtomsIds.get(attributes.atomEndId); 61 | 62 | if (!newAtomStartId || !newAtomEndId) return; 63 | 64 | attributes.atomStartId = newAtomStartId; 65 | attributes.atomEndId = newAtomEndId; 66 | 67 | const args: IBond = { 68 | props: attributes, 69 | }; 70 | const newBond = new Bond(args); 71 | createdBonds.set(newBond.getId(), newBond); 72 | }); 73 | 74 | const movedBondsIds = Array.from(createdBonds.keys()); 75 | 76 | const delta = new Vector2(editorBoundingBox.maxX * 1.1 - pastedMinX, editorBoundingBox.minY - pastedMinY); 77 | 78 | createdAtoms.forEach((a) => { 79 | a.moveByDelta(delta, movedBondsIds); 80 | a.execOuterDrawCommand(); 81 | }); 82 | 83 | createdBonds.forEach((b) => { 84 | b.moveByDelta(delta, false); 85 | b.execOuterDrawCommand(); 86 | }); 87 | 88 | editor.resetSelectedAtoms(); 89 | editor.resetSelectedBonds(); 90 | editor.createHistoryUpdate(); 91 | 92 | store.dispatch(actions.asyncDispatchSelect()); 93 | } 94 | } 95 | 96 | const pasteTool = new Paste(); 97 | RegisterToolbarWithName(ToolsConstants.ToolsNames.Paste, pasteTool); 98 | 99 | const paste = new SimpleToolbarItemButtonBuilder( 100 | "Paste", 101 | ToolsConstants.ToolsNames.Paste, 102 | ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.ToolsNames.Paste) 103 | ); 104 | 105 | RegisterToolbarButtonWithName(paste); 106 | 107 | export default paste; 108 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/debug_tools/drawTree.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import { LayersNames } from "@constants/enum.constants"; 4 | import * as ToolsConstants from "@constants/tools.constants"; 5 | import { Atom, Bond } from "@entities"; 6 | import type { PointRBush } from "@features/shared/storage"; 7 | import { EntitiesMapsStorage } from "@features/shared/storage"; 8 | import { RegisterToolbarButtonWithName } from "@features/toolbar-item/ToolsButtonMapper.helper"; 9 | import { LayersUtils } from "@src/utils/LayersUtils"; 10 | import { Circle } from "@svgdotjs/svg.js"; 11 | 12 | import { ActiveToolbarItem, SimpleToolbarItemButtonBuilder } from "../../ToolbarItem"; 13 | import { RegisterToolbarWithName } from "../ToolsMapper.helper"; 14 | 15 | const { atomsTree, atomsMap, bondsTree, bondsMap } = EntitiesMapsStorage; 16 | 17 | class DrawTree implements ActiveToolbarItem { 18 | tree: PointRBush; 19 | 20 | radius: number; 21 | 22 | drawnCircles: Circle[]; 23 | 24 | color: string; 25 | 26 | map: Map; 27 | 28 | constructor(tree: PointRBush, color: string, radius: number, map: Map) { 29 | this.tree = tree; 30 | this.drawnCircles = []; 31 | this.map = map; 32 | this.radius = radius; 33 | this.color = color; 34 | } 35 | 36 | onActivate() { 37 | if (this.drawnCircles.length > 0) { 38 | this.drawnCircles.forEach((circle) => circle.remove()); 39 | this.drawnCircles = []; 40 | return; 41 | } 42 | 43 | let treesDup = 0; 44 | let treesSize = 0; 45 | const treeDuplicateSet = new Set(); 46 | 47 | this.tree.all().forEach((node) => { 48 | treesSize += 1; 49 | const { x, y } = node.point; 50 | const entry = `x${(Math.round(x * 100) / 100).toFixed(3)}-y${(Math.round(y * 100) / 100).toFixed(3)}`; 51 | 52 | const circle = LayersUtils.getLayer(LayersNames.General) 53 | .circle(this.radius) 54 | .attr({ "pointer-events": "none" }) 55 | .fill(this.color) 56 | .cx(x) 57 | .cy(y); 58 | 59 | if (treeDuplicateSet.has(entry)) { 60 | circle.stroke({ color: "red", width: 2 }); 61 | treesDup += 1; 62 | } 63 | treeDuplicateSet.add(entry); 64 | this.drawnCircles.push(circle); 65 | }); 66 | 67 | let mapDup = 0; 68 | const mapDuplicateSet = new Set(); 69 | 70 | this.map.forEach((node) => { 71 | const { x, y } = node.getCenter(); 72 | const entry = `x${(Math.round(x * 100) / 100).toFixed(3)}-y${(Math.round(y * 100) / 100).toFixed(3)}`; 73 | const circle = LayersUtils.getLayer(LayersNames.General) 74 | .circle(this.radius * 0.2) 75 | .attr({ "pointer-events": "none" }) 76 | .fill("#ffffff") 77 | // .opacity(0.3) 78 | .cx(x) 79 | .cy(y); 80 | if (mapDuplicateSet.has(entry)) { 81 | circle.stroke({ color: "black", width: 1 }); 82 | mapDup += 1; 83 | } 84 | mapDuplicateSet.add(entry); 85 | 86 | this.drawnCircles.push(circle); 87 | }); 88 | console.log( 89 | `Found ${treesDup}/${treesSize} duplicates in tree and ${mapDup}/${this.map.size} duplicates in map` 90 | ); 91 | } 92 | } 93 | 94 | const atomsDrawTree = new DrawTree(atomsTree, "#29cf03", 9, atomsMap); 95 | const bondsDrawTree = new DrawTree(bondsTree, "#00a9f3", 12, bondsMap); 96 | 97 | RegisterToolbarWithName(ToolsConstants.ToolsNames.DebugDrawAtomTree, atomsDrawTree); 98 | RegisterToolbarWithName(ToolsConstants.ToolsNames.DebugDrawBondTree, bondsDrawTree); 99 | 100 | const DrawAtoms = new SimpleToolbarItemButtonBuilder("draw atoms (debug)", ToolsConstants.ToolsNames.DebugDrawAtomTree); 101 | const DrawBonds = new SimpleToolbarItemButtonBuilder( 102 | "draw bonds (debug) ", 103 | ToolsConstants.ToolsNames.DebugDrawBondTree 104 | ); 105 | 106 | RegisterToolbarButtonWithName(DrawAtoms); 107 | RegisterToolbarButtonWithName(DrawBonds); 108 | 109 | export default [DrawAtoms, DrawBonds]; 110 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/_buttons.scss: -------------------------------------------------------------------------------- 1 | @use "material_colors"as c; 2 | 3 | $btnBorderColor: #0f0f0f10; 4 | $txtColor: #fefefe; 5 | 6 | @mixin buttons_category_base($baseColor, $hoverColor) { 7 | @include button-variant($baseColor, $btnBorderColor, $txtColor, $hoverColor, $btnBorderColor, $txtColor); 8 | // @include button-variant($baseColor, $btnBorderColor, $txtColor); 9 | font-weight: 700; 10 | // @include border-radius($border-radius, 0) 11 | } 12 | 13 | .buttons_ok { 14 | @include buttons_category_base(c.$blue-gray-500, c.$blue-gray-400); 15 | } 16 | 17 | .buttons_close { 18 | @include buttons_category_base(c.$red-A700, c.$red-A400); 19 | } 20 | 21 | .buttons_green { 22 | @include buttons_category_base(c.$green-800, c.$green-600); 23 | } 24 | 25 | @mixin buttons_category_base($baseColor, $hoverColor) { 26 | @include button-variant($baseColor, $btnBorderColor, $txtColor, $hoverColor, $btnBorderColor, $txtColor); 27 | // @include button-variant($baseColor, $btnBorderColor, $txtColor); 28 | font-weight: 700; 29 | // @include border-radius($border-radius, 0) 30 | } 31 | 32 | // .toolbar_icon_button { 33 | // @include button-variant(c.$white, c.$white); 34 | // } 35 | $hover-active-border-width: 5px; 36 | 37 | @mixin toolbar_icon_button_shadow_direction_variant($shadow-sizes1, 38 | $shadow-sizes2, 39 | $hover-border-color, 40 | $active-border-color: $hover-border-color) { 41 | 42 | &:not([disabled]):not(.toolbar_icon_button_active):hover { 43 | box-shadow: inset $shadow-sizes1 $shadow-sizes2 $hover-border-color !important; 44 | } 45 | 46 | &.toolbar_icon_button_active { 47 | box-shadow: inset $shadow-sizes1 $shadow-sizes2 $active-border-color !important; 48 | } 49 | } 50 | 51 | // no focus state 52 | 53 | @mixin toolbar_icon_button_variant($color, 54 | $background, 55 | $hover-color, 56 | $hover-background, 57 | $hover-border-color, 58 | $active-border-color: $hover-border-color, 59 | $active-color: $hover-color, 60 | $active-background: $hover-background, 61 | $disabled-background: shade-color($background, 30), 62 | $disabled-color: color-contrast($disabled-background)) { 63 | 64 | color: $color; 65 | // background-color: $background; 66 | background-color: none; 67 | border: none; 68 | 69 | &:not([disabled]):not(.toolbar_icon_button_active):hover { 70 | color: $hover-color; 71 | background-color: $hover-background; 72 | 73 | svg { 74 | filter: invert(27%) sepia(51%) saturate(2878%) hue-rotate(233deg) brightness(104%) contrast(97%); 75 | } 76 | } 77 | 78 | &.toolbar_icon_button_active { 79 | color: $active-color; 80 | background-color: $active-background; 81 | 82 | svg { 83 | filter: invert(23%) sepia(51%) saturate(2878%) hue-rotate(205deg) brightness(74%) contrast(97%) 84 | } 85 | } 86 | 87 | &:disabled, 88 | &.disabled { 89 | // color: $disabled-color; 90 | background-color: $disabled-background; 91 | svg { 92 | filter: invert(223%) sepia(51%) saturate(28%) hue-rotate(299deg) brightness(84%) contrast(37%) 93 | } 94 | } 95 | } 96 | 97 | @mixin toolbar_icon_button() { 98 | @include toolbar_icon_button_variant(c.$black, 99 | c.$white, 100 | c.$white, 101 | c.$gray-300, 102 | c.$purple-600, 103 | c.$blue-600, 104 | c.$gray-300, 105 | ); 106 | 107 | } 108 | 109 | @mixin toolbar_icon_button_shadow($shadow-sizes1, $shadow-sizes2) { 110 | @include toolbar_icon_button_shadow_direction_variant($shadow-sizes1, $shadow-sizes2, 111 | c.$purple-600, 112 | c.$blue-600); 113 | } 114 | 115 | .toolbar_icon_button { 116 | @include toolbar_icon_button(); 117 | 118 | &:focus-visible,&:focus{ 119 | outline: none; 120 | } 121 | } 122 | 123 | .toolbar_icon_button_active {} 124 | 125 | .toolbar_icon_button_bottom { 126 | @include toolbar_icon_button_shadow(0px, $hover-active-border-width); 127 | } 128 | 129 | .toolbar_icon_button_left { 130 | @include toolbar_icon_button_shadow(-$hover-active-border-width, 0); 131 | } 132 | 133 | .toolbar_icon_button_right { 134 | @include toolbar_icon_button_shadow($hover-active-border-width, 0); 135 | } 136 | 137 | .toolbar_icon_button_top { 138 | @include toolbar_icon_button_shadow(0, -$hover-active-border-width); 139 | } -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/Export.tsx: -------------------------------------------------------------------------------- 1 | import * as ToolsConstants from "@constants/tools.constants"; 2 | import * as KekuleUtils from "@src/utils/KekuleUtils"; 3 | import styles from "@styles/index.module.scss"; 4 | import { exportFileFromMolecule } from "@utils/KekuleUtils"; 5 | import clsx from "clsx"; 6 | import React, { useState } from "react"; 7 | import { Button, Container, Form, Modal, Row } from "react-bootstrap"; 8 | 9 | import { DialogProps, ToolbarItemButton } from "../ToolbarItem"; 10 | import { RegisterToolbarButtonWithName } from "../ToolsButtonMapper.helper"; 11 | import { RegisterToolbarWithName } from "./ToolsMapper.helper"; 12 | 13 | function SupportedFiles(props: any) { 14 | /** 15 | * The options array should contain objects. 16 | * Required keys are "name" and "value" but you can have and use any number of key/value pairs. 17 | */ 18 | const { selectOptions, initialFormat, onFormatChange } = props; 19 | 20 | return ( 21 | // 22 | onFormatChange(e.target.value)}> 23 | {selectOptions.map((element: any) => ( 24 | 27 | ))} 28 | 29 | ); 30 | } 31 | 32 | // eslint-disable-next-line react/no-unused-prop-types, @typescript-eslint/no-unused-vars, no-unused-vars 33 | function ExportFile(props: DialogProps & { title: string }) { 34 | const { onHide, editor } = props; 35 | const [format, setFormat] = useState("mol"); 36 | console.log(format); 37 | 38 | const loadText = (download: boolean) => { 39 | if (editor.isEmpty()) { 40 | onHide(); 41 | return; 42 | } 43 | editor.updateAllKekuleNodes(); 44 | const content = exportFileFromMolecule(format); 45 | if (download) { 46 | const blob = new Blob([content], { type: "text/plain" }); 47 | const url = window.URL.createObjectURL(blob); 48 | const link = document.createElement("a"); 49 | link.href = url; 50 | link.download = `sketChem_mol.${format}`; 51 | link.click(); 52 | window.URL.revokeObjectURL(url); 53 | } else { 54 | navigator.clipboard.writeText(content); 55 | } 56 | onHide(); 57 | }; 58 | 59 | const options = KekuleUtils.getSupportedWriteFormats(); 60 | 61 | return ( 62 | <> 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 | {/* Todo!! actually replace or add the molecule on the canvas */} 73 | 76 | 79 |
80 | 83 |
84 | 85 | ); 86 | } 87 | 88 | export function DialogLoadWindow(props: DialogProps) { 89 | const [modalShow, setModalShow] = useState(true); 90 | const { onHide, editor } = props; 91 | 92 | const hideMe = () => { 93 | setModalShow(false); 94 | onHide(); 95 | }; 96 | 97 | return ( 98 | 105 | 106 | 107 | ); 108 | } 109 | 110 | RegisterToolbarWithName(ToolsConstants.ToolsNames.Export, { 111 | DialogRender: DialogLoadWindow, 112 | }); 113 | 114 | const Export: ToolbarItemButton = { 115 | name: "Export", 116 | toolName: ToolsConstants.ToolsNames.Export, 117 | keyboardKeys: ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.ToolsNames.Export), 118 | }; 119 | 120 | RegisterToolbarButtonWithName(Export); 121 | 122 | export default Export; 123 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/debug_tools/drawMol.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { store } from "@app/store"; 3 | import * as ToolsConstants from "@constants/tools.constants"; 4 | import { RegisterToolbarButtonWithName } from "@features/toolbar-item/ToolsButtonMapper.helper"; 5 | 6 | import { ActiveToolbarItem, LaunchAttrs, SimpleToolbarItemButtonBuilder } from "../../ToolbarItem"; 7 | import { actions } from "../../toolbarItemsSlice"; 8 | import { RegisterToolbarWithName } from "../ToolsMapper.helper"; 9 | 10 | class DrawMolClass implements ActiveToolbarItem { 11 | i: number = 0; 12 | 13 | onActivate(attrs?: LaunchAttrs) { 14 | if (!attrs) return; 15 | const { editor } = attrs; 16 | if (!editor) { 17 | throw new Error("DrawMolClass.onActivate: missing attributes or editor"); 18 | } 19 | 20 | if (this.i !== 0) { 21 | return; 22 | } 23 | this.i += 1; 24 | const payload = { 25 | content: 26 | "mannitol.mol\n Ketcher 51622 0392D 1 1.00000 0.00000 0\n\n 13 11 0 0 0 0 999 V2000\n 13.1079 -7.3500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 12.2421 -7.8500 0.0000 I 0 0 0 0 0 0 0 0 0 0 0 0\n 13.9741 -7.8500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 13.1079 -6.3500 0.0000 Br 0 0 0 0 0 0 0 0 0 0 0 0\n 11.3759 -7.3500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 12.2421 -8.8500 0.0000 S 0 0 0 0 0 0 0 0 0 0 0 0\n 14.8399 -7.3500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 13.9741 -8.8500 0.0000 Cl 0 0 0 0 0 0 0 0 0 0 0 0\n 10.5101 -7.8500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 11.3759 -6.3500 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 15.7060 -7.8500 0.0000 I 0 0 0 0 0 0 0 0 0 0 0 0\n 9.6440 -7.3500 0.0000 P 0 0 0 0 0 0 0 0 0 0 0 0\n 7.6440 -7.3500 0.0000 Xe 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 1 3 1 0 0 0 0\n 1 4 1 6 0 0 0\n 2 5 2 0 0 0 0\n 2 6 1 6 0 0 0\n 3 7 1 0 0 0 0\n 3 8 1 1 0 0 0\n 5 9 1 0 0 0 0\n 5 10 1 1 0 0 0\n 7 11 3 0 0 0 0\n 9 12 1 0 0 0 0\nM END\n", 27 | format: "mol", 28 | replace: true, 29 | }; 30 | store.dispatch(actions.loadFile(payload)); 31 | editor.createHistoryUpdate(); 32 | } 33 | // { 34 | // name: "Load an example .mol file (debug)", 35 | // tool: { 36 | // onActivate: () => { 37 | // if (i !== 0) { 38 | // return; 39 | // } 40 | // i += 1; 41 | // const payload = { 42 | // content: 43 | // "mannitol.mol\n Ketcher 51622 0392D 1 1.00000 0.00000 0\n\n 13 11 0 0 0 0 999 V2000\n 13.1079 -7.3500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 12.2421 -7.8500 0.0000 I 0 0 0 0 0 0 0 0 0 0 0 0\n 13.9741 -7.8500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 13.1079 -6.3500 0.0000 Br 0 0 0 0 0 0 0 0 0 0 0 0\n 11.3759 -7.3500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 12.2421 -8.8500 0.0000 S 0 0 0 0 0 0 0 0 0 0 0 0\n 14.8399 -7.3500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 13.9741 -8.8500 0.0000 Cl 0 0 0 0 0 0 0 0 0 0 0 0\n 10.5101 -7.8500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 11.3759 -6.3500 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 15.7060 -7.8500 0.0000 I 0 0 0 0 0 0 0 0 0 0 0 0\n 9.6440 -7.3500 0.0000 P 0 0 0 0 0 0 0 0 0 0 0 0\n 7.6440 -7.3500 0.0000 Xe 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 1 3 1 0 0 0 0\n 1 4 1 6 0 0 0\n 2 5 2 0 0 0 0\n 2 6 1 6 0 0 0\n 3 7 1 0 0 0 0\n 3 8 1 1 0 0 0\n 5 9 1 0 0 0 0\n 5 10 1 1 0 0 0\n 7 11 3 0 0 0 0\n 9 12 1 0 0 0 0\nM END\n", 44 | // format: "mol", 45 | // replace: true, 46 | // }; 47 | // store.dispatch(actions.load_file(payload)); 48 | // }, 49 | // }, 50 | // keyboardKeys: ["A"], 51 | // } as ToolbarItemButton, 52 | } 53 | 54 | const drawMolTool = new DrawMolClass(); 55 | 56 | RegisterToolbarWithName(ToolsConstants.ToolsNames.DebugLoadExampleMol, drawMolTool); 57 | 58 | const DrawMol = new SimpleToolbarItemButtonBuilder( 59 | "draw example .mol (debug)", 60 | ToolsConstants.ToolsNames.DebugLoadExampleMol 61 | ); 62 | 63 | RegisterToolbarButtonWithName(DrawMol); 64 | 65 | export default [DrawMol]; 66 | -------------------------------------------------------------------------------- /sketchem-react/src/features/shared/storage.ts: -------------------------------------------------------------------------------- 1 | import { AtomConstants } from "@constants/atom.constants"; 2 | import { BondConstants } from "@constants/bond.constants"; 3 | import { EntityType } from "@constants/enum.constants"; 4 | import type { Atom, Bond } from "@entities"; 5 | import Vector2 from "@src/utils/mathsTs/Vector2"; 6 | import RBush from "rbush"; 7 | 8 | import { mergeArrays } from "./oldRbushKnn"; 9 | import knn, { INode } from "./rbushKnn"; 10 | 11 | export interface NamedPoint { 12 | id: number; 13 | point: Vector2; 14 | entityType: EntityType; 15 | } 16 | class PointRBush extends RBush { 17 | toBBox(v: NamedPoint) { 18 | return { minX: v.point.x, minY: v.point.y, maxX: v.point.x, maxY: v.point.y }; 19 | } 20 | 21 | compareMinX(a: NamedPoint, b: NamedPoint) { 22 | return a.point.x - b.point.x; 23 | } 24 | 25 | compareMinY(a: NamedPoint, b: NamedPoint) { 26 | return a.point.y - b.point.y; 27 | } 28 | 29 | equals(a: NamedPoint, b: NamedPoint) { 30 | return a.id === b.id; 31 | } 32 | 33 | remove(item: NamedPoint) { 34 | return super.remove(item, this.equals); 35 | } 36 | } 37 | 38 | export type { PointRBush }; 39 | 40 | // maximum number of entries in a tree node. 9 (used by default) is a reasonable 41 | // choice for most applications.Higher value means faster insertion and slower search, 42 | // and vice versa 43 | const ENTRIES_AMOUNT = 7; 44 | const atomsTree = new PointRBush(ENTRIES_AMOUNT); 45 | const bondsTree = new PointRBush(ENTRIES_AMOUNT); 46 | 47 | const atomsMap = new Map(); 48 | const bondsMap = new Map(); 49 | 50 | function getMapInstanceById(map: Map, idNum: number): Type { 51 | const entity = map.get(idNum); 52 | if (!entity) { 53 | throw new Error(`Couldn't find entity with id ${idNum}`); 54 | } 55 | return entity; 56 | } 57 | 58 | function getAtomById(idNum: number): Atom { 59 | return getMapInstanceById(atomsMap, idNum); 60 | } 61 | 62 | function getBondById(idNum: number): Bond { 63 | return getMapInstanceById(bondsMap, idNum); 64 | } 65 | 66 | function knnFromMultipleMaps( 67 | trees: PointRBush[], 68 | point: Vector2, 69 | n?: number, 70 | maxDistances?: number[], 71 | predicate?: (c: any) => boolean 72 | ) { 73 | const treeResults: INode[][] = []; 74 | let index = 0; 75 | trees.forEach((tree) => { 76 | const maxDistance = maxDistances ? maxDistances[index] : undefined; 77 | const treeResult = knn(tree, point.x, point.y, n, maxDistance, predicate); 78 | treeResults.push(treeResult); 79 | index += 1; 80 | }); 81 | const result = mergeArrays(treeResults, n); 82 | return result; 83 | } 84 | 85 | function elementAtPoint( 86 | point: Vector2, 87 | tree: PointRBush, 88 | maxDistance: number, 89 | entityType: EntityType, 90 | predicate?: (c: any) => boolean 91 | ): NamedPoint | undefined { 92 | const NeighborsToFind = 1; 93 | 94 | const closetSomethings = knn(tree, point.x, point.y, NeighborsToFind, maxDistance, predicate); 95 | const [closest] = closetSomethings; 96 | 97 | if (!closest) return undefined; 98 | 99 | const closestNode = closest.node as NamedPoint; 100 | 101 | if (closestNode.entityType !== entityType) return undefined; 102 | // console.debug(`draggedToAtom Distancfe: ${closest.dist}/${maxDistance * maxDistance}`); 103 | return closestNode; 104 | } 105 | 106 | function atomAtPoint(point: Vector2, ignoreAtoms?: number[]): NamedPoint | undefined { 107 | const atomMaxDistance = AtomConstants.SelectDistance; 108 | 109 | // don't include atoms that are in the ignore list in the search 110 | if (ignoreAtoms && ignoreAtoms.length > 0) { 111 | const predicate = (c: any) => !ignoreAtoms.includes(c.id); 112 | return elementAtPoint(point, atomsTree, atomMaxDistance, EntityType.Atom, predicate); 113 | } 114 | 115 | return elementAtPoint(point, atomsTree, atomMaxDistance, EntityType.Atom); 116 | } 117 | 118 | function bondAtPoint(point: Vector2, ignoreBonds?: number[]): NamedPoint | undefined { 119 | const bondMaxDistance = BondConstants.SelectDistance; 120 | 121 | // don't include bonds that are in the ignore list in the search 122 | if (ignoreBonds && ignoreBonds.length > 0) { 123 | const predicate = (c: any) => !ignoreBonds.includes(c.id); 124 | return elementAtPoint(point, bondsTree, bondMaxDistance, EntityType.Bond, predicate); 125 | } 126 | 127 | return elementAtPoint(point, bondsTree, bondMaxDistance, EntityType.Bond); 128 | } 129 | 130 | export const EntitiesMapsStorage = { 131 | atomsTree, 132 | bondsTree, 133 | atomsMap, 134 | bondsMap, 135 | getMapInstanceById, 136 | getAtomById, 137 | getBondById, 138 | knn, 139 | knnFromMultipleMaps, 140 | atomAtPoint, 141 | bondAtPoint, 142 | }; 143 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/ToolbarItems.tsx: -------------------------------------------------------------------------------- 1 | import { useAppDispatch } from "@app/hooks"; 2 | import { 3 | getToolbarFrequentAtoms, 4 | getToolbarItemContext, 5 | isChemistryRedoEnabled, 6 | isChemistryUndoEnabled, 7 | } from "@app/selectors"; 8 | import { Direction } from "@constants/enum.constants"; 9 | import * as ToolsConstants from "@constants/tools.constants"; 10 | import IconByName from "@styles/icons"; 11 | import styles from "@styles/index.module.scss"; 12 | import clsx from "clsx"; 13 | import hotkeys from "hotkeys-js"; 14 | import React, { useEffect } from "react"; 15 | import { useSelector } from "react-redux"; 16 | 17 | import { ToolbarItemButton } from "./ToolbarItem"; 18 | import { generateAtomsButtons } from "./tools"; 19 | import { SentDispatchEventWhenToolbarItemIsChanges } from "./ToolsButtonMapper.helper"; 20 | 21 | interface IToolbarItemsProps { 22 | toolbarItemsList: ToolbarItemButton[]; 23 | direction: Direction; 24 | // eslint-disable-next-line react/require-default-props 25 | className?: string; 26 | } 27 | 28 | type Props = IToolbarItemsProps; 29 | 30 | function isUnredoDisabled(tool: ToolbarItemButton, undoDisabled: boolean, redoDisabled: boolean) { 31 | switch (tool.toolName) { 32 | case ToolsConstants.ToolsNames.Undo: 33 | // return pastLength === 0; 34 | return undoDisabled; 35 | case ToolsConstants.ToolsNames.Redo: 36 | // return futureLength === 0; 37 | return redoDisabled; 38 | default: 39 | return false; 40 | } 41 | } 42 | 43 | export function ToolbarItems(props: Props) { 44 | const dispatch = useAppDispatch(); 45 | const { toolbarItemsList, direction } = props; 46 | 47 | let modifiedToolbarItemsList = toolbarItemsList; 48 | 49 | let { className } = props; 50 | className = className ?? ""; 51 | const directionLower = Direction[direction].toLowerCase(); 52 | const thisClassName: string = `toolbar-${directionLower}`; 53 | 54 | const currentToolbarContext = useSelector(getToolbarItemContext); 55 | const frequentAtoms = useSelector(getToolbarFrequentAtoms); 56 | const undoDisabled = !useSelector(isChemistryUndoEnabled); 57 | const redoDisabled = !useSelector(isChemistryRedoEnabled); 58 | 59 | // programmatically add the frequent atoms to the toolbar 60 | if (direction === Direction.Right) { 61 | const frequentAtomsButtons = generateAtomsButtons(frequentAtoms.atoms); 62 | modifiedToolbarItemsList = [...modifiedToolbarItemsList, ...frequentAtomsButtons]; 63 | } 64 | 65 | const currentToolbarName = currentToolbarContext.subToolName ?? currentToolbarContext?.toolName; 66 | 67 | const onToolbarClick = (event: React.MouseEvent, toolbarItem: ToolbarItemButton) => { 68 | event.stopPropagation(); 69 | SentDispatchEventWhenToolbarItemIsChanges(dispatch, toolbarItem.subToolName ?? toolbarItem.toolName); 70 | }; 71 | 72 | const setKeyboardPressEvent = (item: ToolbarItemButton) => { 73 | const disabled = isUnredoDisabled(item, undoDisabled, redoDisabled); 74 | if (!item.keyboardKeys || disabled) return; 75 | hotkeys(item.keyboardKeys, (event: any, handler: any) => { 76 | event.preventDefault(); 77 | console.log(handler.key); 78 | if (!item.keyboardKeys) return; 79 | SentDispatchEventWhenToolbarItemIsChanges( 80 | dispatch, 81 | ToolsConstants.getNextToolByShortcut(item.keyboardKeys, currentToolbarName) 82 | ); 83 | }); 84 | }; 85 | 86 | return ( 87 |
88 | {modifiedToolbarItemsList.map((item) => { 89 | const name = item.subToolName ?? item.toolName; 90 | let title = item.name; 91 | if (item.keyboardKeys) title = `${item.name} (${item.keyboardKeys})`; 92 | const isActive = currentToolbarName === name; 93 | const activeClass = isActive ? styles.toolbar_icon_button_active : ""; 94 | const disabled = isUnredoDisabled(item, undoDisabled, redoDisabled); 95 | if (!disabled) setKeyboardPressEvent(item); 96 | return ( 97 | 112 | ); 113 | })} 114 |
115 | ); 116 | } 117 | 118 | export type { IToolbarItemsProps }; 119 | -------------------------------------------------------------------------------- /sketchem-react/src/constants/bond.constants.ts: -------------------------------------------------------------------------------- 1 | import type { IBondCache } from "@src/types"; 2 | import { PathArray } from "@svgdotjs/svg.js"; 3 | 4 | const BondPadding = 16; 5 | const SmallerBondPadding = BondPadding * 0.7; 6 | const HoverSelectPadding = BondPadding * 1.2; 7 | const HalfBondPadding = BondPadding * 0.5; 8 | const BondWedgeStroke = 2; 9 | const BondWedgeBarsPadding = 4; 10 | 11 | // There's a problem to apply an svg gradient to path with x or y delta of 0 12 | const deltaNoise = 0.001; 13 | 14 | const createBondWedgeBackPointsArray = (cache: IBondCache) => { 15 | const length = cache.distance; 16 | const sectors = 2 * Math.round(length / (BondWedgeBarsPadding + BondWedgeStroke) / 2); 17 | const pointArray: any = []; 18 | 19 | const tempX = cache.startPosition.x; 20 | const tempY = cache.startPosition.y; 21 | 22 | // for (let i = 1; i <= sectors; i += 1) { 23 | for (let i = 0; i <= sectors; i += 1) { 24 | const tempBarHeight = (i / sectors) * SmallerBondPadding; 25 | const tempPoint = { 26 | x: tempX + (i / sectors) * length, 27 | y1: tempY - tempBarHeight / 2, 28 | y2: tempY + tempBarHeight / 2, 29 | }; 30 | 31 | pointArray.push(["M", tempPoint.x, tempPoint.y1]); 32 | pointArray.push(["V", tempPoint.y2]); 33 | } 34 | 35 | const pathArray: PathArray = new PathArray(pointArray); 36 | return pathArray; 37 | }; 38 | 39 | const createBondWedgeFrontPointsArray = (cache: IBondCache) => { 40 | const pointArray: any[] = []; 41 | 42 | const dx = HalfBondPadding * Math.cos(-cache.angleRad) * 0.7; 43 | const dy = HalfBondPadding * Math.sin(-cache.angleRad) * 0.7; 44 | 45 | pointArray.push(["M", cache.startPosition.x, cache.startPosition.y]); 46 | pointArray.push(["L", cache.endPosition.x - dx, cache.endPosition.y + dy]); 47 | pointArray.push(["L", cache.endPosition.x + dx, cache.endPosition.y - dy]); 48 | pointArray.push(["Z"]); 49 | 50 | const pathArray = new PathArray(pointArray); 51 | return pathArray; 52 | }; 53 | 54 | const createRegularBondPointsArray = (cache: IBondCache, lines: 1 | 2 | 3) => { 55 | const pointArray: any[] = []; 56 | let noise = 0; 57 | if (Math.abs(cache.angleDeg % 90) < 0.00001) { 58 | noise = deltaNoise; 59 | } 60 | 61 | if (lines === 1 || lines === 3) { 62 | pointArray.push(["M", cache.startPosition.x, cache.startPosition.y]); 63 | pointArray.push(["L", cache.endPosition.x + noise, cache.endPosition.y + noise]); 64 | } 65 | 66 | if (lines === 2 || lines === 3) { 67 | let distance = HalfBondPadding; 68 | if (lines === 2) distance /= 2; 69 | 70 | const dx = distance * Math.cos(-cache.angleRad); 71 | const dy = distance * Math.sin(-cache.angleRad); 72 | 73 | pointArray.push(["M", cache.startPosition.x - dx, cache.startPosition.y + dy]); 74 | pointArray.push(["L", cache.endPosition.x - dx, cache.endPosition.y + dy]); 75 | pointArray.push(["M", cache.startPosition.x + dx, cache.startPosition.y - dy]); 76 | pointArray.push(["L", cache.endPosition.x + dx, cache.endPosition.y - dy]); 77 | } 78 | 79 | const pathArray = new PathArray(pointArray); 80 | return pathArray; 81 | }; 82 | 83 | const createSingleOrDoubleBondPointsArrays = (cache: IBondCache) => { 84 | const pointArrays: any[][] = [[], []]; 85 | let noise = 0; 86 | if (Math.abs(cache.angleDeg % 90) < 0.00001) { 87 | noise = deltaNoise; 88 | } 89 | 90 | pointArrays[0].push(["M", cache.startPosition.x, cache.startPosition.y]); 91 | pointArrays[0].push(["L", cache.endPosition.x + noise, cache.endPosition.y + noise]); 92 | const distance = HalfBondPadding * 0.8; 93 | 94 | const dx = distance * Math.cos(-cache.angleRad); 95 | const dy = distance * Math.sin(-cache.angleRad); 96 | 97 | pointArrays[1].push(["M", cache.startPositionCloser.x - dx, cache.startPositionCloser.y + dy]); 98 | pointArrays[1].push(["L", cache.endPositionCloser.x - dx, cache.endPositionCloser.y + dy]); 99 | pointArrays[1].push(["M", cache.startPositionCloser.x + dx, cache.startPositionCloser.y - dy]); 100 | pointArrays[1].push(["L", cache.endPositionCloser.x + dx, cache.endPositionCloser.y - dy]); 101 | 102 | // const closerStartPosition = Vector2.midpoint(cache.startPosition, cache.endPosition, 0.25); 103 | // const closerEndPosition = Vector2.midpoint(cache.startPosition, cache.endPosition, 0.75); 104 | 105 | // pointArrays[1].push(["M", closerStartPosition.x - dx, closerStartPosition.y + dy]); 106 | // pointArrays[1].push(["L", closerEndPosition.x - dx, closerEndPosition.y + dy]); 107 | // pointArrays[1].push(["M", closerStartPosition.x + dx, closerStartPosition.y - dy]); 108 | // pointArrays[1].push(["L", closerEndPosition.x + dx, closerEndPosition.y - dy]); 109 | 110 | const pathArrays = [new PathArray(pointArrays[0]), new PathArray(pointArrays[1])]; 111 | return pathArrays; 112 | }; 113 | 114 | export const BondConstants = { 115 | padding: BondPadding, 116 | HoverSelectPadding, 117 | wedgeStroke: BondWedgeStroke, 118 | createRegularBondPointsArray, 119 | createSingleOrDoubleBondPointsArrays, 120 | createBondWedgeBackPointsArray, 121 | createBondWedgeFrontPointsArray, 122 | poly_clip_id: "poly_bond", 123 | hoverFilter: "bond_hover", 124 | SelectDistance: 10, 125 | }; 126 | -------------------------------------------------------------------------------- /sketchem-react/src/constants/enum.constants.ts: -------------------------------------------------------------------------------- 1 | export enum Direction { 2 | Top = 1, 3 | Bottom, 4 | Left, 5 | Right, 6 | } 7 | 8 | export enum MouseButtons { 9 | None = 0, 10 | Left = 1, 11 | Right = 2, 12 | Scroll = 4, 13 | Back = 8, 14 | Forward = 16, 15 | } 16 | 17 | export enum MouseEventsNames { 18 | onClick = "click", 19 | onContextMenu = "contextmenu", 20 | onDoubleClick = "doubleclick", 21 | onDrag = "drag", 22 | onDragEnd = "dragend", 23 | onDragEnter = "dragenter", 24 | onDragExit = "dragexit", 25 | onDragLeave = "dragleave", 26 | onDragOver = "dragover", 27 | onDragStart = "dragstart", 28 | onDrop = "drop", 29 | onMouseDown = "mousedown", 30 | onMouseEnter = "mouseenter", 31 | onMouseLeave = "mouseleave", 32 | onMouseMove = "mousemove", 33 | onMouseOut = "mouseout", 34 | onMouseOver = "mouseover", 35 | onMouseUp = "mouseup", 36 | } 37 | 38 | export enum LayersNames { 39 | Root = "root", 40 | AtomLabelBackground = "atom_label_background", 41 | BondHover = "bond_hover", 42 | Bond = "bond", 43 | AtomHover = "atom_hover", 44 | AtomValenceError = "atom_valence_error", 45 | AtomLabelLabel = "atom_label_label", 46 | General = "general", 47 | Selection = "selection", 48 | } 49 | 50 | export enum EntityVisualState { 51 | None = 1, 52 | Hover, 53 | Select, 54 | AnimatedClick, 55 | Merge, 56 | } 57 | 58 | export enum MouseMode { 59 | Default = -1, 60 | EmptyPress = 1, 61 | AtomPressed, 62 | BondPressed, 63 | } 64 | 65 | export enum BondOrder { 66 | Single = 1, 67 | Double = 2, 68 | Triple = 3, 69 | Aromatic = 4, 70 | SingleOrDouble = 5, 71 | SingleOrAromatic = 6, 72 | DoubleOrAromatic = 7, 73 | Any = 8, 74 | } 75 | 76 | export enum EntityType { 77 | Atom = 1, 78 | Bond = 2, 79 | } 80 | 81 | export enum EntityLifeStage { 82 | New = 1, 83 | Initialized = 2, 84 | DestroyInit = 3, 85 | Destroyed = 4, 86 | } 87 | 88 | export enum BondStereoMol { 89 | // export enum BondStereo { 90 | // for single 91 | NONE = 0, 92 | UP = 1, 93 | UP_OR_DOWN = 4, 94 | DOWN = 6, 95 | // for double 96 | // XYZ = 0, 97 | CIS_OR_TRANS = 3, 98 | } 99 | 100 | // export enum KekuleBondStereo = { 101 | export enum BondStereoKekule { 102 | /** A bond for which there is no stereochemistry. */ 103 | NONE = 0, 104 | /** A bond pointing up of which the start atom is the stereocenter and 105 | * the end atom is above the drawing plane. */ 106 | UP = 1, 107 | /** A bond pointing up of which the end atom is the stereocenter and 108 | * the start atom is above the drawing plane. */ 109 | UP_INVERTED = 2, 110 | /** A bond pointing down of which the start atom is the stereocenter 111 | * and the end atom is below the drawing plane. */ 112 | DOWN = 3, 113 | /** A bond pointing down of which the end atom is the stereocenter and 114 | * the start atom is below the drawing plane. */ 115 | DOWN_INVERTED = 4, 116 | /** A bond for which there is stereochemistry, we just do not know 117 | * if it is UP or DOWN. The start atom is the stereocenter. 118 | */ 119 | UP_OR_DOWN = 8, 120 | /** A bond for which there is stereochemistry, we just do not know 121 | * if it is UP or DOWN. The end atom is the stereocenter. 122 | */ 123 | UP_OR_DOWN_INVERTED = 9, 124 | /** A bond is closer to observer than papaer, often used in ring structures. */ 125 | CLOSER = 10, 126 | /** Indication that this double bond has a fixed, but unknown E/Z 127 | * configuration. 128 | */ 129 | E_OR_Z = 20, 130 | /** Indication that this double bond has a E configuration. 131 | */ 132 | E = 21, 133 | /** Indication that this double bond has a Z configuration. 134 | */ 135 | Z = 22, 136 | /** Indication that this double bond has a fixed configuration, defined 137 | * by the 2D and/or 3D coordinates. 138 | */ 139 | E_Z_BY_COORDINATES = 23, 140 | /** Indication that this double bond has a fixed, but unknown cis/trans 141 | * configuration. 142 | */ 143 | CIS_OR_TRANS = 30, 144 | /** Indication that this double bond has a Cis configuration. 145 | */ 146 | CIS = 31, 147 | /** Indication that this double bond has a Trans configuration. 148 | */ 149 | TRANS = 32, 150 | } 151 | 152 | // Real Mol -> Kekule 153 | // 0->0 154 | // 1->1 155 | // 3->0 - missing!! 156 | // 4->8 157 | // 6->3 158 | // export enum BondStereoKekule { 159 | // // for single 160 | // None = 0, 161 | // Up = 1, 162 | // Either = 8, 163 | // Down = 3, 164 | // // for double 165 | // // XYZ = 0, 166 | // CisOrTrans = 12, // a mistake, not available in kekule? 167 | // } 168 | 169 | // /** 170 | // * Get inverted stereo direction value. 171 | // * @param {Int} direction 172 | // * @returns {Int} 173 | // */ 174 | // getInvertedDirection: function(direction) 175 | // { 176 | // var S = Kekule.BondStereo; 177 | // switch (direction) 178 | // { 179 | // case S.UP: return S.UP_INVERTED; 180 | // case S.UP_INVERTED: return S.UP; 181 | // case S.DOWN: return S.DOWN_INVERTED; 182 | // case S.DOWN_INVERTED: return S.DOWN; 183 | // case S.UP_OR_DOWN: return S.UP_OR_DOWN_INVERTED; 184 | // default: 185 | // return direction; 186 | // } 187 | // } 188 | 189 | // WedgeBack, 190 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/toolbarItemsSlice.ts: -------------------------------------------------------------------------------- 1 | import { AtomConstants } from "@constants/atom.constants"; 2 | import * as ToolsConstants from "@constants/tools.constants"; 3 | import { drawMolFromFile } from "@features/chemistry/kekuleHandler"; 4 | import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; 5 | import { 6 | FrequentAtoms, 7 | isIAtomAttributes, 8 | LoadFileAction, 9 | SaveFileAction, 10 | ToolbarAction, 11 | ToolbarItemState, 12 | } from "@types"; 13 | 14 | const initialLoadFileAction: LoadFileAction = { 15 | content: "", 16 | format: "", 17 | }; 18 | 19 | const initialSaveFileAction: SaveFileAction = { 20 | format: "", 21 | }; 22 | 23 | const initialFrequentAtoms: FrequentAtoms = { 24 | atoms: AtomConstants.DefaultAtomsLabel, 25 | currentAtom: "", 26 | }; 27 | 28 | const initialToolbarAction: ToolbarAction = { 29 | toolName: "", 30 | }; 31 | 32 | const initialState: ToolbarItemState = { 33 | toolbarContext: initialToolbarAction, 34 | dialogWindow: "", 35 | importContext: initialLoadFileAction, 36 | exportContext: initialSaveFileAction, 37 | frequentAtoms: initialFrequentAtoms, 38 | }; 39 | 40 | function createFrequentAtoms(frequentAtoms: FrequentAtoms, newAtom: string) { 41 | const frequentAtomsList = [...frequentAtoms.atoms]; 42 | 43 | if (frequentAtomsList.length >= AtomConstants.MaxAtomsListSize) { 44 | frequentAtomsList.pop(); 45 | } 46 | 47 | if (!frequentAtomsList.includes(newAtom)) frequentAtomsList.unshift(newAtom); 48 | 49 | return { 50 | atoms: frequentAtomsList, 51 | currentAtom: newAtom, 52 | }; 53 | } 54 | 55 | const slice = createSlice({ 56 | name: "tool_bar_item", 57 | initialState, 58 | reducers: { 59 | tool_change: (state: ToolbarItemState, action: PayloadAction) => { 60 | state.toolbarContext = action.payload; 61 | state.dialogWindow = ""; 62 | 63 | const payloadAttributes = action.payload.attributes; 64 | if (payloadAttributes && isIAtomAttributes(payloadAttributes)) { 65 | const newFrequentAtoms = createFrequentAtoms(state.frequentAtoms, payloadAttributes.label); 66 | state.frequentAtoms = newFrequentAtoms; 67 | } 68 | // state.user = action.payload; 69 | // localStorage.setItem('user', JSON.stringify(action.payload)) 70 | }, 71 | reset_tool: (state: ToolbarItemState) => { 72 | state.toolbarContext = { ...initialToolbarAction }; 73 | state.dialogWindow = ""; 74 | }, 75 | dialog: (state: ToolbarItemState, action: PayloadAction) => { 76 | state.dialogWindow = action.payload; 77 | state.toolbarContext = { ...initialToolbarAction }; 78 | state.toolbarContext.toolName = action.payload; 79 | }, 80 | // add_frequent_atom: (state: ToolbarItemState, action: PayloadAction) => { 81 | // const newFrequentAtoms = createFrequentAtoms(state.frequentAtoms, action.payload); 82 | // state.frequentAtoms = newFrequentAtoms; 83 | // }, 84 | save_file: (state: ToolbarItemState, action: PayloadAction) => { 85 | state.exportContext = action.payload; 86 | }, 87 | }, 88 | // extraReducers: (builder) => { 89 | 90 | // // When we send a request, 91 | // // `fetchTodos.pending` is being fired: 92 | // builder.addCase(fetchTodos.pending, (state) => { 93 | // // At that moment, 94 | // // we change status to `loading` 95 | // // and clear all the previous errors: 96 | // state.status = "loading"; 97 | // state.error = null; 98 | // }); 99 | 100 | // // When a server responses with the data, 101 | // // `fetchTodos.fulfilled` is fired: 102 | // builder.addCase(fetchTodos.fulfilled, 103 | // (state, { payload }) => { 104 | // // We add all the new todos into the state 105 | // // and change `status` back to `idle`: 106 | // state.list.push(...payload); 107 | // state.status = "idle"; 108 | // }); 109 | // }, 110 | }); 111 | 112 | const loadFile = createAsyncThunk("load_file", async (fileContext: LoadFileAction, thunkApi) => { 113 | drawMolFromFile(fileContext); 114 | thunkApi.dispatch( 115 | slice.actions.tool_change({ 116 | toolName: "", 117 | }) 118 | ); 119 | }); 120 | 121 | const asyncDispatchTool = createAsyncThunk("set_selection_tool", async (action, thunkApi) => { 122 | thunkApi.dispatch(slice.actions.tool_change(action)); 123 | }); 124 | 125 | const asyncDispatchSelect = createAsyncThunk("set_selection_tool", async (_, thunkApi) => { 126 | thunkApi.dispatch( 127 | slice.actions.tool_change({ 128 | toolName: ToolsConstants.ToolsNames.SelectBox, 129 | }) 130 | ); 131 | }); 132 | 133 | const asyncDispatchNone = createAsyncThunk("set_empty_tool", async (_, thunkApi) => { 134 | thunkApi.dispatch( 135 | slice.actions.tool_change({ 136 | toolName: "", 137 | }) 138 | ); 139 | }); 140 | 141 | export const actions = { 142 | ...slice.actions, 143 | loadFile, 144 | asyncDispatchTool, 145 | asyncDispatchSelect, 146 | asyncDispatchNone, 147 | }; 148 | export default slice.reducer; 149 | -------------------------------------------------------------------------------- /sketchem-react/src/features/toolbar-item/tools/AtomTool.ts: -------------------------------------------------------------------------------- 1 | import { ElementsData, PtElement } from "@constants/elements.constants"; 2 | import { BondOrder, BondStereoKekule, EntityVisualState, MouseMode } from "@constants/enum.constants"; 3 | import * as ToolsConstants from "@constants/tools.constants"; 4 | import { Atom } from "@entities"; 5 | import { EditorHandler } from "@features/editor/EditorHandler"; 6 | import { IAtomAttributes, MouseEventCallBackProperties } from "@types"; 7 | 8 | import { LaunchAttrs, ToolbarItemButton } from "../ToolbarItem"; 9 | import { IsToolbarButtonExists, RegisterToolbarButtonWithName } from "../ToolsButtonMapper.helper"; 10 | import { EntityBaseTool } from "./BondEntityBaseTool.helper"; 11 | import { RegisterToolbarWithName } from "./ToolsMapper.helper"; 12 | 13 | export interface AtomToolbarItemButton extends ToolbarItemButton { 14 | attributes: IAtomAttributes; 15 | } 16 | 17 | export class AtomToolBarItem extends EntityBaseTool { 18 | atomElement!: PtElement; 19 | 20 | bondOrder: BondOrder = BondOrder.Single; 21 | 22 | bondStereo: BondStereoKekule = BondStereoKekule.NONE; 23 | 24 | init() { 25 | this.mode = MouseMode.Default; 26 | this.context = { 27 | dragged: false, 28 | }; 29 | } 30 | 31 | onDeactivate() { 32 | this.init(); 33 | } 34 | 35 | onActivate(attrs?: LaunchAttrs) { 36 | if (!attrs) return; 37 | const { toolAttributes, editor } = attrs; 38 | if (!toolAttributes || !editor) { 39 | throw new Error("AtomToolBarItem.onActivate: missing attributes or editor"); 40 | } 41 | const attributes = toolAttributes as IAtomAttributes; 42 | this.init(); 43 | const atomElement = ElementsData.elementsBySymbolMap.get(attributes.label); 44 | if (!atomElement) throw new Error(`Atom element with symbol ${attributes.label} wasn't not found`); 45 | this.atomElement = atomElement; 46 | this.symbol = atomElement.symbol; 47 | this.changeSelectionAtoms(editor); 48 | editor.setHoverMode(true, true, false); 49 | } 50 | 51 | changeSelectionAtoms(editor: EditorHandler) { 52 | const mySymbol = this.atomElement.symbol; 53 | let changed = 0; 54 | const updateAtomAttributes = (atom: Atom) => { 55 | atom.updateAttributes({ 56 | symbol: mySymbol, 57 | }); 58 | changed += 1; 59 | }; 60 | 61 | editor.applyFunctionToAtoms(updateAtomAttributes, true); 62 | editor.resetSelectedAtoms(); 63 | editor.resetSelectedBonds(); 64 | if (changed) editor.createHistoryUpdate(); 65 | } 66 | 67 | onMouseDown(eventHolder: MouseEventCallBackProperties) { 68 | this.init(); 69 | const { mouseDownLocation } = eventHolder; 70 | 71 | if (this.atomWasPressed(mouseDownLocation, eventHolder)) return; 72 | 73 | this.mode = MouseMode.EmptyPress; 74 | this.context.startAtom = this.createAtom(mouseDownLocation); 75 | this.context.startAtomIsPredefined = false; 76 | } 77 | 78 | onMouseUp(eventHolder: MouseEventCallBackProperties) { 79 | const { editor, mouseDownLocation } = eventHolder; 80 | editor.setHoverMode(true, true, false); 81 | 82 | if (this.mode === MouseMode.Default) { 83 | // !!! ??? what to do 84 | return; 85 | } 86 | 87 | if (this.mode === MouseMode.EmptyPress && !this.context.startAtom) { 88 | if (!this.context.dragged && !this.context.startAtom) { 89 | this.context.startAtom = this.createAtom(mouseDownLocation); 90 | this.context.startAtomIsPredefined = false; 91 | } else { 92 | return; 93 | } 94 | } 95 | 96 | this.context.startAtom?.execOuterDrawCommand(); 97 | 98 | // update pressed atom symbol only if it was pressed and there was no drag 99 | if ( 100 | this.mode === MouseMode.AtomPressed && 101 | !this.context.dragged && 102 | this.context.startAtom && 103 | this.context.endAtom === undefined 104 | ) { 105 | this.context.startAtom.setVisualState(EntityVisualState.AnimatedClick); 106 | this.context.startAtom.updateAttributes({ symbol: this.atomElement.symbol }); 107 | } 108 | 109 | editor.setHoverMode(true, true, false); 110 | this.createHistoryUpdate(eventHolder); 111 | } 112 | } 113 | 114 | const atom = new AtomToolBarItem(); 115 | RegisterToolbarWithName(ToolsConstants.ToolsNames.Atom, atom); 116 | 117 | export function generateAtomsButtons(atoms: string[]) { 118 | const defaultAtomButtons: AtomToolbarItemButton[] = []; 119 | atoms.forEach((label) => { 120 | const element = ElementsData.elementsBySymbolMap.get(label); 121 | const customName = `${label} Atom`; 122 | const newButton: AtomToolbarItemButton = { 123 | name: customName, 124 | subToolName: customName, 125 | toolName: ToolsConstants.ToolsNames.Atom, 126 | attributes: { 127 | label, 128 | color: element?.customColor ?? element?.cpkColor ?? element?.jmolColor ?? "#000000", 129 | }, 130 | keyboardKeys: 131 | ToolsConstants.ToolsShortcutsMapByToolName.get(customName) ?? 132 | ToolsConstants.ToolsShortcutsMapByToolName.get(ToolsConstants.ToolsNames.Atom), 133 | }; 134 | if (!IsToolbarButtonExists(newButton)) { 135 | RegisterToolbarButtonWithName(newButton); 136 | } 137 | defaultAtomButtons.push(newButton); 138 | }); 139 | return defaultAtomButtons; 140 | } 141 | -------------------------------------------------------------------------------- /sketchem-react/src/utils/mathsTs/maths.ts: -------------------------------------------------------------------------------- 1 | /** ************************************************************************** 2 | MIT License 3 | 4 | Copyright (c) 2012-2019 Simen Storsveen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included 15 | in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | ************************************************************************** */ 25 | 26 | /* eslint-disable no-nested-ternary */ 27 | /* eslint-disable no-param-reassign */ 28 | 29 | /** 30 | * `Maths` is a collection of functions and constants generally relevant to 2D/3D geometry and graphics 31 | * 32 | * Example usage: 33 | * ``` 34 | * import * as Maths from '@utils/mathsTs/maths'; 35 | * ``` 36 | * 37 | * @module 38 | */ 39 | 40 | /** 41 | * `2π` as a constant 42 | */ 43 | export const TWO_PI = Math.PI * 2.0; 44 | 45 | /** 46 | * `π/2` as a constant 47 | */ 48 | export const PI_BY_2 = Math.PI / 2.0; 49 | 50 | /** 51 | * `π/180` as a constant 52 | */ 53 | export const PI_BY_180 = Math.PI / 180.0; 54 | 55 | /** 56 | * `√̅2̅π` as a constant 57 | */ 58 | export const TWO_PI_ROOT = Math.sqrt(TWO_PI); 59 | 60 | /** 61 | * An array of floating point numbers 62 | */ 63 | export type FloatArray = number[] | Float32Array | Float64Array; 64 | 65 | /** 66 | * Calculates the cotangent of the given angle 67 | * 68 | * @param angle - an angle in radians 69 | * @returns the cotangent of `angle` 70 | */ 71 | export const cotan = (angle: number): number => 1.0 / Math.tan(angle); 72 | 73 | /** 74 | * Converts an angle in degrees to an angle in radians 75 | * 76 | * @param degrees - an angle 77 | * @returns `degrees` converted to radians 78 | */ 79 | export const deg2rad = (degrees: number): number => degrees * PI_BY_180; 80 | 81 | /** 82 | * Clamps the value `x` so that it is not less than `min` and not greater than `max` 83 | * 84 | * @param x - the value to clamp 85 | * @param min - the minimum value allowed 86 | * @param max - the maximum value allowed 87 | * @returns `x` clamped to `[min, max]` 88 | */ 89 | export const clamp = (x: number, min: number, max: number): number => (x < min ? min : x > max ? max : x); 90 | 91 | /** 92 | * Clamps the value `x` so that it is not less than `0.0` and not greater than `1.0` 93 | * 94 | * @param x - the value to clamp 95 | * @returns `x` clamped to `[0.0, 1.0]` 96 | */ 97 | export const clamp01 = (x: number): number => clamp(x, 0.0, 1.0); 98 | 99 | /** 100 | * Formats the floating point number `n` as a string. 101 | * 102 | * The result has 4 digits after the decimal point and is left-padded with spaces to a width of 10. 103 | * 104 | * This function is primarily intended for debugging and logging, and is used by the `toString()` functions in this 105 | * library 106 | * 107 | * @param n - the number to format 108 | * @returns `n` formatted as a string 109 | */ 110 | export const fpad = (n: number): string => { 111 | const d = 4; 112 | const c = " "; 113 | const w = 10; 114 | let s = n.toFixed(d); 115 | while (s.length < w) s = c + s; 116 | return s; 117 | }; 118 | 119 | /** 120 | * Linear interpolation between `a` and `b` based on `t`, where `t` is a number between `0.0` and `1.0`. 121 | * 122 | * The result will be equal to `a` when `t` is `0.0`, 123 | * equal to `b` when `t` is `1.0`, 124 | * and halfway between `a` and `b` when `t` is `0.5` 125 | * 126 | * @param a - the start value - a floating point number 127 | * @param b - the end value - a floating point number 128 | * @param t - a floating point number in the interval `[0.0, 1.0]` 129 | * @returns a value between `a` and `b` 130 | */ 131 | export const lerp = (a: number, b: number, t: number): number => (1 - t) * a + t * b; 132 | 133 | /** 134 | * Bilinear interpolation between `a1`, `b1`, `a2` and `b2` based on `s` and `t`, where `s` and `t` are numbers 135 | * between `0.0` and `1.0`. 136 | * 137 | * The calculation is as follows: `a1` and `b1` are interpolated based on `s` to give `p`, 138 | * `a2` and `b2` are interpolated based on `s` to give `q`, 139 | * and then the final result is obtained by interpolating `p` and `q` based on `t`. 140 | * 141 | * The result will be equal to `a1` when both `s` and `t` is `0.0`, 142 | * equal to `a2` when `s` is `0.0` and `t` is `1.0`, 143 | * equal to `b1` when `s` is `1.0` and `t` is `0.0`, 144 | * and equal to `b2` when both `s` and `t` is `1.0` 145 | * 146 | * @param a1 - the first start value - a floating point number 147 | * @param b1 - the first end value - a floating point number 148 | * @param a2 - the second start value - a floating point number 149 | * @param b2 - the second end value - a floating point number 150 | * @param s - a floating point number in the interval `[0.0, 1.0]` 151 | * @param t - a floating point number in the interval `[0.0, 1.0]` 152 | * @returns a value between `a1`, `b1`, `a2` and `b2` 153 | */ 154 | export const lerp2 = (a1: number, b1: number, a2: number, b2: number, s: number, t: number): number => 155 | lerp(lerp(a1, b1, s), lerp(a2, b2, s), t); 156 | -------------------------------------------------------------------------------- /sketchem-react/src/styles/icons/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ToolsConstants from "@constants/tools.constants"; 2 | import type { ToolbarItemButton } from "@features/toolbar-item/ToolbarItem"; 3 | import { AtomToolbarItemButton } from "@features/toolbar-item/tools/AtomTool"; 4 | import React from "react"; 5 | 6 | // bonds 7 | import { ReactComponent as BondDoubleIcon } from "./bond_double.svg"; 8 | import { ReactComponent as BondSingleIcon } from "./bond_single.svg"; 9 | import { ReactComponent as BondSingleOrDoubleIcon } from "./bond_single_or_double.svg"; 10 | import { ReactComponent as BondTripleIcon } from "./bond_triple.svg"; 11 | import { ReactComponent as BondWedgeBackIcon } from "./bond_wedge_back.svg"; 12 | import { ReactComponent as BondWedgeFrontIcon } from "./bond_wedge_front.svg"; 13 | // icons 14 | import { ReactComponent as ChainIcon } from "./chain.svg"; 15 | import { ReactComponent as ChargeMinus } from "./charge_minus.svg"; 16 | import { ReactComponent as ChargePlus } from "./charge_plus.svg"; 17 | import { ReactComponent as ClearCanvasIcon } from "./clear_canvas.svg"; 18 | import { ReactComponent as CopyIcon } from "./copy.svg"; 19 | import { ReactComponent as EraseIcon } from "./erase.svg"; 20 | import { ReactComponent as ExportIcon } from "./export.svg"; 21 | import { ReactComponent as ImportIcon } from "./import.svg"; 22 | import { ReactComponent as PasteIcon } from "./paste.svg"; 23 | import { ReactComponent as PeriodicTable } from "./periodic_table.svg"; 24 | import { ReactComponent as RedoIcon } from "./redo.svg"; 25 | import { ReactComponent as SelectBoxIcon } from "./select_box.svg"; 26 | import { ReactComponent as SelectLassoIcon } from "./select_lasso.svg"; 27 | import { ReactComponent as UndoIcon } from "./undo.svg"; 28 | 29 | type ButtonType = React.FunctionComponent< 30 | React.SVGProps & { 31 | title?: string | undefined; 32 | } 33 | >; 34 | // create a map, where the key is the tool name and the value is the icon 35 | const IconsMap = new Map(); 36 | IconsMap.set(ToolsConstants.ToolsNames.Chain, ChainIcon); 37 | IconsMap.set(ToolsConstants.ToolsNames.Clear, ClearCanvasIcon); 38 | IconsMap.set(ToolsConstants.ToolsNames.Copy, CopyIcon); 39 | IconsMap.set(ToolsConstants.ToolsNames.Erase, EraseIcon); 40 | IconsMap.set(ToolsConstants.ToolsNames.Export, ExportIcon); 41 | IconsMap.set(ToolsConstants.ToolsNames.Import, ImportIcon); 42 | IconsMap.set(ToolsConstants.ToolsNames.Paste, PasteIcon); 43 | IconsMap.set(ToolsConstants.ToolsNames.PeriodicTable, PeriodicTable); 44 | IconsMap.set(ToolsConstants.ToolsNames.SelectBox, SelectBoxIcon); 45 | IconsMap.set(ToolsConstants.ToolsNames.SelectLasso, SelectLassoIcon); 46 | 47 | // bonds 48 | IconsMap.set(ToolsConstants.SubToolsNames.BondDouble, BondDoubleIcon); 49 | IconsMap.set(ToolsConstants.SubToolsNames.BondSingle, BondSingleIcon); 50 | IconsMap.set(ToolsConstants.SubToolsNames.BondTriple, BondTripleIcon); 51 | IconsMap.set(ToolsConstants.SubToolsNames.BondSingleOrDouble, BondSingleOrDoubleIcon); 52 | IconsMap.set(ToolsConstants.SubToolsNames.BondWedgeBack, BondWedgeBackIcon); 53 | IconsMap.set(ToolsConstants.SubToolsNames.BondWedgeFront, BondWedgeFrontIcon); 54 | 55 | // charge 56 | IconsMap.set(ToolsConstants.SubToolsNames.ChargeMinus, ChargeMinus); 57 | IconsMap.set(ToolsConstants.SubToolsNames.ChargePlus, ChargePlus); 58 | 59 | // undo redo 60 | IconsMap.set(ToolsConstants.ToolsNames.Undo, UndoIcon); 61 | IconsMap.set(ToolsConstants.ToolsNames.Redo, RedoIcon); 62 | 63 | const IconSize = "2.5rem"; 64 | const IconFontSize = "2rem"; 65 | 66 | // function generateAtomIcon(tool: AtomToolbarItemButton): JSX.Element { 67 | function generateAtomIcon(tool: AtomToolbarItemButton): (props: any) => JSX.Element { 68 | const { attributes } = tool; 69 | const { label, color } = attributes; 70 | 71 | // create an div with label in horizontal and vertical center, and with the color 72 | function Icon(props: any) { 73 | const { height } = props; 74 | return ( 75 |
92 | {label} 93 |
94 | ); 95 | } 96 | 97 | return Icon; 98 | } 99 | 100 | function generateDebugIcon(tool: ToolbarItemButton): (props: any) => JSX.Element { 101 | const { name } = tool; 102 | 103 | function Icon() { 104 | return ( 105 |
117 | {name} 118 |
119 | ); 120 | } 121 | 122 | return Icon; 123 | } 124 | 125 | export default function getToolbarIconByName(tool: ToolbarItemButton, ...props: any) { 126 | const name = tool.subToolName ?? tool.toolName; 127 | let Icon; 128 | if (tool.toolName === ToolsConstants.ToolsNames.Atom) { 129 | Icon = generateAtomIcon(tool as AtomToolbarItemButton); 130 | } else if (tool.toolName && tool.toolName.startsWith("debug")) { 131 | Icon = generateDebugIcon(tool); 132 | } 133 | Icon = Icon ?? IconsMap.get(name); 134 | if (!Icon) { 135 | return
; 136 | } 137 | return ; 138 | } 139 | --------------------------------------------------------------------------------