lhasa-ligand-builder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +18 -0
- package/LICENSE +674 -0
- package/README.md +41 -0
- package/index.html +49 -0
- package/package.json +37 -0
- package/public/.gitkeep +0 -0
- package/public/Components-inchikey.ich +48167 -0
- package/public/icons/README +7 -0
- package/public/icons/dark/layla_3c.svg +82 -0
- package/public/icons/dark/layla_4c.svg +82 -0
- package/public/icons/dark/layla_5c.svg +82 -0
- package/public/icons/dark/layla_6arom.svg +89 -0
- package/public/icons/dark/layla_6c.svg +82 -0
- package/public/icons/dark/layla_7c.svg +82 -0
- package/public/icons/dark/layla_8c.svg +82 -0
- package/public/icons/dark/layla_charge_tool.svg +78 -0
- package/public/icons/dark/layla_delete_hydrogens_tool.svg +384 -0
- package/public/icons/dark/layla_double_bond.svg +78 -0
- package/public/icons/dark/layla_format_tool.svg +283 -0
- package/public/icons/dark/layla_geometry_tool.svg +105 -0
- package/public/icons/dark/layla_key.svg +76 -0
- package/public/icons/dark/layla_move_tool.svg +110 -0
- package/public/icons/dark/layla_single_bond.svg +73 -0
- package/public/icons/dark/layla_triple_bond.svg +87 -0
- package/public/icons/dark/lhasa_delete_tool.svg +401 -0
- package/public/icons/dark/lhasa_flip_x_tool.svg +106 -0
- package/public/icons/dark/lhasa_flip_y_tool.svg +106 -0
- package/public/icons/dark/lhasa_rotate_tool.svg +112 -0
- package/public/icons/icons/hicolor_apps_scalable_coot-layla.svg +105 -0
- package/public/icons/layla_3c.svg +82 -0
- package/public/icons/layla_4c.svg +82 -0
- package/public/icons/layla_5c.svg +82 -0
- package/public/icons/layla_6arom.svg +89 -0
- package/public/icons/layla_6c.svg +82 -0
- package/public/icons/layla_7c.svg +82 -0
- package/public/icons/layla_8c.svg +82 -0
- package/public/icons/layla_charge_tool.svg +78 -0
- package/public/icons/layla_delete_hydrogens_tool.svg +384 -0
- package/public/icons/layla_double_bond.svg +78 -0
- package/public/icons/layla_format_tool.svg +283 -0
- package/public/icons/layla_geometry_tool-dark.svg +105 -0
- package/public/icons/layla_geometry_tool.svg +105 -0
- package/public/icons/layla_key.svg +76 -0
- package/public/icons/layla_move_tool.svg +110 -0
- package/public/icons/layla_single_bond.svg +73 -0
- package/public/icons/layla_triple_bond.svg +87 -0
- package/public/icons/lhasa_delete_tool.svg +401 -0
- package/public/icons/lhasa_flip_x_tool.svg +106 -0
- package/public/icons/lhasa_flip_y_tool.svg +106 -0
- package/public/icons/lhasa_rotate_tool.svg +112 -0
- package/public/lhasa.js +2 -0
- package/public/lhasa.wasm +0 -0
- package/public/react.svg +1 -0
- package/src/Lhasa.tsx +1452 -0
- package/src/assets/.gitkeep +0 -0
- package/src/bansu_integration.tsx +315 -0
- package/src/customize_mui.scss +97 -0
- package/src/inchikey_database_parse.tsx +20 -0
- package/src/index.d.ts +11 -0
- package/src/index.scss +352 -0
- package/src/main.tsx +79 -0
- package/src/qed_property_infobox.tsx +14 -0
- package/src/types.d.ts +375 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +25 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +55 -0
package/src/Lhasa.tsx
ADDED
|
@@ -0,0 +1,1452 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, useState, createContext, useMemo } from 'react'
|
|
2
|
+
import { HotKeys } from "react-hotkeys"
|
|
3
|
+
//import * as d3 from "d3";
|
|
4
|
+
import { create as D3Create } from 'd3';
|
|
5
|
+
import './index.scss';
|
|
6
|
+
import './customize_mui.scss';
|
|
7
|
+
import { Canvas, Color, DisplayMode, MainModule, QEDInfo, TextMeasurementCache } from './types';
|
|
8
|
+
import { ToggleButton, Button, Switch, FormGroup, FormControlLabel, FormControl, RadioGroup, Radio, Slider, TextField, Menu, MenuItem, Accordion, AccordionSummary, AccordionDetails, Popover, StyledEngineProvider, IconButton, Tabs, Tab, Tooltip } from '@mui/material';
|
|
9
|
+
import { Redo, Undo } from '@mui/icons-material';
|
|
10
|
+
import { QedPropertyInfobox } from './qed_property_infobox';
|
|
11
|
+
import { BansuButton } from './bansu_integration';
|
|
12
|
+
import { parseInchikeyDatabase } from './inchikey_database_parse';
|
|
13
|
+
|
|
14
|
+
class ToolButtonProps {
|
|
15
|
+
onclick?: () => void;
|
|
16
|
+
action_name: string | undefined;
|
|
17
|
+
caption: string | undefined;
|
|
18
|
+
caption_optional?: boolean = false;
|
|
19
|
+
icon: string | undefined | null;
|
|
20
|
+
tooltip_body?: React.JSX.Element | null = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class ActiveToolContextData {
|
|
24
|
+
active_tool_name?: string = '';
|
|
25
|
+
show_optional_captions?: boolean = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ActiveToolContext = createContext<ActiveToolContextData>(new ActiveToolContextData());
|
|
29
|
+
|
|
30
|
+
function ToolButton(props:ToolButtonProps) {
|
|
31
|
+
// console.log(props.caption);
|
|
32
|
+
return (
|
|
33
|
+
<ActiveToolContext.Consumer>
|
|
34
|
+
{context => (
|
|
35
|
+
<Tooltip
|
|
36
|
+
title={props.tooltip_body}
|
|
37
|
+
enterDelay={1000}
|
|
38
|
+
enterNextDelay={1000}
|
|
39
|
+
disableInteractive
|
|
40
|
+
>
|
|
41
|
+
<ToggleButton
|
|
42
|
+
selected={context.active_tool_name == props.action_name}
|
|
43
|
+
onChange={props.onclick}
|
|
44
|
+
value={'dummy'}
|
|
45
|
+
// Doesn't work: autoCapitalize='false'
|
|
46
|
+
style={{textTransform: 'none', padding: '0px'}}
|
|
47
|
+
>
|
|
48
|
+
<div className='tool_button'>
|
|
49
|
+
{props.icon &&
|
|
50
|
+
<>
|
|
51
|
+
<img src={props.icon} className="lhasa_icon" />
|
|
52
|
+
</>}
|
|
53
|
+
{(context.show_optional_captions || !props.caption_optional) && props.caption}
|
|
54
|
+
</div>
|
|
55
|
+
</ToggleButton>
|
|
56
|
+
</Tooltip>
|
|
57
|
+
)}
|
|
58
|
+
</ActiveToolContext.Consumer>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Parameters for scaling the slider
|
|
63
|
+
// It's made so that leftmost edge is min_scale
|
|
64
|
+
// rightmost edge is max_scale
|
|
65
|
+
// and the value of 1 is in the middle
|
|
66
|
+
|
|
67
|
+
const max_scale = 4;
|
|
68
|
+
const min_scale = 0.4;
|
|
69
|
+
const c_const = (min_scale*max_scale-1) / (min_scale+max_scale-2);
|
|
70
|
+
const d_bottom = (1 - max_scale) / (min_scale - 1);
|
|
71
|
+
const d_top= (min_scale-1)**2 / (min_scale+max_scale-2);
|
|
72
|
+
const d_const = (Math.log(d_top))/(2 * Math.log(d_bottom));
|
|
73
|
+
const theta_const = (max_scale -1)**2 / (min_scale - 1)**2;
|
|
74
|
+
|
|
75
|
+
class LhasaComponentProps {
|
|
76
|
+
Lhasa: MainModule | any;
|
|
77
|
+
show_top_panel?: boolean;
|
|
78
|
+
show_footer?: boolean;
|
|
79
|
+
icons_path_prefix?: string;
|
|
80
|
+
/// Base64-encoded pickles
|
|
81
|
+
rdkit_molecule_pickle_list?: { pickle: string; id: string }[];
|
|
82
|
+
/// When Lhasa is embedded, what is it embedded in?
|
|
83
|
+
name_of_host_program?: string;
|
|
84
|
+
/// Called when a molecule changes.
|
|
85
|
+
/// Can be provided to get updates when a molecule changes
|
|
86
|
+
smiles_callback?: (internal_id: number, id_from_prop: string | null, smiles: string) => void;
|
|
87
|
+
bansu_endpoint?: string | undefined;
|
|
88
|
+
data_path_prefix?: string;
|
|
89
|
+
dark_mode?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
export function LhasaComponent({
|
|
94
|
+
Lhasa,
|
|
95
|
+
show_top_panel = false,
|
|
96
|
+
show_footer = true,
|
|
97
|
+
icons_path_prefix = '',
|
|
98
|
+
rdkit_molecule_pickle_list,
|
|
99
|
+
name_of_host_program = 'Moorhen',
|
|
100
|
+
smiles_callback,
|
|
101
|
+
bansu_endpoint = 'https://www.ccp4.ac.uk/bansu',
|
|
102
|
+
data_path_prefix = '',
|
|
103
|
+
dark_mode = false,
|
|
104
|
+
} : LhasaComponentProps) {
|
|
105
|
+
function on_render(lh: Canvas, text_measurement_cache: TextMeasurementCache, text_measurement_worker_div: string) {
|
|
106
|
+
console.debug("on_render() called.");
|
|
107
|
+
|
|
108
|
+
const css_color_from_lhasa_color = (lhasa_color: Color) => {
|
|
109
|
+
return 'rgba(' + lhasa_color.r * 255 + ','+ lhasa_color.g * 255 + ',' + lhasa_color.b * 255 + ',' + lhasa_color.a + ')';
|
|
110
|
+
}
|
|
111
|
+
const lhasa_text_to_d3js = (msvg, text) => {
|
|
112
|
+
const style_to_attrstring = (style) => {
|
|
113
|
+
let style_string = "";
|
|
114
|
+
if(style.specifies_color) {
|
|
115
|
+
style_string += "fill:";
|
|
116
|
+
style_string += css_color_from_lhasa_color(style.color);
|
|
117
|
+
style_string += ";";
|
|
118
|
+
}
|
|
119
|
+
// font-size:75%;baseline-shift:sub / super
|
|
120
|
+
switch(style.positioning) {
|
|
121
|
+
case Lhasa.TextPositioning.Normal:
|
|
122
|
+
if(style.size != "") {
|
|
123
|
+
style_string += "font-size:";
|
|
124
|
+
style_string += style.size;
|
|
125
|
+
style_string += ";";
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
case Lhasa.TextPositioning.Sub:
|
|
129
|
+
style_string += "font-size:75%;baseline-shift:sub;";
|
|
130
|
+
break;
|
|
131
|
+
case Lhasa.TextPositioning.Super:
|
|
132
|
+
style_string += "font-size:75%;baseline-shift:super;";
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if(style.weight != "") {
|
|
136
|
+
style_string += "font-weight:";
|
|
137
|
+
style_string += style.weight;
|
|
138
|
+
style_string += ";";
|
|
139
|
+
}
|
|
140
|
+
return style_string;
|
|
141
|
+
};
|
|
142
|
+
const append_spans_to_node = (spans, text_node, text_origin) => {
|
|
143
|
+
for(let i = 0; i < spans.size(); i++) {
|
|
144
|
+
const span = spans.get(i);
|
|
145
|
+
let child = text_node
|
|
146
|
+
// .enter()
|
|
147
|
+
.append("tspan");
|
|
148
|
+
if(span.specifies_style) {
|
|
149
|
+
child.attr("style", style_to_attrstring(span.style));
|
|
150
|
+
}
|
|
151
|
+
if(span.has_subspans()) {
|
|
152
|
+
append_spans_to_node(span.as_subspans(), child, text_origin);
|
|
153
|
+
} else if(span.is_newline()) {
|
|
154
|
+
child.attr("dy", "1em");
|
|
155
|
+
child.attr("x", text_origin.x);
|
|
156
|
+
// Invisible U+2063 to force tspan to be rendered
|
|
157
|
+
child.text("");
|
|
158
|
+
} else {
|
|
159
|
+
const caption = span.as_caption();
|
|
160
|
+
child.text(caption);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// return ret;
|
|
164
|
+
}
|
|
165
|
+
const ret = msvg.append("text")
|
|
166
|
+
.attr("x", text.origin.x)
|
|
167
|
+
.attr("y", text.origin.y);
|
|
168
|
+
|
|
169
|
+
const style_string = style_to_attrstring(text.style);
|
|
170
|
+
if(style_string != "") {
|
|
171
|
+
ret.attr("style", style_string);
|
|
172
|
+
} else {
|
|
173
|
+
// console.warn("Empty style string!");
|
|
174
|
+
}
|
|
175
|
+
if(text.spans.size() == 0) {
|
|
176
|
+
console.warn("Text contains no spans!");
|
|
177
|
+
}
|
|
178
|
+
append_spans_to_node(text.spans, ret, text.origin);
|
|
179
|
+
return ret;
|
|
180
|
+
};
|
|
181
|
+
const text_measure_function = (text) => {
|
|
182
|
+
// let size_info = new Lhasa.TextSize;
|
|
183
|
+
let size_info = {
|
|
184
|
+
'width': 0,
|
|
185
|
+
'height': 0
|
|
186
|
+
};
|
|
187
|
+
try{
|
|
188
|
+
//const domNode = document.createElement("div");
|
|
189
|
+
const domNode = document.getElementById(text_measurement_worker_div);
|
|
190
|
+
if (domNode == null) {
|
|
191
|
+
// todo: do something with this.
|
|
192
|
+
return size_info;
|
|
193
|
+
}
|
|
194
|
+
const msvg = D3Create("svg")
|
|
195
|
+
.attr("width", 100)
|
|
196
|
+
.attr("height", 100)
|
|
197
|
+
.attr("id", "measurement_temporary");
|
|
198
|
+
const text_elem = lhasa_text_to_d3js(msvg, text);
|
|
199
|
+
domNode.append(msvg.node());
|
|
200
|
+
const node = text_elem.node();
|
|
201
|
+
// This has awful performance but I don't really have a choice
|
|
202
|
+
const bbox = node.getBBox();
|
|
203
|
+
size_info.width = Math.ceil(bbox.width);
|
|
204
|
+
size_info.height = Math.ceil(bbox.height);
|
|
205
|
+
// console.log("measurement returning:", size_info);
|
|
206
|
+
msvg.remove();
|
|
207
|
+
} catch(err) {
|
|
208
|
+
console.error('Error occured in text measurement: ', err);
|
|
209
|
+
} finally {
|
|
210
|
+
return size_info;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
};
|
|
214
|
+
const ren = new Lhasa.Renderer(text_measure_function, text_measurement_cache);
|
|
215
|
+
lh.render(ren);
|
|
216
|
+
const commands = ren.get_commands();
|
|
217
|
+
const get_width = () => {
|
|
218
|
+
const measured = lh.measure(Lhasa.MeasurementDirection.HORIZONTAL).requested_size;
|
|
219
|
+
const min_size = 480;
|
|
220
|
+
if(measured < min_size) {
|
|
221
|
+
return min_size;
|
|
222
|
+
}
|
|
223
|
+
return measured;
|
|
224
|
+
};
|
|
225
|
+
const get_height = () => {
|
|
226
|
+
const measured = lh.measure(Lhasa.MeasurementDirection.VERTICAL).requested_size;
|
|
227
|
+
const min_size = 270;
|
|
228
|
+
if(measured < min_size) {
|
|
229
|
+
return min_size;
|
|
230
|
+
}
|
|
231
|
+
return measured;
|
|
232
|
+
};
|
|
233
|
+
const svg = D3Create("svg")
|
|
234
|
+
.attr("class", "lhasa_drawing")
|
|
235
|
+
.attr("width", get_width())
|
|
236
|
+
.attr("height", get_height());
|
|
237
|
+
|
|
238
|
+
for(var i = 0; i < commands.size(); i++) {
|
|
239
|
+
const command = commands.get(i);
|
|
240
|
+
if(command.is_path()) {
|
|
241
|
+
const path = command.as_path();
|
|
242
|
+
// this causes a crash
|
|
243
|
+
// if(path.commands.empty()) {
|
|
244
|
+
// // console.warn("Empty path!");
|
|
245
|
+
// // return;
|
|
246
|
+
// }
|
|
247
|
+
// TODO: COMPLETE REWRITE
|
|
248
|
+
const path_node = svg.append("path");
|
|
249
|
+
let path_started = false;
|
|
250
|
+
let d_string = "";
|
|
251
|
+
// This should be just a reference
|
|
252
|
+
const elements = path.get_elements();
|
|
253
|
+
for(var j = 0; j < elements.size(); j++) {
|
|
254
|
+
const element = elements.get(j);
|
|
255
|
+
if(element.is_arc()) {
|
|
256
|
+
const arc = element.as_arc();
|
|
257
|
+
|
|
258
|
+
// Thanks to: https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
|
|
259
|
+
function polarToCartesian(centerX, centerY, radius, angleInRadians) {
|
|
260
|
+
//var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
x: centerX + (radius * Math.cos(angleInRadians)),
|
|
264
|
+
y: centerY + (radius * Math.sin(angleInRadians))
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function describeArc(x, y, radius, startAngle, endAngle){
|
|
269
|
+
|
|
270
|
+
var start = polarToCartesian(x, y, radius, endAngle);
|
|
271
|
+
var end = polarToCartesian(x, y, radius, startAngle);
|
|
272
|
+
|
|
273
|
+
var largeArcFlag = endAngle - startAngle <= Math.PI ? "0" : "1";
|
|
274
|
+
//var largeArcFlag = "1";
|
|
275
|
+
|
|
276
|
+
// From the SVG reference:
|
|
277
|
+
//
|
|
278
|
+
// If sweep-flag is '1',
|
|
279
|
+
// then the arc will be drawn in a "positive-angle" direction
|
|
280
|
+
// (i.e., the ellipse formula x=cx+rx*cos(theta) and y=cy+ry*sin(theta)
|
|
281
|
+
// is evaluated such that theta starts at an angle corresponding
|
|
282
|
+
// to the current point and increases positively until the arc reaches (x,y)).
|
|
283
|
+
// A value of 0 causes the arc to be drawn in a "negative-angle" direction
|
|
284
|
+
// (i.e., theta starts at an angle value corresponding to the current point and decreases until the arc reaches (x,y)).
|
|
285
|
+
const sweep = 1;
|
|
286
|
+
var p = path_started ? [] : ["M", start.x, start.y];
|
|
287
|
+
var d = p.concat([
|
|
288
|
+
"A", radius, radius, 0, largeArcFlag, sweep, end.x, end.y,
|
|
289
|
+
// "L", x,y,
|
|
290
|
+
// "L", start.x, start.y,
|
|
291
|
+
// "Z"
|
|
292
|
+
]).join(" ");
|
|
293
|
+
// console.log("d", d);
|
|
294
|
+
|
|
295
|
+
return d;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
d_string += describeArc(arc.origin.x, arc.origin.y, arc.radius, arc.angle_one - 0.001, arc.angle_two) + " ";
|
|
299
|
+
|
|
300
|
+
} else if(element.is_line()) {
|
|
301
|
+
const line = element.as_line();
|
|
302
|
+
|
|
303
|
+
function describeLine(x1, y1, x2, y2) {
|
|
304
|
+
var p = path_started ? [] : ["M", x1, y1];
|
|
305
|
+
var d = p.concat(["L", x2, y2]).join(" ");
|
|
306
|
+
return d;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
d_string += describeLine(line.start.x, line.start.y, line.end.x, line.end.y) + " ";
|
|
310
|
+
} else {
|
|
311
|
+
console.error("Unknown path element type");
|
|
312
|
+
console.log(element);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
path_started = true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
//Breaks wavy bond
|
|
319
|
+
//d_string += "Z";
|
|
320
|
+
|
|
321
|
+
path_node.attr("d", d_string);
|
|
322
|
+
|
|
323
|
+
let style_str = '';
|
|
324
|
+
|
|
325
|
+
if(path.has_fill) {
|
|
326
|
+
style_str += "fill: " + css_color_from_lhasa_color(path.fill_color) + ";";
|
|
327
|
+
} else {
|
|
328
|
+
style_str += "fill: none;";
|
|
329
|
+
}
|
|
330
|
+
if(path.has_stroke) {
|
|
331
|
+
style_str += "stroke:" + css_color_from_lhasa_color(path.stroke_style.color) + ";";
|
|
332
|
+
style_str += "stroke-width:" + path.stroke_style.line_width + ";";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
path_node.attr("style", style_str);
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
} else if(command.is_text()) {
|
|
339
|
+
const text = command.as_text();
|
|
340
|
+
lhasa_text_to_d3js(svg, text);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
commands.delete();
|
|
344
|
+
ren.delete();
|
|
345
|
+
|
|
346
|
+
const node = svg.node();
|
|
347
|
+
return node;
|
|
348
|
+
};
|
|
349
|
+
const text_measurement_worker_div = useId();
|
|
350
|
+
const smiles_input = useId();
|
|
351
|
+
const x_element_symbol_input = useId();
|
|
352
|
+
|
|
353
|
+
// Text measurement relies on elements with certain IDs being in the DOM already.
|
|
354
|
+
// This means that text measurement will be incorrect (zeroed) for the first render
|
|
355
|
+
// meaning that if 'first_render' is true, we should re-render immediately
|
|
356
|
+
const isFirstRenderRef = useRef<boolean>(false);
|
|
357
|
+
// Assigns internal molecule IDs to external pickle IDs as given by rdkit_molecule_pickle_map
|
|
358
|
+
const canvasIdsToPropsIdsRef = useRef<Map<number, string>>(new Map<number, string>());
|
|
359
|
+
/// Database of InChIKey -> [Monomer code, Chemical name], fetched asynchronously
|
|
360
|
+
const inchiKeyDatabase = useRef<Map<string, [string, string]> | null>(null);
|
|
361
|
+
|
|
362
|
+
const [svgNode, setSvgNode] = useState(null);
|
|
363
|
+
/// [molecule_id, smiles, [monomer_id, chem_name]?]]
|
|
364
|
+
const [smiles, setSmiles] = useState<[number, string, [string, string]?][]>([]);
|
|
365
|
+
const [scale, setScale] = useState<number>(1.0);
|
|
366
|
+
const [statusText, setStatusText] = useState<string>('');
|
|
367
|
+
const [xElementInputShown, setXElementInputShown] = useState<boolean>(false);
|
|
368
|
+
const [activeToolName, setActiveToolName] = useState<string>('');
|
|
369
|
+
|
|
370
|
+
const [smiles_error_string, setSmilesErrorString] = useState<null | string>(null);
|
|
371
|
+
const [x_element_error_string, setXElementErrorString] = useState<null | string>(null);
|
|
372
|
+
const [qedInfo, setQedInfo] = useState<Map<number, QEDInfo>>(new Map<number, QEDInfo>());
|
|
373
|
+
|
|
374
|
+
const setupLhasaCanvas = (tmc: TextMeasurementCache) => {
|
|
375
|
+
const lh = new Lhasa.Canvas();
|
|
376
|
+
lh.connect("queue_redraw", () => {
|
|
377
|
+
const node = on_render(lh, tmc, text_measurement_worker_div);
|
|
378
|
+
setSvgNode(node);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const on_status_updated = function (status_txt: string) {
|
|
382
|
+
setStatusText(status_txt);
|
|
383
|
+
};
|
|
384
|
+
lh.connect("status_updated", on_status_updated);
|
|
385
|
+
lh.connect("smiles_changed", function () {
|
|
386
|
+
const smiles_array: [number, string, [string, string]?][] = [];
|
|
387
|
+
const smiles_map = lh.get_smiles();
|
|
388
|
+
const smiles_keys = smiles_map.keys();
|
|
389
|
+
|
|
390
|
+
const inchikey_map = lh.get_inchi_keys();
|
|
391
|
+
|
|
392
|
+
for(let i = 0; i < smiles_keys.size(); i++) {
|
|
393
|
+
const mol_id = smiles_keys.get(i);
|
|
394
|
+
const inchi_key = inchikey_map.get(mol_id) as string;
|
|
395
|
+
let inchi_lookup_result : [string, string] | null = null;
|
|
396
|
+
if (inchiKeyDatabase.current?.has(inchi_key)) {
|
|
397
|
+
inchi_lookup_result = inchiKeyDatabase.current?.get(inchi_key) as [string, string];
|
|
398
|
+
}
|
|
399
|
+
const smiles_tuple = [mol_id, smiles_map.get(mol_id), inchi_lookup_result] as [number, string, [string, string]?];
|
|
400
|
+
console.log(`Inchi lookup: mol_id=${mol_id} key=${inchi_key} ${inchi_lookup_result ? `monomer_id=${inchi_lookup_result[0]} chem_name=${inchi_lookup_result[1]}` : inchiKeyDatabase.current != null ? ` not found in database` : ` (database not loaded)`}`);
|
|
401
|
+
smiles_array.push(smiles_tuple);
|
|
402
|
+
}
|
|
403
|
+
smiles_keys.delete();
|
|
404
|
+
smiles_map.delete();
|
|
405
|
+
setSmiles(smiles_array);
|
|
406
|
+
// const inchikey_map_keys = inchikey_map.keys();
|
|
407
|
+
// inchikey_map.delete();
|
|
408
|
+
// inchikey_map_keys.delete();
|
|
409
|
+
});
|
|
410
|
+
lh.connect("molecule_deleted", function (mol_id: number) {
|
|
411
|
+
console.log("Molecule with id " + mol_id + " has been deleted.");
|
|
412
|
+
const newQedInfo = qedInfo;
|
|
413
|
+
newQedInfo.delete(mol_id);
|
|
414
|
+
setQedInfo(newQedInfo);
|
|
415
|
+
});
|
|
416
|
+
lh.connect("scale_changed", function (new_scale: number) {
|
|
417
|
+
console.log('new scale: ', new_scale);
|
|
418
|
+
setScale(new_scale);
|
|
419
|
+
});
|
|
420
|
+
lh.connect("qed_info_updated", function (mol_id: number, qed_info_for_mol: QEDInfo) {
|
|
421
|
+
const newQedInfo = qedInfo;
|
|
422
|
+
newQedInfo.set(mol_id, qed_info_for_mol);
|
|
423
|
+
setQedInfo(newQedInfo);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const default_scale = 1.0;
|
|
427
|
+
lh.set_scale(default_scale);
|
|
428
|
+
|
|
429
|
+
//console.log('Adding demo molecule.');
|
|
430
|
+
//Lhasa.append_from_smiles(lh, "O=C(C)Oc1ccccc1C(=O)O");
|
|
431
|
+
|
|
432
|
+
return lh;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const lh = useRef<any>(null);
|
|
436
|
+
// Text measurement cache
|
|
437
|
+
const tmc = useRef<any>(null);
|
|
438
|
+
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (tmc.current === null || tmc.current?.isDeleted()) {
|
|
441
|
+
console.log("Creating text measurement cache.");
|
|
442
|
+
tmc.current = new Lhasa.TextMeasurementCache();
|
|
443
|
+
}
|
|
444
|
+
if (lh.current === null || lh.current?.isDeleted()) {
|
|
445
|
+
console.log("Setting up LhasaCanvas.");
|
|
446
|
+
lh.current = setupLhasaCanvas(tmc.current);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const InchiKeyDatabaseLoaderTask = async () => {
|
|
450
|
+
if (inchiKeyDatabase.current !== null) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
let retries_remaining = 15;
|
|
454
|
+
while (retries_remaining > 0) {
|
|
455
|
+
try {
|
|
456
|
+
let begin_time = performance.now();
|
|
457
|
+
console.log("Fetching InchiKeyDatabase...");
|
|
458
|
+
const response: Response = await fetch(data_path_prefix + "Components-inchikey.ich");
|
|
459
|
+
if (!response.ok) {
|
|
460
|
+
throw new Error(`InchiKeyDatabase fetch status: ${response.status}`);
|
|
461
|
+
}
|
|
462
|
+
const raw_data = await response.text();
|
|
463
|
+
let end_time = performance.now();
|
|
464
|
+
console.log(`InchiKeyDatabase fetched successfully in ${(end_time - begin_time).toFixed(2)} ms. Size = ${raw_data.length} bytes. Parsing...`);
|
|
465
|
+
begin_time = performance.now();
|
|
466
|
+
const db = parseInchikeyDatabase(raw_data);
|
|
467
|
+
end_time = performance.now();
|
|
468
|
+
inchiKeyDatabase.current = db;
|
|
469
|
+
console.log(`InchiKeyDatabase parsed successfully in ${(end_time - begin_time).toFixed(2)} ms. Entries = ${db.size}.`);
|
|
470
|
+
break;
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.error(`Could not fetch InchiKeyDatabase: ${err}\n ${retries_remaining > 0 ? `\nRetrying again in two seconds (retries remaining: ${retries_remaining})...` : "\nGiving up."}`);
|
|
473
|
+
retries_remaining -= 1;
|
|
474
|
+
// Sleep for 2 seconds before retrying
|
|
475
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
InchiKeyDatabaseLoaderTask();
|
|
482
|
+
|
|
483
|
+
return () => {
|
|
484
|
+
if (lh.current !== null && !lh.current?.isDeleted()) {
|
|
485
|
+
console.warn("Cleaning up component upon unmounting.");
|
|
486
|
+
lh.current?.delete();
|
|
487
|
+
}
|
|
488
|
+
if (tmc.current !== null && !tmc.current?.isDeleted()) {
|
|
489
|
+
console.warn("Deleting text measurement cache.");
|
|
490
|
+
tmc.current?.delete();
|
|
491
|
+
}
|
|
492
|
+
canvasIdsToPropsIdsRef.current = new Map<number, string>();
|
|
493
|
+
};
|
|
494
|
+
}, []); // Empty dependency array: run only once on mount and cleanup on unmount
|
|
495
|
+
|
|
496
|
+
useEffect(() => {
|
|
497
|
+
if(rdkit_molecule_pickle_list !== undefined) {
|
|
498
|
+
rdkit_molecule_pickle_list.forEach(item => {
|
|
499
|
+
if(! Array.from(canvasIdsToPropsIdsRef.current.values()).some(id => id === item.id) ) {
|
|
500
|
+
const internalId = Lhasa.append_from_pickle_base64(lh.current, item.pickle);
|
|
501
|
+
canvasIdsToPropsIdsRef.current.set(internalId, item.id);
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
}, [rdkit_molecule_pickle_list]);
|
|
506
|
+
|
|
507
|
+
function switch_tool(tool : any) {
|
|
508
|
+
lh.current?.set_active_tool(Lhasa.make_active_tool(tool));
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
function on_x_element_button() {
|
|
512
|
+
setXElementInputShown(prev => !prev);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function on_smiles_import_button() {
|
|
516
|
+
const smiles_input_el = document.getElementById(smiles_input) as HTMLInputElement;
|
|
517
|
+
try {
|
|
518
|
+
Lhasa.append_from_smiles(lh.current, smiles_input_el.value);
|
|
519
|
+
setSmilesErrorString(null);
|
|
520
|
+
} catch(err) {
|
|
521
|
+
console.warn("Could not import molecule from SMILES: ", err);
|
|
522
|
+
setSmilesErrorString("Could not load import molecule from SMILES. Is your SMILES valid?");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function on_x_element_submit_button() {
|
|
527
|
+
const symbol_input = document.getElementById(x_element_symbol_input) as HTMLInputElement;
|
|
528
|
+
try {
|
|
529
|
+
const el_ins = Lhasa.element_insertion_from_symbol(symbol_input.value);
|
|
530
|
+
switch_tool(el_ins);
|
|
531
|
+
setXElementInputShown(false);
|
|
532
|
+
setXElementErrorString(null);
|
|
533
|
+
}catch(err) {
|
|
534
|
+
console.warn("Could not set custom element: ", err);
|
|
535
|
+
setXElementErrorString("Could not custom element. The symbol must be invalid.");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function switch_display_mode(value: string) {
|
|
540
|
+
switch(value) {
|
|
541
|
+
default:
|
|
542
|
+
case 'standard':
|
|
543
|
+
lh.current?.set_display_mode(Lhasa.DisplayMode.Standard);
|
|
544
|
+
break;
|
|
545
|
+
case 'atom_indices':
|
|
546
|
+
lh.current?.set_display_mode(Lhasa.DisplayMode.AtomIndices);
|
|
547
|
+
break;
|
|
548
|
+
case 'atom_names':
|
|
549
|
+
lh.current?.set_display_mode(Lhasa.DisplayMode.AtomNames);
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
function display_mode_to_value_name(value: DisplayMode) {
|
|
555
|
+
switch(value) {
|
|
556
|
+
default:
|
|
557
|
+
case Lhasa.DisplayMode.Standard:
|
|
558
|
+
return 'standard';
|
|
559
|
+
case Lhasa.DisplayMode.AtomIndices:
|
|
560
|
+
return 'atom_indices';
|
|
561
|
+
case Lhasa.DisplayMode.AtomNames:
|
|
562
|
+
return 'atom_names';
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// From what I understand, this becomes non-null
|
|
567
|
+
// after the first render at which point it
|
|
568
|
+
// should point to the "lhasa_editor" div.
|
|
569
|
+
const editorRef = useRef<HTMLDivElement>(null);
|
|
570
|
+
// From what I understand, this becomes non-null
|
|
571
|
+
// after the first render at which point it
|
|
572
|
+
// should point to the "editor_canvas_container" div.
|
|
573
|
+
const svgRef = useRef<HTMLDivElement>(null);
|
|
574
|
+
// defers the callback to run after render, which is crucial for text measurement
|
|
575
|
+
// to work after the first render (we need to render it again after the first render)
|
|
576
|
+
useEffect(()=>{
|
|
577
|
+
if(svgRef.current && svgNode) {
|
|
578
|
+
svgRef.current.replaceChildren(svgNode);
|
|
579
|
+
if(isFirstRenderRef.current === true) {
|
|
580
|
+
isFirstRenderRef.current = false;
|
|
581
|
+
const newNode = on_render(lh.current, tmc.current, text_measurement_worker_div);
|
|
582
|
+
setSvgNode(newNode);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}, [svgNode]);
|
|
586
|
+
|
|
587
|
+
const icons_path_final = dark_mode ? icons_path_prefix + "/dark" : icons_path_prefix;
|
|
588
|
+
|
|
589
|
+
const carbon_ring_tooltip = useRef<React.JSX.Element>(<div>
|
|
590
|
+
<b>Carbon Ring Tool</b><br/>
|
|
591
|
+
Insert a carbon ring.<br/>
|
|
592
|
+
<br/>
|
|
593
|
+
<b>Left click on a bond</b> - Insert a carbon ring (of the selected kind) adjacent to the bond.<br/>
|
|
594
|
+
<b>Left click on an atom</b> - Attach a carbon ring (of the selected kind) to the atom.<br/>
|
|
595
|
+
<b>Control + Left click on an atom</b> - Attach a carbon ring (of the selected kind) to the atom (spiro mode).<br/>
|
|
596
|
+
<b>Left click on empty canvas</b> - Initialize a new molecule with the selected kind of carbon ring.
|
|
597
|
+
</div>);
|
|
598
|
+
|
|
599
|
+
const element_tool_tooltip = useRef<React.JSX.Element>(<div>
|
|
600
|
+
<b>Element Tool</b><br/>
|
|
601
|
+
Add a new atom or replace an existent atom with the chosen element.<br/>
|
|
602
|
+
<br/>
|
|
603
|
+
<b>Left click on a bond</b> - Insert a new atom (of the selected kind) in between the atoms of the bond.<br/>
|
|
604
|
+
<b>Left click on an atom</b> - Replace the atom with the chosen element.<br/>
|
|
605
|
+
<br/>
|
|
606
|
+
<b><i>X</i></b> allows to manually type in a custom symbol.
|
|
607
|
+
</div>);
|
|
608
|
+
|
|
609
|
+
const tool_button_data = useMemo(() => {return {
|
|
610
|
+
Move: {
|
|
611
|
+
caption:"Move",
|
|
612
|
+
raw_handler:() => switch_tool(new Lhasa.TransformTool(Lhasa.TransformMode.Translation)),
|
|
613
|
+
icon: icons_path_final + "/layla_move_tool.svg",
|
|
614
|
+
hotkey:"m",
|
|
615
|
+
caption_optional: true,
|
|
616
|
+
tooltip_core: <div>
|
|
617
|
+
<b>Move Tool</b><br/>
|
|
618
|
+
Move molecules around the screen.<br/>
|
|
619
|
+
<b>Left click</b> on a molecule (and drag) to move it.<br/>
|
|
620
|
+
<br/>
|
|
621
|
+
Move mode is also triggered by pressing the <b>Alt</b> key.
|
|
622
|
+
</div>
|
|
623
|
+
},
|
|
624
|
+
Rotate: {
|
|
625
|
+
caption:"Rotate",
|
|
626
|
+
raw_handler:() => switch_tool(new Lhasa.TransformTool(Lhasa.TransformMode.Rotation)),
|
|
627
|
+
icon: icons_path_final + "/lhasa_rotate_tool.svg",
|
|
628
|
+
hotkey:"r",
|
|
629
|
+
caption_optional: true,
|
|
630
|
+
tooltip_core: <div>
|
|
631
|
+
<b>Rotate Tool</b><br/>
|
|
632
|
+
Rotate molecules.<br/>
|
|
633
|
+
<br/>
|
|
634
|
+
<b>Left click</b> on a molecule (and drag) to rotate it.<br/>
|
|
635
|
+
Press <b>Alt</b> while dragging to snap angles (by 15 degrees).<br/>
|
|
636
|
+
<br/>
|
|
637
|
+
Rotation mode is also triggered by pressing the <b>Shift</b> key.
|
|
638
|
+
</div>
|
|
639
|
+
},
|
|
640
|
+
Flip_around_X: {
|
|
641
|
+
caption:"Flip around X",
|
|
642
|
+
raw_handler:() => switch_tool(new Lhasa.FlipTool(Lhasa.FlipMode.Horizontal)),
|
|
643
|
+
icon: icons_path_final + "/lhasa_flip_x_tool.svg",
|
|
644
|
+
hotkey:"alt+f",
|
|
645
|
+
caption_optional: true,
|
|
646
|
+
tooltip_core: <div>
|
|
647
|
+
<b>Flip Horizontal</b><br/>
|
|
648
|
+
Flip molecule in the X axis.<br/>
|
|
649
|
+
<br/>
|
|
650
|
+
<b>Left click</b> on a molecule to flip it.<br/>
|
|
651
|
+
If there's a single molecule on the screen, it will get flipped when the tool is selected.
|
|
652
|
+
</div>
|
|
653
|
+
},
|
|
654
|
+
Flip_around_Y: {
|
|
655
|
+
caption:"Flip around Y",
|
|
656
|
+
raw_handler:() => switch_tool(new Lhasa.FlipTool(Lhasa.FlipMode.Vertical)),
|
|
657
|
+
icon: icons_path_final + "/lhasa_flip_y_tool.svg",
|
|
658
|
+
hotkey:"ctrl+alt+f",
|
|
659
|
+
caption_optional: true,
|
|
660
|
+
tooltip_core: <div>
|
|
661
|
+
<b>Flip Vertical</b><br/>
|
|
662
|
+
Flip molecule in the Y axis.<br/>
|
|
663
|
+
<br/>
|
|
664
|
+
<b>Left click</b> on a molecule to flip it.<br/>
|
|
665
|
+
If there's a single molecule on the screen, it will get flipped when the tool is selected.
|
|
666
|
+
</div>
|
|
667
|
+
},
|
|
668
|
+
Delete_hydrogens: {
|
|
669
|
+
caption:"Delete hydrogens",
|
|
670
|
+
raw_handler:() => switch_tool(new Lhasa.RemoveHydrogensTool()),
|
|
671
|
+
icon: icons_path_final + "/layla_delete_hydrogens_tool.svg",
|
|
672
|
+
hotkey:"alt+delete",
|
|
673
|
+
caption_optional: true,
|
|
674
|
+
tooltip_core: <div>
|
|
675
|
+
<b>Remove Hydrogens Tool</b><br/>
|
|
676
|
+
Removes explicit non-polar hydrogens from the selected molecule.<br/>
|
|
677
|
+
<br/>
|
|
678
|
+
<b>Left click</b> on a molecule to remove explicit non-polar hydrogens.<br/>
|
|
679
|
+
If there's a single molecule on the screen, it will get processed automatically when the tool is selected.
|
|
680
|
+
</div>
|
|
681
|
+
},
|
|
682
|
+
Format: {
|
|
683
|
+
caption:"Format",
|
|
684
|
+
raw_handler:() => switch_tool(new Lhasa.FormatTool()),
|
|
685
|
+
icon: icons_path_final + "/layla_format_tool.svg",
|
|
686
|
+
hotkey:"f",
|
|
687
|
+
caption_optional: true,
|
|
688
|
+
tooltip_core: <div>
|
|
689
|
+
<b>Format Tool</b><br/>
|
|
690
|
+
"Formatting" re-computes atom positions of the selected molecule on the screen.<br/>
|
|
691
|
+
<br/>
|
|
692
|
+
<b>Left click</b> on a molecule to format it.<br/>
|
|
693
|
+
If there's a single molecule on the screen, it will get formatted when the tool is selected.
|
|
694
|
+
</div>
|
|
695
|
+
},
|
|
696
|
+
Single_Bond: {
|
|
697
|
+
caption:"Single Bond",
|
|
698
|
+
raw_handler:() => switch_tool(new Lhasa.BondModifier(Lhasa.BondModifierMode.Single)),
|
|
699
|
+
icon: icons_path_final + "/layla_single_bond.svg",
|
|
700
|
+
hotkey:"s",
|
|
701
|
+
caption_optional: true,
|
|
702
|
+
tooltip_core: <div>
|
|
703
|
+
<b>Single Bond Tool</b><br/>
|
|
704
|
+
Add bond (of the selected kind) or replace an existing one.<br/>
|
|
705
|
+
<br/>
|
|
706
|
+
<b>Left click on a bond</b> - Replace it with the selected kind of bond.<br/>
|
|
707
|
+
<b>Left click on an atom</b> - Append a new bond to the atom (along with a new carbon atom).<br/>
|
|
708
|
+
<b>Left click on an atom and drag</b> - Begin creating new bond via dragging. Release mouse at the final atom to complete the now bond.
|
|
709
|
+
</div>
|
|
710
|
+
},
|
|
711
|
+
Double_Bond: {
|
|
712
|
+
caption:"Double Bond",
|
|
713
|
+
raw_handler:() => switch_tool(new Lhasa.BondModifier(Lhasa.BondModifierMode.Double)),
|
|
714
|
+
icon: icons_path_final + "/layla_double_bond.svg",
|
|
715
|
+
hotkey:"d",
|
|
716
|
+
caption_optional: true,
|
|
717
|
+
tooltip_core: <div>
|
|
718
|
+
<b>Double Bond Tool</b><br/>
|
|
719
|
+
Add bond (of the selected kind) or replace an existing one.<br/>
|
|
720
|
+
<br/>
|
|
721
|
+
<b>Left click on a bond</b> - Replace it with the selected kind of bond.<br/>
|
|
722
|
+
<b>Left click on an atom</b> - Append a new bond to the atom (along with a new carbon atom).<br/>
|
|
723
|
+
<b>Left click on an atom and drag</b> - Begin creating new bond via dragging. Release mouse at the final atom to complete the now bond.
|
|
724
|
+
</div>
|
|
725
|
+
},
|
|
726
|
+
Triple_Bond: {
|
|
727
|
+
caption:"Triple Bond",
|
|
728
|
+
raw_handler:() => switch_tool(new Lhasa.BondModifier(Lhasa.BondModifierMode.Triple)),
|
|
729
|
+
icon: icons_path_final + "/layla_triple_bond.svg",
|
|
730
|
+
hotkey:"t",
|
|
731
|
+
caption_optional: true,
|
|
732
|
+
tooltip_core: <div>
|
|
733
|
+
<b>Triple Bond Tool</b><br/>
|
|
734
|
+
Add bond (of the selected kind) or replace an existing one.<br/>
|
|
735
|
+
<br/>
|
|
736
|
+
<b>Left click on a bond</b> - Replace it with the selected kind of bond.<br/>
|
|
737
|
+
<b>Left click on an atom</b> - Append a new bond to the atom (along with a new carbon atom).<br/>
|
|
738
|
+
<b>Left click on an atom and drag</b> - Begin creating new bond via dragging. Release mouse at the final atom to complete the now bond.
|
|
739
|
+
</div>
|
|
740
|
+
},
|
|
741
|
+
Geometry: {
|
|
742
|
+
caption:"Geometry",
|
|
743
|
+
raw_handler:() => switch_tool(new Lhasa.GeometryModifier()),
|
|
744
|
+
icon: icons_path_final + "/layla_geometry_tool.svg",
|
|
745
|
+
hotkey:"g",
|
|
746
|
+
caption_optional: true,
|
|
747
|
+
tooltip_core: <div>
|
|
748
|
+
<b>Bond Geometry Tool</b><br/>
|
|
749
|
+
Alter geometry of a bond.<br/>
|
|
750
|
+
<br/>
|
|
751
|
+
<b>Left click</b> on a bond cycles between <i>"wedged"</i>, <i>"dashed"</i> and unspecified (<i>"wavy"</i>) bond geometry.<br/>
|
|
752
|
+
Bond geometry is only represented for single bonds.
|
|
753
|
+
</div>
|
|
754
|
+
},
|
|
755
|
+
Charge: {
|
|
756
|
+
caption:"Charge",
|
|
757
|
+
raw_handler:() => switch_tool(new Lhasa.ChargeModifier()),
|
|
758
|
+
icon: icons_path_final + "/layla_charge_tool.svg",
|
|
759
|
+
hotkey:"v",
|
|
760
|
+
caption_optional: true,
|
|
761
|
+
tooltip_core: <div>
|
|
762
|
+
<b>Atom Charge Tool</b><br/>
|
|
763
|
+
Alter charge of an atom.<br/>
|
|
764
|
+
<br/>
|
|
765
|
+
<b>Left click on an atom</b> - Add electrons (more negative charge).<br/>
|
|
766
|
+
<b>Right click on an atom</b> - Remove electrons (more positive charge).
|
|
767
|
+
</div>
|
|
768
|
+
},
|
|
769
|
+
Delete: {
|
|
770
|
+
caption:"Delete",
|
|
771
|
+
raw_handler:() => switch_tool(new Lhasa.DeleteTool()),
|
|
772
|
+
icon: icons_path_final + "/lhasa_delete_tool.svg",
|
|
773
|
+
hotkey:"delete",
|
|
774
|
+
caption_optional: true,
|
|
775
|
+
tooltip_core: <div>
|
|
776
|
+
<b>Delete Tool</b><br/>
|
|
777
|
+
Delete atoms, bonds or molecules.<br/>
|
|
778
|
+
<br/>
|
|
779
|
+
<b>Left click on a bond</b> - Removes the bond (or the whole R-chain).<br/>
|
|
780
|
+
<b>Left click on an atom</b> - Removes the atom (or the whole R-chain).<br/>
|
|
781
|
+
<br/>
|
|
782
|
+
<b>Control + Alt + Left click on a bond</b> - Removes just the bond (without any R-chains).<br/>
|
|
783
|
+
<b>Control + Alt + Left click on an atom</b> - Removes just the atom (without any R-chains).<br/>
|
|
784
|
+
<br/>
|
|
785
|
+
<b>Control + Left click on a molecule</b> - Remove the selected molecule
|
|
786
|
+
</div>
|
|
787
|
+
},
|
|
788
|
+
C3: {
|
|
789
|
+
caption:"3-C",
|
|
790
|
+
raw_handler:() => switch_tool(new Lhasa.StructureInsertion(Lhasa.LhasaStructure.CycloPropaneRing)),
|
|
791
|
+
icon: icons_path_final + "/layla_3c.svg",
|
|
792
|
+
hotkey:"3",
|
|
793
|
+
caption_optional: true,
|
|
794
|
+
tooltip_core: carbon_ring_tooltip.current
|
|
795
|
+
},
|
|
796
|
+
C4: {
|
|
797
|
+
caption:"4-C",
|
|
798
|
+
raw_handler:() => switch_tool(new Lhasa.StructureInsertion(Lhasa.LhasaStructure.CycloButaneRing)),
|
|
799
|
+
icon: icons_path_final + "/layla_4c.svg",
|
|
800
|
+
hotkey:"4",
|
|
801
|
+
caption_optional: true,
|
|
802
|
+
tooltip_core: carbon_ring_tooltip.current
|
|
803
|
+
},
|
|
804
|
+
C5: {
|
|
805
|
+
caption:"5-C",
|
|
806
|
+
raw_handler:() => switch_tool(new Lhasa.StructureInsertion(Lhasa.LhasaStructure.CycloPentaneRing)),
|
|
807
|
+
icon: icons_path_final + "/layla_5c.svg",
|
|
808
|
+
hotkey:"5",
|
|
809
|
+
caption_optional: true,
|
|
810
|
+
tooltip_core: carbon_ring_tooltip.current
|
|
811
|
+
},
|
|
812
|
+
C6: {
|
|
813
|
+
caption:"6-C",
|
|
814
|
+
raw_handler:() => switch_tool(new Lhasa.StructureInsertion(Lhasa.LhasaStructure.CycloHexaneRing)),
|
|
815
|
+
icon: icons_path_final + "/layla_6c.svg",
|
|
816
|
+
hotkey:"6",
|
|
817
|
+
caption_optional: true,
|
|
818
|
+
tooltip_core: carbon_ring_tooltip.current
|
|
819
|
+
},
|
|
820
|
+
Arom6: {
|
|
821
|
+
caption:"6-Arom",
|
|
822
|
+
raw_handler:() => switch_tool(new Lhasa.StructureInsertion(Lhasa.LhasaStructure.BenzeneRing)),
|
|
823
|
+
icon: icons_path_final + "/layla_6arom.svg",
|
|
824
|
+
hotkey:["b","alt+6"],
|
|
825
|
+
caption_optional: true,
|
|
826
|
+
tooltip_core: carbon_ring_tooltip.current
|
|
827
|
+
},
|
|
828
|
+
C7: {
|
|
829
|
+
caption:"7-C",
|
|
830
|
+
raw_handler:() => switch_tool(new Lhasa.StructureInsertion(Lhasa.LhasaStructure.CycloHeptaneRing)),
|
|
831
|
+
icon: icons_path_final + "/layla_7c.svg",
|
|
832
|
+
hotkey:"7",
|
|
833
|
+
caption_optional: true,
|
|
834
|
+
tooltip_core: carbon_ring_tooltip.current
|
|
835
|
+
},
|
|
836
|
+
C8: {
|
|
837
|
+
caption:"8-C",
|
|
838
|
+
raw_handler:() => switch_tool(new Lhasa.StructureInsertion(Lhasa.LhasaStructure.CycloOctaneRing)),
|
|
839
|
+
icon: icons_path_final + "/layla_8c.svg",
|
|
840
|
+
hotkey:"8",
|
|
841
|
+
caption_optional: true,
|
|
842
|
+
tooltip_core: carbon_ring_tooltip.current
|
|
843
|
+
},
|
|
844
|
+
C: {
|
|
845
|
+
caption:"C",
|
|
846
|
+
raw_handler:() => switch_tool(new Lhasa.ElementInsertion(Lhasa.LhasaElement.C)),
|
|
847
|
+
icon: null,
|
|
848
|
+
hotkey:"c",
|
|
849
|
+
caption_optional: false,
|
|
850
|
+
tooltip_core: element_tool_tooltip.current
|
|
851
|
+
},
|
|
852
|
+
N: {
|
|
853
|
+
caption:"N",
|
|
854
|
+
raw_handler:() => switch_tool(new Lhasa.ElementInsertion(Lhasa.LhasaElement.N)),
|
|
855
|
+
icon: null,
|
|
856
|
+
hotkey:"n",
|
|
857
|
+
caption_optional: false,
|
|
858
|
+
tooltip_core: element_tool_tooltip.current
|
|
859
|
+
},
|
|
860
|
+
O: {
|
|
861
|
+
caption:"O",
|
|
862
|
+
raw_handler:() => switch_tool(new Lhasa.ElementInsertion(Lhasa.LhasaElement.O)),
|
|
863
|
+
icon: null,
|
|
864
|
+
hotkey:"o",
|
|
865
|
+
caption_optional: false,
|
|
866
|
+
tooltip_core: element_tool_tooltip.current
|
|
867
|
+
},
|
|
868
|
+
S: {
|
|
869
|
+
caption:"S",
|
|
870
|
+
raw_handler:() => switch_tool(new Lhasa.ElementInsertion(Lhasa.LhasaElement.S)),
|
|
871
|
+
icon: null,
|
|
872
|
+
hotkey:"alt+s",
|
|
873
|
+
caption_optional: false,
|
|
874
|
+
tooltip_core: element_tool_tooltip.current
|
|
875
|
+
},
|
|
876
|
+
P: {
|
|
877
|
+
caption:"P",
|
|
878
|
+
raw_handler:() => switch_tool(new Lhasa.ElementInsertion(Lhasa.LhasaElement.P)),
|
|
879
|
+
icon: null,
|
|
880
|
+
hotkey:"p",
|
|
881
|
+
caption_optional: false,
|
|
882
|
+
tooltip_core: element_tool_tooltip.current
|
|
883
|
+
},
|
|
884
|
+
H: {
|
|
885
|
+
caption:"H",
|
|
886
|
+
raw_handler:() => switch_tool(new Lhasa.ElementInsertion(Lhasa.LhasaElement.H)),
|
|
887
|
+
icon: null,
|
|
888
|
+
hotkey:"h",
|
|
889
|
+
caption_optional: false,
|
|
890
|
+
tooltip_core: element_tool_tooltip.current
|
|
891
|
+
},
|
|
892
|
+
F: {
|
|
893
|
+
caption:"F",
|
|
894
|
+
raw_handler:() => switch_tool(new Lhasa.ElementInsertion(Lhasa.LhasaElement.F)),
|
|
895
|
+
icon: null,
|
|
896
|
+
hotkey:"alt+i",
|
|
897
|
+
caption_optional: false,
|
|
898
|
+
tooltip_core: element_tool_tooltip.current
|
|
899
|
+
},
|
|
900
|
+
Cl: {
|
|
901
|
+
caption:"Cl",
|
|
902
|
+
raw_handler:() => switch_tool(new Lhasa.ElementInsertion(Lhasa.LhasaElement.Cl)),
|
|
903
|
+
icon: null,
|
|
904
|
+
hotkey:"alt+c",
|
|
905
|
+
caption_optional: false,
|
|
906
|
+
tooltip_core: element_tool_tooltip.current
|
|
907
|
+
},
|
|
908
|
+
Br: {
|
|
909
|
+
caption:"Br",
|
|
910
|
+
raw_handler:() => switch_tool(new Lhasa.ElementInsertion(Lhasa.LhasaElement.Br)),
|
|
911
|
+
icon: null,
|
|
912
|
+
hotkey:"alt+b",
|
|
913
|
+
caption_optional: false,
|
|
914
|
+
tooltip_core: element_tool_tooltip.current
|
|
915
|
+
},
|
|
916
|
+
I: {
|
|
917
|
+
caption:"I",
|
|
918
|
+
raw_handler:() => switch_tool(new Lhasa.ElementInsertion(Lhasa.LhasaElement.I)),
|
|
919
|
+
icon: null,
|
|
920
|
+
hotkey:"i",
|
|
921
|
+
caption_optional: false,
|
|
922
|
+
tooltip_core: element_tool_tooltip.current
|
|
923
|
+
},
|
|
924
|
+
X: {
|
|
925
|
+
caption:"X",
|
|
926
|
+
raw_handler:() => on_x_element_button(),
|
|
927
|
+
icon: null,
|
|
928
|
+
hotkey:"x",
|
|
929
|
+
caption_optional: false,
|
|
930
|
+
tooltip_core: element_tool_tooltip.current
|
|
931
|
+
}
|
|
932
|
+
}}, [icons_path_final]);
|
|
933
|
+
|
|
934
|
+
function wrap_handler(action_name: string, raw_handler: () => void) : () => void {
|
|
935
|
+
return () => {
|
|
936
|
+
setActiveToolName(action_name);
|
|
937
|
+
raw_handler();
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const handler_map = useMemo(() => {
|
|
942
|
+
let ret = Object.fromEntries(
|
|
943
|
+
Object.entries(tool_button_data)
|
|
944
|
+
.map(([k,v]) => [k, wrap_handler(k,v["raw_handler"])])
|
|
945
|
+
);
|
|
946
|
+
// Those are not tool buttons. We handle them manually.
|
|
947
|
+
ret['Undo'] = () => lh.current?.undo_edition();
|
|
948
|
+
ret['Redo'] = () => lh.current?.redo_edition();
|
|
949
|
+
return ret;
|
|
950
|
+
}, [tool_button_data]);
|
|
951
|
+
|
|
952
|
+
let tool_buttons = useMemo(() => {
|
|
953
|
+
let m_tool_buttons = new Map<string,React.JSX.Element>();
|
|
954
|
+
for(const [k,v] of Object.entries(tool_button_data)) {
|
|
955
|
+
const hotkey_to_infoblock = (hotkey: string | string[]) => {
|
|
956
|
+
let hotkey_arr = typeof hotkey === 'string' ? [hotkey] : hotkey;
|
|
957
|
+
return (<div className='lhasa_tooltip_keybind_infoblock'>
|
|
958
|
+
<b>Hotkeys:</b><br/><br/>
|
|
959
|
+
{hotkey_arr.map((hotkey_text) => <span key={hotkey_text} className='lhasa_tooltip_keybind_info'>
|
|
960
|
+
{hotkey_text}
|
|
961
|
+
</span>)}
|
|
962
|
+
</div>);
|
|
963
|
+
};
|
|
964
|
+
m_tool_buttons.set(k, ToolButton({
|
|
965
|
+
onclick: () => {handler_map[k]()},
|
|
966
|
+
caption: v.caption,
|
|
967
|
+
caption_optional: v.caption_optional,
|
|
968
|
+
tooltip_body: <div className='lhasa_tooltip'>
|
|
969
|
+
{v.tooltip_core}
|
|
970
|
+
{v.hotkey &&
|
|
971
|
+
hotkey_to_infoblock(v.hotkey)
|
|
972
|
+
}
|
|
973
|
+
</div>,
|
|
974
|
+
icon: v.icon,
|
|
975
|
+
action_name: k
|
|
976
|
+
}));
|
|
977
|
+
}
|
|
978
|
+
return m_tool_buttons;
|
|
979
|
+
}, [tool_button_data, handler_map]);
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
let key_map = useMemo(() => {
|
|
983
|
+
let ret = Object.fromEntries(
|
|
984
|
+
Object.entries(tool_button_data)
|
|
985
|
+
.filter(([k,v]) => 'hotkey' in v)
|
|
986
|
+
.map(([k,v]) => [k, v['hotkey']])
|
|
987
|
+
);
|
|
988
|
+
// Those are not tool buttons. We handle them manually.
|
|
989
|
+
ret['Undo'] = 'ctrl+z';
|
|
990
|
+
ret['Redo'] = ['ctrl+r','ctrl+shift+z'];
|
|
991
|
+
return ret;
|
|
992
|
+
}, [tool_button_data]);
|
|
993
|
+
|
|
994
|
+
const editButtonRef = useRef<HTMLButtonElement | null>(null)
|
|
995
|
+
const [editOpened, setEditOpen] = useState<boolean>(false);
|
|
996
|
+
|
|
997
|
+
const optionButtonRef = useRef<HTMLButtonElement | null>(null)
|
|
998
|
+
const [optionOpened, setOptionOpen] = useState<boolean>(false);
|
|
999
|
+
|
|
1000
|
+
const displayModeButtonRef = useRef<HTMLLIElement | null>(null);
|
|
1001
|
+
const [displayModeOpened, setDisplayModeOpen] = useState<boolean>(false);
|
|
1002
|
+
|
|
1003
|
+
const [aimChecked, setAimChecked] = useState<boolean>(() => lh.current?.get_allow_invalid_molecules());
|
|
1004
|
+
|
|
1005
|
+
const [showToolButtonLabels, setShowToolButtonLabels] = useState<boolean>(true);
|
|
1006
|
+
|
|
1007
|
+
const [showQedChecked, setShowQedChecked] = useState<boolean>(false);
|
|
1008
|
+
const [qedTab, setQedTab] = useState<number>(0);
|
|
1009
|
+
|
|
1010
|
+
const [editedSmiles, setEditedSmiles] = useState<number | null>(null);
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
const scale_mapper = (x) => {
|
|
1014
|
+
return theta_const ** (x + d_const) + c_const;
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
const reverse_scale_mapper = (f) => {
|
|
1018
|
+
return Math.log(f - c_const) / Math.log(theta_const) - d_const;
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const getTargetOffset = (touch: React.Touch, event: React.TouchEvent<HTMLDivElement>) => {
|
|
1022
|
+
const boundingRect = (event.target as Element).getBoundingClientRect();
|
|
1023
|
+
return {
|
|
1024
|
+
x: touch.clientX - boundingRect.left,
|
|
1025
|
+
y: touch.clientY - boundingRect.top
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return (
|
|
1030
|
+
<>
|
|
1031
|
+
<ActiveToolContext.Provider value={{active_tool_name: activeToolName, show_optional_captions: showToolButtonLabels}}>
|
|
1032
|
+
<HotKeys keyMap={key_map} handlers={handler_map}>
|
|
1033
|
+
<StyledEngineProvider injectFirst>
|
|
1034
|
+
<div className={"lhasa_editor LhasaMuiStyling" + (dark_mode ? " lhasa_dark_mode" : "")} ref={editorRef}>
|
|
1035
|
+
{show_top_panel &&
|
|
1036
|
+
<div className="horizontal_container">
|
|
1037
|
+
<img src={icons_path_prefix + "/icons/hicolor_apps_scalable_coot-layla.svg"} />
|
|
1038
|
+
<div /*id_="lhasa_hello"*/ >
|
|
1039
|
+
<h3>Welcome to Lhasa!</h3>
|
|
1040
|
+
<p>
|
|
1041
|
+
Lhasa is a WebAssemby port of Layla - Coot's Ligand Editor.<br/>
|
|
1042
|
+
</p>
|
|
1043
|
+
</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
}
|
|
1046
|
+
<div className="horizontal_toolbar">
|
|
1047
|
+
<Button
|
|
1048
|
+
ref={editButtonRef}
|
|
1049
|
+
disableElevation
|
|
1050
|
+
onClick={(_evt) => setEditOpen((prev) => !prev)}
|
|
1051
|
+
>
|
|
1052
|
+
Edit
|
|
1053
|
+
</Button>
|
|
1054
|
+
<Menu
|
|
1055
|
+
open={editOpened}
|
|
1056
|
+
anchorEl={editButtonRef.current}
|
|
1057
|
+
onClose={() => setEditOpen(false)}
|
|
1058
|
+
className={"LhasaMuiStyling" + (dark_mode ? " lhasa_dark_mode" : "")}
|
|
1059
|
+
>
|
|
1060
|
+
<MenuItem onClick={() => handler_map["Undo"]()} >
|
|
1061
|
+
<Undo />
|
|
1062
|
+
Undo <div className="keybind_hint">Ctrl+Z</div>
|
|
1063
|
+
</MenuItem>
|
|
1064
|
+
<MenuItem onClick={() => handler_map["Redo"]()} >
|
|
1065
|
+
<Redo />
|
|
1066
|
+
Redo <div className="keybind_hint">Ctrl+Shift+Z</div>
|
|
1067
|
+
</MenuItem>
|
|
1068
|
+
</Menu>
|
|
1069
|
+
<Button
|
|
1070
|
+
ref={optionButtonRef}
|
|
1071
|
+
disableElevation
|
|
1072
|
+
onClick={(_evt) => setOptionOpen((prev) => !prev)}
|
|
1073
|
+
|
|
1074
|
+
>
|
|
1075
|
+
Options
|
|
1076
|
+
</Button>
|
|
1077
|
+
<Menu
|
|
1078
|
+
open={optionOpened}
|
|
1079
|
+
anchorEl={optionButtonRef.current}
|
|
1080
|
+
onClose={() => setOptionOpen(false)}
|
|
1081
|
+
className={"LhasaMuiStyling" + (dark_mode ? " lhasa_dark_mode" : "")}
|
|
1082
|
+
>
|
|
1083
|
+
<MenuItem>
|
|
1084
|
+
<FormGroup>
|
|
1085
|
+
<FormControlLabel
|
|
1086
|
+
label="Allow Invalid Molecules"
|
|
1087
|
+
control={<Switch />}
|
|
1088
|
+
checked={aimChecked}
|
|
1089
|
+
onChange={(_e) => {
|
|
1090
|
+
const new_val = !lh.current?.get_allow_invalid_molecules();
|
|
1091
|
+
lh.current?.set_allow_invalid_molecules(new_val);
|
|
1092
|
+
setAimChecked(new_val);
|
|
1093
|
+
}}
|
|
1094
|
+
/>
|
|
1095
|
+
<FormControlLabel
|
|
1096
|
+
label="Show QED"
|
|
1097
|
+
control={<Switch />}
|
|
1098
|
+
checked={showQedChecked}
|
|
1099
|
+
onChange={(_e) => {
|
|
1100
|
+
setShowQedChecked(!showQedChecked);
|
|
1101
|
+
}}
|
|
1102
|
+
/>
|
|
1103
|
+
<FormControlLabel
|
|
1104
|
+
label="Show Tool Button Labels"
|
|
1105
|
+
control={<Switch />}
|
|
1106
|
+
checked={showToolButtonLabels}
|
|
1107
|
+
onChange={(_e) => {
|
|
1108
|
+
setShowToolButtonLabels(!showToolButtonLabels);
|
|
1109
|
+
}}
|
|
1110
|
+
/>
|
|
1111
|
+
</FormGroup>
|
|
1112
|
+
</MenuItem>
|
|
1113
|
+
<MenuItem
|
|
1114
|
+
ref={displayModeButtonRef}
|
|
1115
|
+
onClick={(_evt) => setDisplayModeOpen((prev) => !prev)}
|
|
1116
|
+
>
|
|
1117
|
+
Display Mode...
|
|
1118
|
+
</MenuItem>
|
|
1119
|
+
<Popover
|
|
1120
|
+
open={displayModeOpened}
|
|
1121
|
+
anchorEl={displayModeButtonRef.current}
|
|
1122
|
+
anchorOrigin={{horizontal: 'right', vertical: 'top'}}
|
|
1123
|
+
onClose={() => setDisplayModeOpen(false)}
|
|
1124
|
+
className={"LhasaMuiStyling" + (dark_mode ? " lhasa_dark_mode" : "")}
|
|
1125
|
+
// onMouseOut={(_ev) => setDisplayModeAnchorEl(null)}
|
|
1126
|
+
>
|
|
1127
|
+
<FormControl>
|
|
1128
|
+
<RadioGroup
|
|
1129
|
+
name="display_mode"
|
|
1130
|
+
onChange={(_event, value) => switch_display_mode(value)}
|
|
1131
|
+
value={display_mode_to_value_name(lh.current?.get_display_mode())}
|
|
1132
|
+
>
|
|
1133
|
+
<FormControlLabel
|
|
1134
|
+
label="Standard"
|
|
1135
|
+
control={<Radio/>}
|
|
1136
|
+
value="standard"
|
|
1137
|
+
/>
|
|
1138
|
+
<FormControlLabel
|
|
1139
|
+
label="Atom Indices"
|
|
1140
|
+
control={<Radio/>}
|
|
1141
|
+
value="atom_indices"
|
|
1142
|
+
/>
|
|
1143
|
+
<FormControlLabel
|
|
1144
|
+
label="Atom Names"
|
|
1145
|
+
control={<Radio/>}
|
|
1146
|
+
value="atom_names"
|
|
1147
|
+
/>
|
|
1148
|
+
</RadioGroup>
|
|
1149
|
+
</FormControl>
|
|
1150
|
+
</Popover>
|
|
1151
|
+
</Menu>
|
|
1152
|
+
</div>
|
|
1153
|
+
<div /*id_="molecule_tools_toolbar"*/ className="horizontal_toolbar">
|
|
1154
|
+
{ tool_buttons.get("Move") }
|
|
1155
|
+
{ tool_buttons.get("Rotate") }
|
|
1156
|
+
{ tool_buttons.get("Flip_around_X") }
|
|
1157
|
+
{ tool_buttons.get("Flip_around_Y") }
|
|
1158
|
+
{ tool_buttons.get("Delete_hydrogens") }
|
|
1159
|
+
{ tool_buttons.get("Format") }
|
|
1160
|
+
</div>
|
|
1161
|
+
<div /*id_="main_tools_toolbar"*/ className="horizontal_toolbar">
|
|
1162
|
+
{ tool_buttons.get("Single_Bond") }
|
|
1163
|
+
{ tool_buttons.get("Double_Bond") }
|
|
1164
|
+
{ tool_buttons.get("Triple_Bond") }
|
|
1165
|
+
{ tool_buttons.get("Geometry") }
|
|
1166
|
+
{ tool_buttons.get("Charge") }
|
|
1167
|
+
{ tool_buttons.get("Delete") }
|
|
1168
|
+
</div>
|
|
1169
|
+
<div /*id_="structure_toolbar"*/ className="horizontal_toolbar">
|
|
1170
|
+
{ tool_buttons.get("C3") }
|
|
1171
|
+
{ tool_buttons.get("C4") }
|
|
1172
|
+
{ tool_buttons.get("C5") }
|
|
1173
|
+
{ tool_buttons.get("C6") }
|
|
1174
|
+
{ tool_buttons.get("Arom6") }
|
|
1175
|
+
{ tool_buttons.get("C7") }
|
|
1176
|
+
{ tool_buttons.get("C8") }
|
|
1177
|
+
<div className="scale_panel vertical_panel">
|
|
1178
|
+
<div className='horizontal_container'>
|
|
1179
|
+
<b>SCALE</b>
|
|
1180
|
+
<div className="scale_display">
|
|
1181
|
+
{scale.toFixed(2)}
|
|
1182
|
+
</div>
|
|
1183
|
+
</div>
|
|
1184
|
+
<div className="horizontal_panel" style={{border: "0px", padding: "0px"}}>
|
|
1185
|
+
<IconButton
|
|
1186
|
+
onClick={() => {const s = lh.current?.get_scale(); lh.current?.set_scale(s-0.05);}}
|
|
1187
|
+
>
|
|
1188
|
+
<b>-</b>
|
|
1189
|
+
</IconButton>
|
|
1190
|
+
<Slider
|
|
1191
|
+
value={reverse_scale_mapper(lh.current?.get_scale() ?? 1.0)}
|
|
1192
|
+
max={1}
|
|
1193
|
+
min={0}
|
|
1194
|
+
step={0.0001}
|
|
1195
|
+
// marks={[0.5,1,2]}
|
|
1196
|
+
scale={scale_mapper}
|
|
1197
|
+
// valueLabelDisplay="auto"
|
|
1198
|
+
// valueLabelFormat={(v) => v.toFixed(2)}
|
|
1199
|
+
onChange={(_ev, scale)=>{ lh.current?.set_scale(scale_mapper(scale))}}
|
|
1200
|
+
/>
|
|
1201
|
+
<IconButton
|
|
1202
|
+
onClick={() => {const s = lh.current?.get_scale(); lh.current?.set_scale(s+0.05);}}
|
|
1203
|
+
>
|
|
1204
|
+
<b>+</b>
|
|
1205
|
+
</IconButton>
|
|
1206
|
+
</div>
|
|
1207
|
+
</div>
|
|
1208
|
+
</div>
|
|
1209
|
+
{xElementInputShown &&
|
|
1210
|
+
<>
|
|
1211
|
+
<div className="x_element_panel horizontal_panel" >
|
|
1212
|
+
<TextField
|
|
1213
|
+
label="Custom element symbol"
|
|
1214
|
+
id={x_element_symbol_input}
|
|
1215
|
+
variant="outlined"
|
|
1216
|
+
error={x_element_error_string != null}
|
|
1217
|
+
helperText={x_element_error_string}
|
|
1218
|
+
style={{alignSelf: "center", flexGrow: "1"}}
|
|
1219
|
+
/>
|
|
1220
|
+
<Button
|
|
1221
|
+
variant='contained'
|
|
1222
|
+
// className='x_element_submit_button'
|
|
1223
|
+
onClick={() => on_x_element_submit_button()}
|
|
1224
|
+
>
|
|
1225
|
+
Submit
|
|
1226
|
+
</Button>
|
|
1227
|
+
</div>
|
|
1228
|
+
</>
|
|
1229
|
+
}
|
|
1230
|
+
<div /*id_="main_horizontal_container"*/ className="horizontal_panel">
|
|
1231
|
+
<div /*id_="element_toolbar"*/ className="vertical_toolbar">
|
|
1232
|
+
{ tool_buttons.get("C") }
|
|
1233
|
+
{ tool_buttons.get("N") }
|
|
1234
|
+
{ tool_buttons.get("O") }
|
|
1235
|
+
{ tool_buttons.get("S") }
|
|
1236
|
+
{ tool_buttons.get("P") }
|
|
1237
|
+
{ tool_buttons.get("H") }
|
|
1238
|
+
{ tool_buttons.get("F") }
|
|
1239
|
+
{ tool_buttons.get("Cl") }
|
|
1240
|
+
{ tool_buttons.get("Br") }
|
|
1241
|
+
{ tool_buttons.get("I") }
|
|
1242
|
+
{ tool_buttons.get("X") }
|
|
1243
|
+
</div>
|
|
1244
|
+
<div
|
|
1245
|
+
className={"editor_canvas_container" + (dark_mode ? " lhasa_dark_mode" : "")}
|
|
1246
|
+
onContextMenu={(e) => {e.preventDefault();}}
|
|
1247
|
+
onMouseMove={(event) => {
|
|
1248
|
+
// console.log('Mousemove');
|
|
1249
|
+
lh.current?.on_hover(event.nativeEvent.offsetX, event.nativeEvent.offsetY, event.altKey, event.ctrlKey);
|
|
1250
|
+
}}
|
|
1251
|
+
onMouseDown={(event) => {
|
|
1252
|
+
if(event.button == 0) {
|
|
1253
|
+
//console.log('lclick');
|
|
1254
|
+
lh.current?.on_left_click(event.nativeEvent.offsetX, event.nativeEvent.offsetY, event.altKey, event.ctrlKey, event.shiftKey);
|
|
1255
|
+
} else if(event.button == 2) {
|
|
1256
|
+
//console.log('rclick');
|
|
1257
|
+
lh.current?.on_right_click(event.nativeEvent.offsetX, event.nativeEvent.offsetY, event.altKey, event.ctrlKey, event.shiftKey);
|
|
1258
|
+
}
|
|
1259
|
+
}}
|
|
1260
|
+
onMouseUp={(event) => {
|
|
1261
|
+
if(event.button == 0) {
|
|
1262
|
+
//console.log('lreleased');
|
|
1263
|
+
lh.current?.on_left_click_released(event.nativeEvent.offsetX, event.nativeEvent.offsetY, event.altKey, event.ctrlKey, event.shiftKey);
|
|
1264
|
+
} else if(event.button == 2) {
|
|
1265
|
+
//console.log('rreleased');
|
|
1266
|
+
lh.current?.on_right_click_released(event.nativeEvent.offsetX, event.nativeEvent.offsetY, event.altKey, event.ctrlKey, event.shiftKey);
|
|
1267
|
+
}
|
|
1268
|
+
}}
|
|
1269
|
+
onWheel={(event) => {
|
|
1270
|
+
lh.current?.on_scroll(event.deltaX, event.deltaY, event.altKey);
|
|
1271
|
+
}}
|
|
1272
|
+
onTouchStart={(event) => {
|
|
1273
|
+
// console.log('touchstart');
|
|
1274
|
+
if (event.touches.length === 1) {
|
|
1275
|
+
const touch = event.touches[0];
|
|
1276
|
+
const offset = getTargetOffset(touch, event);
|
|
1277
|
+
lh.current?.on_left_click(offset.x, offset.y, event.altKey, event.ctrlKey, event.shiftKey);
|
|
1278
|
+
}
|
|
1279
|
+
}}
|
|
1280
|
+
onTouchEnd={(event) => {
|
|
1281
|
+
// console.log('touchend');
|
|
1282
|
+
if (event.touches.length === 1) {
|
|
1283
|
+
const touch = event.touches[0];
|
|
1284
|
+
const offset = getTargetOffset(touch, event);
|
|
1285
|
+
lh.current?.on_left_click_released(offset.x, offset.y, event.altKey, event.ctrlKey, event.shiftKey);
|
|
1286
|
+
}
|
|
1287
|
+
}}
|
|
1288
|
+
onTouchMove={(event) => {
|
|
1289
|
+
// console.log('touchmove');
|
|
1290
|
+
if (event.touches.length === 1) {
|
|
1291
|
+
const touch = event.touches[0];
|
|
1292
|
+
const offset = getTargetOffset(touch, event);
|
|
1293
|
+
lh.current?.on_hover(offset.x, offset.y, event.altKey, event.ctrlKey);
|
|
1294
|
+
}
|
|
1295
|
+
}}
|
|
1296
|
+
|
|
1297
|
+
ref={svgRef}
|
|
1298
|
+
|
|
1299
|
+
>
|
|
1300
|
+
<div className="pre_render_message">Lhasa not rendered.</div>
|
|
1301
|
+
</div>
|
|
1302
|
+
<div id={text_measurement_worker_div} className="text_measurement_worker_div">
|
|
1303
|
+
{/* Ugly, I know */}
|
|
1304
|
+
</div>
|
|
1305
|
+
</div>
|
|
1306
|
+
<div className="status_display_panel horizontal_panel">
|
|
1307
|
+
<span>▶</span>
|
|
1308
|
+
<span /*id_="status_display"*/>{ statusText }</span>
|
|
1309
|
+
</div>
|
|
1310
|
+
<Accordion>
|
|
1311
|
+
<AccordionSummary>
|
|
1312
|
+
<b>SMILES</b>
|
|
1313
|
+
</AccordionSummary>
|
|
1314
|
+
<AccordionDetails>
|
|
1315
|
+
<div className="smiles_display vertical_panel">
|
|
1316
|
+
{smiles.map((smiles_tuple) => <div key={smiles_tuple[0]} className='horizontal_container'>
|
|
1317
|
+
{smiles_callback && <Button variant="contained" onClick={() => {
|
|
1318
|
+
const lookup_result = canvasIdsToPropsIdsRef.current.get(smiles_tuple[0]);
|
|
1319
|
+
let external_id = null;
|
|
1320
|
+
if(lookup_result !== undefined) {
|
|
1321
|
+
external_id = lookup_result;
|
|
1322
|
+
}
|
|
1323
|
+
smiles_callback(smiles_tuple[0], external_id, smiles_tuple[1])
|
|
1324
|
+
}}>Send to {name_of_host_program}</Button>}
|
|
1325
|
+
{bansu_endpoint &&
|
|
1326
|
+
<BansuButton
|
|
1327
|
+
smiles={smiles_tuple[1]}
|
|
1328
|
+
anchorEl={editorRef.current}
|
|
1329
|
+
bansu_endpoint={bansu_endpoint}
|
|
1330
|
+
dark_mode={dark_mode}
|
|
1331
|
+
/>
|
|
1332
|
+
}
|
|
1333
|
+
<TextField
|
|
1334
|
+
variant="standard"
|
|
1335
|
+
value={editedSmiles !== smiles_tuple[0] ? smiles_tuple[1] : undefined}
|
|
1336
|
+
onFocus={(_event) => setEditedSmiles(smiles_tuple[0])}
|
|
1337
|
+
onBlur={(_event) => setEditedSmiles(null)}
|
|
1338
|
+
onChange={(event) => lh.current.update_molecule_from_smiles(smiles_tuple[0], event.target.value)}
|
|
1339
|
+
/>
|
|
1340
|
+
{smiles_tuple[2] &&
|
|
1341
|
+
<div style={{alignSelf: "center"}}>
|
|
1342
|
+
{smiles_tuple[2][0]} {smiles_tuple[2][1]}
|
|
1343
|
+
</div>
|
|
1344
|
+
}
|
|
1345
|
+
</div>)}
|
|
1346
|
+
</div>
|
|
1347
|
+
{/* <Divider /> */}
|
|
1348
|
+
<div className="horizontal_toolbar">
|
|
1349
|
+
{/* SMILES: */}
|
|
1350
|
+
<TextField
|
|
1351
|
+
label="SMILES"
|
|
1352
|
+
id={smiles_input}
|
|
1353
|
+
variant="outlined"
|
|
1354
|
+
error={smiles_error_string != null}
|
|
1355
|
+
helperText={smiles_error_string}
|
|
1356
|
+
style={{"flexGrow": 1}}
|
|
1357
|
+
/>
|
|
1358
|
+
<Button
|
|
1359
|
+
variant="contained"
|
|
1360
|
+
onClick={() => on_smiles_import_button()}
|
|
1361
|
+
>
|
|
1362
|
+
Import SMILES
|
|
1363
|
+
</Button>
|
|
1364
|
+
</div>
|
|
1365
|
+
</AccordionDetails>
|
|
1366
|
+
</Accordion>
|
|
1367
|
+
{showQedChecked &&
|
|
1368
|
+
<div className="vertical_toolbar qed_panel">
|
|
1369
|
+
<b>QED</b>
|
|
1370
|
+
<Tabs value={qedTab} onChange={(event, value) => setQedTab(value)}>
|
|
1371
|
+
|
|
1372
|
+
{Array.from(qedInfo.keys()).map((mol_id) => {
|
|
1373
|
+
// Counter-intuitively, a "Tab" here is what Gtk considers to be a tab label
|
|
1374
|
+
return <Tab
|
|
1375
|
+
key={mol_id}
|
|
1376
|
+
label={mol_id.toString()}
|
|
1377
|
+
value={mol_id}
|
|
1378
|
+
/>;
|
|
1379
|
+
})}
|
|
1380
|
+
</Tabs>
|
|
1381
|
+
{Array.from(qedInfo.keys()).map((mol_id) => {
|
|
1382
|
+
// This is the proper tab
|
|
1383
|
+
return <div hidden={qedTab !== mol_id} role="tabpanel" key={mol_id}>
|
|
1384
|
+
<div className="horizontal_container">
|
|
1385
|
+
<div className="vertical_panel" style={{flexGrow: 1}}>
|
|
1386
|
+
<QedPropertyInfobox
|
|
1387
|
+
property_name='QED score:'
|
|
1388
|
+
display_value={qedInfo.get(mol_id)?.qed_score.toFixed(4)}
|
|
1389
|
+
progressbar_value={qedInfo.get(mol_id)?.qed_score}
|
|
1390
|
+
/>
|
|
1391
|
+
<QedPropertyInfobox
|
|
1392
|
+
property_name='MW'
|
|
1393
|
+
display_value={qedInfo.get(mol_id)?.molecular_weight.toFixed(4)}
|
|
1394
|
+
progressbar_value={qedInfo.get(mol_id)?.ads_mw}
|
|
1395
|
+
/>
|
|
1396
|
+
<QedPropertyInfobox
|
|
1397
|
+
property_name='PSA'
|
|
1398
|
+
display_value={qedInfo.get(mol_id)?.molecular_polar_surface_area.toFixed(4)}
|
|
1399
|
+
progressbar_value={qedInfo.get(mol_id)?.ads_psa}
|
|
1400
|
+
/>
|
|
1401
|
+
</div>
|
|
1402
|
+
<div className="vertical_panel" style={{flexGrow: 1}}>
|
|
1403
|
+
<QedPropertyInfobox
|
|
1404
|
+
property_name='cLogP'
|
|
1405
|
+
display_value={qedInfo.get(mol_id)?.alogp.toFixed(4)}
|
|
1406
|
+
progressbar_value={qedInfo.get(mol_id)?.ads_alogp}
|
|
1407
|
+
/>
|
|
1408
|
+
<QedPropertyInfobox
|
|
1409
|
+
property_name='#ALERTS'
|
|
1410
|
+
display_value={qedInfo.get(mol_id)?.number_of_alerts}
|
|
1411
|
+
progressbar_value={qedInfo.get(mol_id)?.ads_alert}
|
|
1412
|
+
/>
|
|
1413
|
+
<QedPropertyInfobox
|
|
1414
|
+
property_name='#HBA'
|
|
1415
|
+
display_value={qedInfo.get(mol_id)?.number_of_hydrogen_bond_acceptors}
|
|
1416
|
+
progressbar_value={qedInfo.get(mol_id)?.ads_hba}
|
|
1417
|
+
/>
|
|
1418
|
+
</div>
|
|
1419
|
+
<div className="vertical_panel" style={{flexGrow: 1}}>
|
|
1420
|
+
<QedPropertyInfobox
|
|
1421
|
+
property_name='#HBD'
|
|
1422
|
+
display_value={qedInfo.get(mol_id)?.number_of_hydrogen_bond_donors}
|
|
1423
|
+
progressbar_value={qedInfo.get(mol_id)?.ads_hbd}
|
|
1424
|
+
/>
|
|
1425
|
+
<QedPropertyInfobox
|
|
1426
|
+
property_name='#AROM'
|
|
1427
|
+
display_value={qedInfo.get(mol_id)?.number_of_aromatic_rings}
|
|
1428
|
+
progressbar_value={qedInfo.get(mol_id)?.ads_arom}
|
|
1429
|
+
/>
|
|
1430
|
+
<QedPropertyInfobox
|
|
1431
|
+
property_name='#RotBonds'
|
|
1432
|
+
display_value={qedInfo.get(mol_id)?.number_of_rotatable_bonds}
|
|
1433
|
+
progressbar_value={qedInfo.get(mol_id)?.ads_rotb}
|
|
1434
|
+
/>
|
|
1435
|
+
</div>
|
|
1436
|
+
</div>
|
|
1437
|
+
</div>;
|
|
1438
|
+
})}
|
|
1439
|
+
</div>
|
|
1440
|
+
}
|
|
1441
|
+
{show_footer &&
|
|
1442
|
+
<div className="lhasa_footer">
|
|
1443
|
+
<i>Written by Jakub Smulski</i>
|
|
1444
|
+
</div>
|
|
1445
|
+
}
|
|
1446
|
+
</div>
|
|
1447
|
+
</StyledEngineProvider>
|
|
1448
|
+
</HotKeys>
|
|
1449
|
+
</ActiveToolContext.Provider>
|
|
1450
|
+
</>
|
|
1451
|
+
)
|
|
1452
|
+
}
|