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 |
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 |
--------------------------------------------------------------------------------