react-cheminfo 0.73.0 → 0.74.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/lib/i18n/core/chromeCatalog.d.ts +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
- package/lib/locales/de.json +22 -0
- package/lib/locales/en.json +22 -0
- package/lib/locales/es.json +22 -0
- package/lib/locales/fr.json +22 -0
- package/lib/locales/it.json +22 -0
- package/lib/structure/core/fragmentIdCode.d.ts +15 -0
- package/lib/structure/core/fragmentIdCode.d.ts.map +1 -0
- package/lib/structure/core/fragmentIdCode.js +25 -0
- package/lib/structure/core/fragmentIdCode.js.map +1 -0
- package/lib/structure/core/index.d.ts +2 -0
- package/lib/structure/core/index.d.ts.map +1 -1
- package/lib/structure/core/index.js +1 -0
- package/lib/structure/core/index.js.map +1 -1
- package/lib/structure/core/structureSearch.d.ts +44 -0
- package/lib/structure/core/structureSearch.d.ts.map +1 -0
- package/lib/structure/core/structureSearch.js +31 -0
- package/lib/structure/core/structureSearch.js.map +1 -0
- package/lib/structure/core/substructureMatch.d.ts +33 -0
- package/lib/structure/core/substructureMatch.d.ts.map +1 -0
- package/lib/structure/core/substructureMatch.js +73 -0
- package/lib/structure/core/substructureMatch.js.map +1 -0
- package/lib/structure/ui/Structure.d.ts +18 -0
- package/lib/structure/ui/Structure.d.ts.map +1 -1
- package/lib/structure/ui/Structure.js +2 -2
- package/lib/structure/ui/Structure.js.map +1 -1
- package/lib/structure/ui/StructureSearchDialog.d.ts +55 -0
- package/lib/structure/ui/StructureSearchDialog.d.ts.map +1 -0
- package/lib/structure/ui/StructureSearchDialog.js +77 -0
- package/lib/structure/ui/StructureSearchDialog.js.map +1 -0
- package/lib/structure/ui/StructureSvg.d.ts +9 -0
- package/lib/structure/ui/StructureSvg.d.ts.map +1 -1
- package/lib/structure/ui/StructureSvg.js +7 -2
- package/lib/structure/ui/StructureSvg.js.map +1 -1
- package/lib/structure/ui/index.d.ts +4 -0
- package/lib/structure/ui/index.d.ts.map +1 -1
- package/lib/structure/ui/index.js +2 -0
- package/lib/structure/ui/index.js.map +1 -1
- package/lib/structure/ui/useStructureSearchModes.d.ts +18 -0
- package/lib/structure/ui/useStructureSearchModes.d.ts.map +1 -0
- package/lib/structure/ui/useStructureSearchModes.js +36 -0
- package/lib/structure/ui/useStructureSearchModes.js.map +1 -0
- package/package.json +1 -1
- package/src/locales/de.json +22 -0
- package/src/locales/en.json +22 -0
- package/src/locales/es.json +22 -0
- package/src/locales/fr.json +22 -0
- package/src/locales/it.json +22 -0
- package/src/structure/core/fragmentIdCode.ts +25 -0
- package/src/structure/core/index.ts +10 -0
- package/src/structure/core/structureSearch.ts +68 -0
- package/src/structure/core/substructureMatch.ts +85 -0
- package/src/structure/ui/Structure.tsx +24 -0
- package/src/structure/ui/StructureSearchDialog.tsx +218 -0
- package/src/structure/ui/StructureSvg.tsx +35 -0
- package/src/structure/ui/index.ts +4 -0
- package/src/structure/ui/useStructureSearchModes.ts +51 -0
- package/styles/chrome.css +31 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** How a drawn structure is matched against a collection. */
|
|
2
|
+
export type StructureSearchMode =
|
|
3
|
+
| 'substructure'
|
|
4
|
+
| 'similarity'
|
|
5
|
+
| 'exact'
|
|
6
|
+
| 'exactNoStereo'
|
|
7
|
+
| 'exactNoStereoTautomer';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Every mode, in the order a dialog offers them: the two broad scans, then
|
|
11
|
+
* exact and its two relaxations.
|
|
12
|
+
*/
|
|
13
|
+
export const STRUCTURE_SEARCH_MODES: readonly StructureSearchMode[] = [
|
|
14
|
+
'substructure',
|
|
15
|
+
'similarity',
|
|
16
|
+
'exact',
|
|
17
|
+
'exactNoStereo',
|
|
18
|
+
'exactNoStereoTautomer',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** A structure question: what is drawn, and how it is matched. */
|
|
22
|
+
export interface StructureSearchQuery {
|
|
23
|
+
/**
|
|
24
|
+
* The drawing as an idCode, with the coordinates the editor laid out written
|
|
25
|
+
* after a space; empty when nothing is drawn.
|
|
26
|
+
*/
|
|
27
|
+
idCode: string;
|
|
28
|
+
/** How the drawing is matched. */
|
|
29
|
+
mode: StructureSearchMode;
|
|
30
|
+
/** The least Tanimoto similarity a hit reaches, from 0 to 1. */
|
|
31
|
+
threshold: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A fragment a drawing can start from. */
|
|
35
|
+
export interface StructureSearchExample {
|
|
36
|
+
/** What the button says. */
|
|
37
|
+
label: string;
|
|
38
|
+
/** The fragment, as a SMILES. */
|
|
39
|
+
smiles: string;
|
|
40
|
+
/**
|
|
41
|
+
* One line on what the fragment finds.
|
|
42
|
+
* @default undefined
|
|
43
|
+
*/
|
|
44
|
+
help?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The least similarities offered, as Tanimoto coefficients, in steps of 5 %.
|
|
49
|
+
*/
|
|
50
|
+
export const SIMILARITY_THRESHOLDS: readonly number[] = [
|
|
51
|
+
0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The thresholds to offer when one is already chosen: the usual steps, plus the
|
|
56
|
+
* chosen one when it is none of them, so a link asking for 0.42 still finds it
|
|
57
|
+
* in the list.
|
|
58
|
+
* @param current - The threshold in force.
|
|
59
|
+
* @returns The thresholds, ascending.
|
|
60
|
+
*/
|
|
61
|
+
export function similarityThresholds(current: number): number[] {
|
|
62
|
+
if (SIMILARITY_THRESHOLDS.includes(current) || !Number.isFinite(current)) {
|
|
63
|
+
return [...SIMILARITY_THRESHOLDS];
|
|
64
|
+
}
|
|
65
|
+
return [...SIMILARITY_THRESHOLDS, current].toSorted(
|
|
66
|
+
(left, right) => left - right,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a query fragment sits in a molecule, as the atoms and bonds to paint.
|
|
3
|
+
*
|
|
4
|
+
* **Not exported from `core/index.ts`**: it value-imports `openchemlib`, so a
|
|
5
|
+
* barrel carrying it would pull the whole library into every site that imports
|
|
6
|
+
* `react-cheminfo/core`. Only the lazily loaded depiction calls it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Molecule, SSSearcher } from 'openchemlib';
|
|
10
|
+
|
|
11
|
+
import { isEmptyIdCode, splitIdCode } from './editorValue.ts';
|
|
12
|
+
|
|
13
|
+
/** The atoms and bonds of a molecule that a query covers. */
|
|
14
|
+
export interface SubstructureMatch {
|
|
15
|
+
/** Atom indices, ascending, as openchemlib counts atoms from 0. */
|
|
16
|
+
atoms: number[];
|
|
17
|
+
/** Bond indices, ascending, as openchemlib counts bonds from 0. */
|
|
18
|
+
bonds: number[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find every match of a query in a molecule, merged into one set of atoms and
|
|
23
|
+
* bonds.
|
|
24
|
+
*
|
|
25
|
+
* Every match is painted, not only the first: a query for a phenyl ring that a
|
|
26
|
+
* molecule carries twice is shown twice. A bond is painted only when it is a
|
|
27
|
+
* bond of the query, so a ring closing between two matched atoms that the
|
|
28
|
+
* query leaves open stays unpainted. The query is read as a fragment whether
|
|
29
|
+
* or not it was drawn as one — a molecule not flagged as a fragment only ever
|
|
30
|
+
* matches itself, which is never what a substructure search asked.
|
|
31
|
+
* @param molecule - The molecule the depiction draws; its atom order is the
|
|
32
|
+
* one the indices refer to.
|
|
33
|
+
* @param query - The query as an openchemlib idCode, coordinates included or
|
|
34
|
+
* not.
|
|
35
|
+
* @returns The covered atoms and bonds, both empty when nothing matches.
|
|
36
|
+
*/
|
|
37
|
+
export function substructureMatch(
|
|
38
|
+
molecule: Molecule,
|
|
39
|
+
query: string,
|
|
40
|
+
): SubstructureMatch {
|
|
41
|
+
if (isEmptyIdCode(query)) return { atoms: [], bonds: [] };
|
|
42
|
+
const fragment = Molecule.fromIDCode(splitIdCode(query).idCode);
|
|
43
|
+
fragment.setFragment(true);
|
|
44
|
+
|
|
45
|
+
const searcher = new SSSearcher();
|
|
46
|
+
searcher.setMol(fragment, molecule);
|
|
47
|
+
if (searcher.findFragmentInMolecule({ countMode: 'overlapping' }) === 0) {
|
|
48
|
+
return { atoms: [], bonds: [] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const atomCovered = new Uint8Array(molecule.getAllAtoms());
|
|
52
|
+
const bondCovered = new Uint8Array(molecule.getAllBonds());
|
|
53
|
+
const fragmentBonds = fragment.getAllBonds();
|
|
54
|
+
for (const match of searcher.getMatchList()) {
|
|
55
|
+
for (const target of match) {
|
|
56
|
+
// An exclude group of the query maps onto nothing.
|
|
57
|
+
if (target >= 0) atomCovered[target] = 1;
|
|
58
|
+
}
|
|
59
|
+
for (let bond = 0; bond < fragmentBonds; bond++) {
|
|
60
|
+
const first = match[fragment.getBondAtom(0, bond)] ?? -1;
|
|
61
|
+
const second = match[fragment.getBondAtom(1, bond)] ?? -1;
|
|
62
|
+
if (first < 0 || second < 0) continue;
|
|
63
|
+
const target = molecule.getBond(first, second);
|
|
64
|
+
if (target >= 0) bondCovered[target] = 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
atoms: coveredIndices(atomCovered),
|
|
70
|
+
bonds: coveredIndices(bondCovered),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The positions of a flag array that are set, in ascending order.
|
|
76
|
+
* @param flags - One flag per atom or bond.
|
|
77
|
+
* @returns The indices whose flag is set.
|
|
78
|
+
*/
|
|
79
|
+
function coveredIndices(flags: Uint8Array): number[] {
|
|
80
|
+
const indices: number[] = [];
|
|
81
|
+
for (let index = 0; index < flags.length; index++) {
|
|
82
|
+
if (flags[index] === 1) indices.push(index);
|
|
83
|
+
}
|
|
84
|
+
return indices;
|
|
85
|
+
}
|
|
@@ -133,6 +133,24 @@ export interface StructureProps {
|
|
|
133
133
|
* @default '#ffd8a8'
|
|
134
134
|
*/
|
|
135
135
|
bondHighlightColor?: string;
|
|
136
|
+
/**
|
|
137
|
+
* A query fragment, as an openchemlib idCode, whose every match is painted —
|
|
138
|
+
* atoms and bonds alike, so the hit reads as one shape. When set, it takes
|
|
139
|
+
* the place of `atomHighlight` and `bondHighlight`.
|
|
140
|
+
* @default undefined
|
|
141
|
+
*/
|
|
142
|
+
substructure?: string;
|
|
143
|
+
/**
|
|
144
|
+
* The colour the `substructure` match is painted.
|
|
145
|
+
* @default '#ffd500'
|
|
146
|
+
*/
|
|
147
|
+
substructureColor?: string;
|
|
148
|
+
/**
|
|
149
|
+
* How opaque the `substructure` match is painted, from 0 to 1. Light, so the
|
|
150
|
+
* bonds and the atom labels under it stay legible.
|
|
151
|
+
* @default 0.15
|
|
152
|
+
*/
|
|
153
|
+
substructureOpacity?: number;
|
|
136
154
|
/**
|
|
137
155
|
* Called with the index of the atom that was clicked, counted from 0 as
|
|
138
156
|
* openchemlib counts atoms.
|
|
@@ -181,6 +199,9 @@ export function Structure(props: StructureProps): ReactElement {
|
|
|
181
199
|
atomHighlightColor = '#a5d8ff',
|
|
182
200
|
bondHighlight,
|
|
183
201
|
bondHighlightColor = '#ffd8a8',
|
|
202
|
+
substructure,
|
|
203
|
+
substructureColor = '#ffd500',
|
|
204
|
+
substructureOpacity = 0.15,
|
|
184
205
|
onAtomClick,
|
|
185
206
|
onBondClick,
|
|
186
207
|
fallback = '—',
|
|
@@ -208,6 +229,9 @@ export function Structure(props: StructureProps): ReactElement {
|
|
|
208
229
|
atomHighlightColor={atomHighlightColor}
|
|
209
230
|
bondHighlight={bondHighlight}
|
|
210
231
|
bondHighlightColor={bondHighlightColor}
|
|
232
|
+
substructure={substructure}
|
|
233
|
+
substructureColor={substructureColor}
|
|
234
|
+
substructureOpacity={substructureOpacity}
|
|
211
235
|
showAtomNumber={labels.atoms ?? false}
|
|
212
236
|
showBondNumber={labels.bonds ?? false}
|
|
213
237
|
showMapping={labels.mapping ?? false}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dialog a structure search is drawn in, shared by every site that
|
|
3
|
+
* filters a collection by structure.
|
|
4
|
+
*
|
|
5
|
+
* This file stays free of `openchemlib`: the editor loads it behind its own
|
|
6
|
+
* `React.lazy`, and an example is read only once it is clicked.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Button,
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogBody,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
HTMLSelect,
|
|
15
|
+
} from '@blueprintjs/core';
|
|
16
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
17
|
+
import { useState } from 'react';
|
|
18
|
+
|
|
19
|
+
import { formatPercent } from '../../format/core/numbers.ts';
|
|
20
|
+
import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
|
|
21
|
+
import { useChromeT } from '../../i18n/ui/useT.ts';
|
|
22
|
+
import { isEmptyIdCode } from '../core/editorValue.ts';
|
|
23
|
+
import type {
|
|
24
|
+
StructureSearchExample,
|
|
25
|
+
StructureSearchMode,
|
|
26
|
+
StructureSearchQuery,
|
|
27
|
+
} from '../core/structureSearch.ts';
|
|
28
|
+
import {
|
|
29
|
+
STRUCTURE_SEARCH_MODES,
|
|
30
|
+
similarityThresholds,
|
|
31
|
+
} from '../core/structureSearch.ts';
|
|
32
|
+
|
|
33
|
+
import { StructureEditor } from './StructureEditor.tsx';
|
|
34
|
+
import { useStructureDraft } from './useStructureDraft.ts';
|
|
35
|
+
import { useStructureSearchModes } from './useStructureSearchModes.ts';
|
|
36
|
+
|
|
37
|
+
/** What {@link StructureSearchDialog} needs. */
|
|
38
|
+
export interface StructureSearchDialogProps {
|
|
39
|
+
/** Whether the dialog is on screen. */
|
|
40
|
+
isOpen: boolean;
|
|
41
|
+
/** The question the dialog opens on; its drawing is loaded into the canvas. */
|
|
42
|
+
value: StructureSearchQuery;
|
|
43
|
+
/**
|
|
44
|
+
* Called when the dialog closes — by Apply, by Escape or by its × alike —
|
|
45
|
+
* with what it then holds, so no drawing is lost to a stray Escape. The
|
|
46
|
+
* idCode is empty when the canvas was cleared.
|
|
47
|
+
*/
|
|
48
|
+
onClose: (query: StructureSearchQuery) => void;
|
|
49
|
+
/**
|
|
50
|
+
* The modes offered, in order.
|
|
51
|
+
* @default STRUCTURE_SEARCH_MODES
|
|
52
|
+
*/
|
|
53
|
+
modes?: readonly StructureSearchMode[];
|
|
54
|
+
/**
|
|
55
|
+
* The fragments offered under the canvas.
|
|
56
|
+
* @default benzene, pyridine, amide, sulfonamide and trifluoromethyl
|
|
57
|
+
*/
|
|
58
|
+
examples?: readonly StructureSearchExample[];
|
|
59
|
+
/**
|
|
60
|
+
* More controls beside the similarity threshold, such as the fingerprint a
|
|
61
|
+
* site compares.
|
|
62
|
+
* @default undefined
|
|
63
|
+
*/
|
|
64
|
+
similarityControls?: ReactNode;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the dialog is rendered through a portal on `document.body`.
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
69
|
+
usePortal?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const DEFAULT_EXAMPLES: ReadonlyArray<{ key: ChromeKey; smiles: string }> = [
|
|
73
|
+
{ key: 'structure.search.exampleBenzene', smiles: 'c1ccccc1' },
|
|
74
|
+
{ key: 'structure.search.examplePyridine', smiles: 'c1ccncc1' },
|
|
75
|
+
{ key: 'structure.search.exampleAmide', smiles: 'CC(=O)N' },
|
|
76
|
+
{ key: 'structure.search.exampleSulfonamide', smiles: 'S(=O)(=O)N' },
|
|
77
|
+
{ key: 'structure.search.exampleTrifluoromethyl', smiles: 'FC(F)F' },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Draw a structure and say how it is matched: contained, similar, or the same
|
|
82
|
+
* compound with or without its stereochemistry and tautomerism.
|
|
83
|
+
*
|
|
84
|
+
* What is drawn stays a draft until the dialog closes, and the canvas is never
|
|
85
|
+
* reloaded under the pen: only opening the dialog and clicking an example put
|
|
86
|
+
* a structure in it.
|
|
87
|
+
* @param props - See {@link StructureSearchDialogProps}.
|
|
88
|
+
* @returns The dialog.
|
|
89
|
+
*/
|
|
90
|
+
export function StructureSearchDialog(
|
|
91
|
+
props: StructureSearchDialogProps,
|
|
92
|
+
): ReactElement {
|
|
93
|
+
const {
|
|
94
|
+
isOpen,
|
|
95
|
+
value,
|
|
96
|
+
onClose,
|
|
97
|
+
modes = STRUCTURE_SEARCH_MODES,
|
|
98
|
+
examples,
|
|
99
|
+
similarityControls,
|
|
100
|
+
usePortal = true,
|
|
101
|
+
} = props;
|
|
102
|
+
const t = useChromeT();
|
|
103
|
+
const modeLabels = useStructureSearchModes(modes);
|
|
104
|
+
const draft = useStructureDraft(value.idCode);
|
|
105
|
+
const [mode, setMode] = useState(value.mode);
|
|
106
|
+
const [threshold, setThreshold] = useState(value.threshold);
|
|
107
|
+
// An example is read by a lazily loaded module: until it is in the canvas,
|
|
108
|
+
// Apply would hand back the drawing it replaces.
|
|
109
|
+
const [loading, setLoading] = useState(false);
|
|
110
|
+
const offered: readonly StructureSearchExample[] =
|
|
111
|
+
examples ??
|
|
112
|
+
DEFAULT_EXAMPLES.map((example) => ({
|
|
113
|
+
label: t(example.key),
|
|
114
|
+
smiles: example.smiles,
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
function close(): void {
|
|
118
|
+
const idCode = isEmptyIdCode(draft.value) ? '' : draft.value;
|
|
119
|
+
onClose({ idCode, mode, threshold });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function loadExample(smiles: string): Promise<void> {
|
|
123
|
+
setLoading(true);
|
|
124
|
+
try {
|
|
125
|
+
const { fragmentIdCode } = await import('../core/fragmentIdCode.ts');
|
|
126
|
+
const idCode = fragmentIdCode(smiles);
|
|
127
|
+
if (idCode !== '') draft.load(idCode);
|
|
128
|
+
} finally {
|
|
129
|
+
setLoading(false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Dialog
|
|
135
|
+
isOpen={isOpen}
|
|
136
|
+
onOpening={() => {
|
|
137
|
+
draft.load(value.idCode);
|
|
138
|
+
setMode(modes.includes(value.mode) ? value.mode : (modes[0] ?? mode));
|
|
139
|
+
setThreshold(value.threshold);
|
|
140
|
+
}}
|
|
141
|
+
onClose={close}
|
|
142
|
+
usePortal={usePortal}
|
|
143
|
+
title={t('structure.search.title')}
|
|
144
|
+
className="structure-search"
|
|
145
|
+
>
|
|
146
|
+
<DialogBody>
|
|
147
|
+
<div className="structure-search__row">
|
|
148
|
+
{modeLabels.map((option) => (
|
|
149
|
+
<Button
|
|
150
|
+
key={option.mode}
|
|
151
|
+
size="small"
|
|
152
|
+
active={mode === option.mode}
|
|
153
|
+
intent={mode === option.mode ? 'primary' : 'none'}
|
|
154
|
+
text={option.label}
|
|
155
|
+
title={option.help}
|
|
156
|
+
onClick={() => setMode(option.mode)}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
{mode === 'similarity' && (
|
|
161
|
+
<div className="structure-search__row">
|
|
162
|
+
<span className="structure-search__label">
|
|
163
|
+
{t('structure.search.similarityLabel')}
|
|
164
|
+
</span>
|
|
165
|
+
<HTMLSelect
|
|
166
|
+
minimal
|
|
167
|
+
value={threshold}
|
|
168
|
+
aria-label={t('structure.search.threshold')}
|
|
169
|
+
title={t('structure.search.thresholdHelp')}
|
|
170
|
+
options={similarityThresholds(threshold).map((option) => ({
|
|
171
|
+
value: option,
|
|
172
|
+
label: t('structure.search.thresholdOption', {
|
|
173
|
+
percent: formatPercent(option, 0),
|
|
174
|
+
}),
|
|
175
|
+
}))}
|
|
176
|
+
onChange={(event) =>
|
|
177
|
+
setThreshold(Number(event.currentTarget.value))
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
{similarityControls}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
<StructureEditor
|
|
184
|
+
fragment
|
|
185
|
+
value={draft.value}
|
|
186
|
+
revision={draft.revision}
|
|
187
|
+
onChange={(change) => draft.update(change.idCode)}
|
|
188
|
+
/>
|
|
189
|
+
<div className="structure-search__row structure-search__examples">
|
|
190
|
+
<span className="structure-search__label">
|
|
191
|
+
{t('structure.search.examples')}
|
|
192
|
+
</span>
|
|
193
|
+
{offered.map((example) => (
|
|
194
|
+
<Button
|
|
195
|
+
key={example.label}
|
|
196
|
+
variant="minimal"
|
|
197
|
+
size="small"
|
|
198
|
+
text={example.label}
|
|
199
|
+
title={example.help}
|
|
200
|
+
onClick={() => void loadExample(example.smiles)}
|
|
201
|
+
/>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
</DialogBody>
|
|
205
|
+
<DialogFooter
|
|
206
|
+
actions={
|
|
207
|
+
<Button
|
|
208
|
+
intent="primary"
|
|
209
|
+
icon="search"
|
|
210
|
+
text={t('structure.search.apply')}
|
|
211
|
+
loading={loading}
|
|
212
|
+
onClick={close}
|
|
213
|
+
/>
|
|
214
|
+
}
|
|
215
|
+
/>
|
|
216
|
+
</Dialog>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -17,6 +17,7 @@ import type { AtomLabelPlacement } from '../core/atomLabels.ts';
|
|
|
17
17
|
import { applyAtomLabels } from '../core/atomLabels.ts';
|
|
18
18
|
import { sourceMolecule } from '../core/sourceMolecule.ts';
|
|
19
19
|
import type { StructureSource } from '../core/structureSource.ts';
|
|
20
|
+
import { substructureMatch } from '../core/substructureMatch.ts';
|
|
20
21
|
|
|
21
22
|
import { StructurePlaceholder } from './StructurePlaceholder.tsx';
|
|
22
23
|
|
|
@@ -46,6 +47,15 @@ interface StructureSvgProps {
|
|
|
46
47
|
bondHighlight?: number[];
|
|
47
48
|
/** The colour the highlighted bonds are painted. */
|
|
48
49
|
bondHighlightColor: string;
|
|
50
|
+
/**
|
|
51
|
+
* A query idCode whose matches are painted in place of the highlights.
|
|
52
|
+
* @default undefined
|
|
53
|
+
*/
|
|
54
|
+
substructure?: string;
|
|
55
|
+
/** The colour the substructure match is painted. */
|
|
56
|
+
substructureColor: string;
|
|
57
|
+
/** How opaque the substructure match is painted, from 0 to 1. */
|
|
58
|
+
substructureOpacity: number;
|
|
49
59
|
/** Write the index of every atom next to it. */
|
|
50
60
|
showAtomNumber: boolean;
|
|
51
61
|
/** Write the index of every bond next to it. */
|
|
@@ -94,6 +104,13 @@ export function StructureSvg(props: StructureSvgProps): ReactElement {
|
|
|
94
104
|
onAtomClick,
|
|
95
105
|
onBondClick,
|
|
96
106
|
showCIPParity,
|
|
107
|
+
atomHighlight,
|
|
108
|
+
atomHighlightColor,
|
|
109
|
+
bondHighlight,
|
|
110
|
+
bondHighlightColor,
|
|
111
|
+
substructure,
|
|
112
|
+
substructureColor,
|
|
113
|
+
substructureOpacity,
|
|
97
114
|
...rest
|
|
98
115
|
} = props;
|
|
99
116
|
const { kind, value, coordinates } = source;
|
|
@@ -106,6 +123,14 @@ export function StructureSvg(props: StructureSvgProps): ReactElement {
|
|
|
106
123
|
() => readDrawing(kind, value, coordinates, labelsKey, atomLabelPlacement),
|
|
107
124
|
[kind, value, coordinates, labelsKey, atomLabelPlacement],
|
|
108
125
|
);
|
|
126
|
+
// Matched on the very molecule drawn, so the indices are the drawn atoms.
|
|
127
|
+
const match = useMemo(
|
|
128
|
+
() =>
|
|
129
|
+
drawing === null || substructure === undefined
|
|
130
|
+
? null
|
|
131
|
+
: substructureMatch(drawing.molecule, substructure),
|
|
132
|
+
[drawing, substructure],
|
|
133
|
+
);
|
|
109
134
|
|
|
110
135
|
if (drawing === null) {
|
|
111
136
|
return (
|
|
@@ -118,6 +143,16 @@ export function StructureSvg(props: StructureSvgProps): ReactElement {
|
|
|
118
143
|
return (
|
|
119
144
|
<SvgRenderer
|
|
120
145
|
{...rest}
|
|
146
|
+
atomHighlight={match === null ? atomHighlight : match.atoms}
|
|
147
|
+
atomHighlightColor={
|
|
148
|
+
match === null ? atomHighlightColor : substructureColor
|
|
149
|
+
}
|
|
150
|
+
bondHighlight={match === null ? bondHighlight : match.bonds}
|
|
151
|
+
bondHighlightColor={
|
|
152
|
+
match === null ? bondHighlightColor : substructureColor
|
|
153
|
+
}
|
|
154
|
+
atomHighlightOpacity={match === null ? undefined : substructureOpacity}
|
|
155
|
+
bondHighlightOpacity={match === null ? undefined : substructureOpacity}
|
|
121
156
|
molecule={drawing.molecule}
|
|
122
157
|
noCarbonLabelWithCustomLabel={drawing.labelled}
|
|
123
158
|
suppressCIPParity={!showCIPParity}
|
|
@@ -25,6 +25,10 @@ export type { StructureDropProps } from './StructureDrop.tsx';
|
|
|
25
25
|
export { StructureDrop } from './StructureDrop.tsx';
|
|
26
26
|
export type { StructureEditorHelpProps } from './StructureEditorHelp.tsx';
|
|
27
27
|
export { StructureEditorHelp } from './StructureEditorHelp.tsx';
|
|
28
|
+
export type { StructureSearchDialogProps } from './StructureSearchDialog.tsx';
|
|
29
|
+
export { StructureSearchDialog } from './StructureSearchDialog.tsx';
|
|
30
|
+
export type { StructureSearchModeLabel } from './useStructureSearchModes.ts';
|
|
31
|
+
export { useStructureSearchModes } from './useStructureSearchModes.ts';
|
|
28
32
|
export type { StructureExportDialogProps } from './StructureExportDialog.tsx';
|
|
29
33
|
export { StructureExportDialog } from './StructureExportDialog.tsx';
|
|
30
34
|
export type { StructureExportButtonProps } from './StructureExportButton.tsx';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
|
|
2
|
+
import { useChromeT } from '../../i18n/ui/useT.ts';
|
|
3
|
+
import type { StructureSearchMode } from '../core/structureSearch.ts';
|
|
4
|
+
import { STRUCTURE_SEARCH_MODES } from '../core/structureSearch.ts';
|
|
5
|
+
|
|
6
|
+
/** A mode, with what a button says and what its tooltip adds. */
|
|
7
|
+
export interface StructureSearchModeLabel {
|
|
8
|
+
/** The mode. */
|
|
9
|
+
mode: StructureSearchMode;
|
|
10
|
+
/** What the button says. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** One line on what the mode finds. */
|
|
13
|
+
help: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MODE_KEYS: Record<StructureSearchMode, [ChromeKey, ChromeKey]> = {
|
|
17
|
+
substructure: [
|
|
18
|
+
'structure.search.substructure',
|
|
19
|
+
'structure.search.substructureHelp',
|
|
20
|
+
],
|
|
21
|
+
similarity: [
|
|
22
|
+
'structure.search.similarity',
|
|
23
|
+
'structure.search.similarityHelp',
|
|
24
|
+
],
|
|
25
|
+
exact: ['structure.search.exact', 'structure.search.exactHelp'],
|
|
26
|
+
exactNoStereo: [
|
|
27
|
+
'structure.search.exactNoStereo',
|
|
28
|
+
'structure.search.exactNoStereoHelp',
|
|
29
|
+
],
|
|
30
|
+
exactNoStereoTautomer: [
|
|
31
|
+
'structure.search.exactNoStereoTautomer',
|
|
32
|
+
'structure.search.exactNoStereoTautomerHelp',
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The modes in the reader's language, for a site that shows the mode in force
|
|
38
|
+
* beside the drawing.
|
|
39
|
+
* @param modes - The modes, in order.
|
|
40
|
+
* @returns Each mode with its label and its one-line help.
|
|
41
|
+
*/
|
|
42
|
+
export function useStructureSearchModes(
|
|
43
|
+
modes: readonly StructureSearchMode[] = STRUCTURE_SEARCH_MODES,
|
|
44
|
+
): StructureSearchModeLabel[] {
|
|
45
|
+
const t = useChromeT();
|
|
46
|
+
return modes.map((mode) => ({
|
|
47
|
+
mode,
|
|
48
|
+
label: t(MODE_KEYS[mode][0]),
|
|
49
|
+
help: t(MODE_KEYS[mode][1]),
|
|
50
|
+
}));
|
|
51
|
+
}
|
package/styles/chrome.css
CHANGED
|
@@ -889,6 +889,37 @@ a.source-badge:hover {
|
|
|
889
889
|
}
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
+
/* The structure search dialog: the modes above the canvas, the fragments to
|
|
893
|
+
start from under it. A drag on the canvas that leaves it would otherwise
|
|
894
|
+
select the labels. */
|
|
895
|
+
.structure-search {
|
|
896
|
+
width: min(56rem, calc(100vw - 2rem));
|
|
897
|
+
user-select: none;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.structure-search :is(input, textarea) {
|
|
901
|
+
user-select: text;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.structure-search__row {
|
|
905
|
+
display: flex;
|
|
906
|
+
flex-wrap: wrap;
|
|
907
|
+
align-items: center;
|
|
908
|
+
margin-bottom: 0.6rem;
|
|
909
|
+
gap: 0.35rem;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.structure-search__examples {
|
|
913
|
+
margin-top: 0.6rem;
|
|
914
|
+
margin-bottom: 0;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.structure-search__label {
|
|
918
|
+
margin-right: 0.25rem;
|
|
919
|
+
color: var(--text-muted);
|
|
920
|
+
font-size: 0.8125rem;
|
|
921
|
+
}
|
|
922
|
+
|
|
892
923
|
/* ── Tokens for molecules.cheminfo.org ─────────────────────────────────────── */
|
|
893
924
|
|
|
894
925
|
/* The heading, when the tokens were checked, and the button asking again, on
|