react-cheminfo 0.73.0 → 0.75.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.
Files changed (109) hide show
  1. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  2. package/lib/ecosystem/core/sites.js +26 -0
  3. package/lib/ecosystem/core/sites.js.map +1 -1
  4. package/lib/ecosystem/core/types.d.ts +1 -1
  5. package/lib/ecosystem/core/types.d.ts.map +1 -1
  6. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  7. package/lib/ecosystem/ui/glyphs.js +7 -0
  8. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  9. package/lib/formula/core/elementRanges.d.ts +25 -0
  10. package/lib/formula/core/elementRanges.d.ts.map +1 -0
  11. package/lib/formula/core/elementRanges.js +35 -0
  12. package/lib/formula/core/elementRanges.js.map +1 -0
  13. package/lib/formula/core/formulaCandidates.d.ts +59 -0
  14. package/lib/formula/core/formulaCandidates.d.ts.map +1 -0
  15. package/lib/formula/core/formulaCandidates.js +50 -0
  16. package/lib/formula/core/formulaCandidates.js.map +1 -0
  17. package/lib/formula/core/index.d.ts +4 -0
  18. package/lib/formula/core/index.d.ts.map +1 -1
  19. package/lib/formula/core/index.js +2 -0
  20. package/lib/formula/core/index.js.map +1 -1
  21. package/lib/formula/ui/ElementRangeFields.d.ts +19 -0
  22. package/lib/formula/ui/ElementRangeFields.d.ts.map +1 -0
  23. package/lib/formula/ui/ElementRangeFields.js +23 -0
  24. package/lib/formula/ui/ElementRangeFields.js.map +1 -0
  25. package/lib/formula/ui/FormulaLookup.d.ts +47 -0
  26. package/lib/formula/ui/FormulaLookup.d.ts.map +1 -0
  27. package/lib/formula/ui/FormulaLookup.js +74 -0
  28. package/lib/formula/ui/FormulaLookup.js.map +1 -0
  29. package/lib/formula/ui/FormulaLookupResults.d.ts +20 -0
  30. package/lib/formula/ui/FormulaLookupResults.d.ts.map +1 -0
  31. package/lib/formula/ui/FormulaLookupResults.js +45 -0
  32. package/lib/formula/ui/FormulaLookupResults.js.map +1 -0
  33. package/lib/formula/ui/index.d.ts +2 -0
  34. package/lib/formula/ui/index.d.ts.map +1 -1
  35. package/lib/formula/ui/index.js +1 -0
  36. package/lib/formula/ui/index.js.map +1 -1
  37. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  38. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  39. package/lib/locales/de.json +38 -0
  40. package/lib/locales/en.json +38 -0
  41. package/lib/locales/es.json +38 -0
  42. package/lib/locales/fr.json +38 -0
  43. package/lib/locales/it.json +38 -0
  44. package/lib/structure/core/fragmentIdCode.d.ts +15 -0
  45. package/lib/structure/core/fragmentIdCode.d.ts.map +1 -0
  46. package/lib/structure/core/fragmentIdCode.js +25 -0
  47. package/lib/structure/core/fragmentIdCode.js.map +1 -0
  48. package/lib/structure/core/index.d.ts +2 -0
  49. package/lib/structure/core/index.d.ts.map +1 -1
  50. package/lib/structure/core/index.js +1 -0
  51. package/lib/structure/core/index.js.map +1 -1
  52. package/lib/structure/core/structureSearch.d.ts +44 -0
  53. package/lib/structure/core/structureSearch.d.ts.map +1 -0
  54. package/lib/structure/core/structureSearch.js +31 -0
  55. package/lib/structure/core/structureSearch.js.map +1 -0
  56. package/lib/structure/core/substructureMatch.d.ts +33 -0
  57. package/lib/structure/core/substructureMatch.d.ts.map +1 -0
  58. package/lib/structure/core/substructureMatch.js +73 -0
  59. package/lib/structure/core/substructureMatch.js.map +1 -0
  60. package/lib/structure/ui/Structure.d.ts +18 -0
  61. package/lib/structure/ui/Structure.d.ts.map +1 -1
  62. package/lib/structure/ui/Structure.js +2 -2
  63. package/lib/structure/ui/Structure.js.map +1 -1
  64. package/lib/structure/ui/StructureSearchDialog.d.ts +55 -0
  65. package/lib/structure/ui/StructureSearchDialog.d.ts.map +1 -0
  66. package/lib/structure/ui/StructureSearchDialog.js +77 -0
  67. package/lib/structure/ui/StructureSearchDialog.js.map +1 -0
  68. package/lib/structure/ui/StructureSvg.d.ts +9 -0
  69. package/lib/structure/ui/StructureSvg.d.ts.map +1 -1
  70. package/lib/structure/ui/StructureSvg.js +7 -2
  71. package/lib/structure/ui/StructureSvg.js.map +1 -1
  72. package/lib/structure/ui/index.d.ts +4 -0
  73. package/lib/structure/ui/index.d.ts.map +1 -1
  74. package/lib/structure/ui/index.js +2 -0
  75. package/lib/structure/ui/index.js.map +1 -1
  76. package/lib/structure/ui/useStructureSearchModes.d.ts +18 -0
  77. package/lib/structure/ui/useStructureSearchModes.d.ts.map +1 -0
  78. package/lib/structure/ui/useStructureSearchModes.js +36 -0
  79. package/lib/structure/ui/useStructureSearchModes.js.map +1 -0
  80. package/lib/structure/ui/useToolbarFloor.d.ts.map +1 -1
  81. package/lib/structure/ui/useToolbarFloor.js +20 -10
  82. package/lib/structure/ui/useToolbarFloor.js.map +1 -1
  83. package/package.json +4 -3
  84. package/src/ecosystem/core/sites.ts +26 -0
  85. package/src/ecosystem/core/types.ts +3 -1
  86. package/src/ecosystem/ui/glyphs.tsx +46 -0
  87. package/src/formula/core/elementRanges.ts +44 -0
  88. package/src/formula/core/formulaCandidates.ts +109 -0
  89. package/src/formula/core/index.ts +15 -0
  90. package/src/formula/ui/ElementRangeFields.tsx +65 -0
  91. package/src/formula/ui/FormulaLookup.tsx +184 -0
  92. package/src/formula/ui/FormulaLookupResults.tsx +89 -0
  93. package/src/formula/ui/index.ts +2 -0
  94. package/src/locales/de.json +38 -0
  95. package/src/locales/en.json +38 -0
  96. package/src/locales/es.json +38 -0
  97. package/src/locales/fr.json +38 -0
  98. package/src/locales/it.json +38 -0
  99. package/src/structure/core/fragmentIdCode.ts +25 -0
  100. package/src/structure/core/index.ts +10 -0
  101. package/src/structure/core/structureSearch.ts +68 -0
  102. package/src/structure/core/substructureMatch.ts +85 -0
  103. package/src/structure/ui/Structure.tsx +24 -0
  104. package/src/structure/ui/StructureSearchDialog.tsx +218 -0
  105. package/src/structure/ui/StructureSvg.tsx +35 -0
  106. package/src/structure/ui/index.ts +4 -0
  107. package/src/structure/ui/useStructureSearchModes.ts +51 -0
  108. package/src/structure/ui/useToolbarFloor.ts +19 -9
  109. package/styles/chrome.css +164 -0
@@ -0,0 +1,89 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ import { formatDecimal, formatInteger } from '../../format/core/numbers.ts';
4
+ import type { AsyncResult } from '../../hooks/ui/useAsync.ts';
5
+ import { useChromeT } from '../../i18n/ui/useT.ts';
6
+ import type { FormulaCandidates } from '../core/formulaCandidates.ts';
7
+
8
+ import { CopyableFormula } from './CopyableFormula.tsx';
9
+
10
+ /** What {@link FormulaLookupResults} lists. */
11
+ export interface FormulaLookupResultsProps {
12
+ /** The mass in the box, `undefined` while it is empty. */
13
+ mass: number | undefined;
14
+ /** The latest search: its formulas, or `null` when it gave up. */
15
+ answer: AsyncResult<FormulaCandidates | null>;
16
+ /** Whether a newer search is still to come, so an empty answer is stale. */
17
+ waiting: boolean;
18
+ }
19
+
20
+ /**
21
+ * The formulas a lookup found, closest first, each with its distance in ppm
22
+ * and copied by a click; or the sentence saying why there are none.
23
+ * @param props - See {@link FormulaLookupResultsProps}.
24
+ * @returns The results.
25
+ */
26
+ export function FormulaLookupResults(
27
+ props: FormulaLookupResultsProps,
28
+ ): ReactElement {
29
+ const { mass, answer, waiting } = props;
30
+ const t = useChromeT();
31
+ const found = answer.data;
32
+
33
+ if (mass === undefined) return hintOnly(t('formula.lookup.typeMass'));
34
+ if (found === null) return hintOnly(t('formula.lookup.tooWide'));
35
+ // A search still to come is not "nothing fits": the reader is mid-number.
36
+ if (found === undefined || (found.candidates.length === 0 && waiting)) {
37
+ return <div className="formula-lookup__results" />;
38
+ }
39
+
40
+ const { candidates, total } = found;
41
+ const count = formatInteger(candidates.length);
42
+ let heading: string = t('formula.lookup.countMany', { count });
43
+ if (candidates.length < total) {
44
+ heading = t('formula.lookup.closest', {
45
+ count,
46
+ total: formatInteger(total),
47
+ });
48
+ } else if (candidates.length === 1) {
49
+ heading = t('formula.lookup.countOne', { count });
50
+ }
51
+
52
+ return (
53
+ <div className="formula-lookup__results">
54
+ <span className="formula-lookup__count">{heading}</span>
55
+ {candidates.length === 0 ? (
56
+ <span className="formula-lookup__hint">
57
+ {t('formula.lookup.nothingFits')}
58
+ </span>
59
+ ) : (
60
+ <ul className="formula-lookup__list">
61
+ {candidates.map((candidate) => (
62
+ <li
63
+ key={candidate.mf}
64
+ title={t('formula.lookup.detail', {
65
+ mass: formatDecimal(candidate.em, 4),
66
+ unsaturation: formatInteger(candidate.unsaturation),
67
+ })}
68
+ >
69
+ <CopyableFormula formula={candidate.mf} />
70
+ <span className="formula-lookup__ppm">
71
+ {t('formula.lookup.ppm', {
72
+ value: formatDecimal(candidate.ppm, 1),
73
+ })}
74
+ </span>
75
+ </li>
76
+ ))}
77
+ </ul>
78
+ )}
79
+ </div>
80
+ );
81
+ }
82
+
83
+ function hintOnly(hint: string): ReactElement {
84
+ return (
85
+ <div className="formula-lookup__results">
86
+ <span className="formula-lookup__hint">{hint}</span>
87
+ </div>
88
+ );
89
+ }
@@ -2,5 +2,7 @@ export type { CopyableFormulaProps } from './CopyableFormula.tsx';
2
2
  export { CopyableFormula } from './CopyableFormula.tsx';
3
3
  export type { FormulaInputProps } from './FormulaInput.tsx';
4
4
  export { FormulaInput } from './FormulaInput.tsx';
5
+ export type { FormulaLookupProps } from './FormulaLookup.tsx';
6
+ export { FormulaLookup } from './FormulaLookup.tsx';
5
7
  export type { QuestionFormulaProps } from './QuestionFormula.tsx';
6
8
  export { QuestionFormula } from './QuestionFormula.tsx';
@@ -103,6 +103,7 @@
103
103
  "site.regexp.tagline": "Reguläre Ausdrücke in einer interaktiven Spielwiese lernen.",
104
104
  "site.pdb.tagline": "Ein schneller Blick auf jeden Eintrag der Protein Data Bank.",
105
105
  "site.elucidation.tagline": "Eine Struktur aus einem 1H-NMR-Spektrum und einer Formel.",
106
+ "site.spectroscopy.tagline": "Eine Struktur aus ihren IR-, Massen- und NMR-Spektren bestimmen.",
106
107
  "site.equilibrium.tagline": "Chemische Gleichgewichte: pH, Speziation und Titrationskurven.",
107
108
  "site.polycarp.tagline": "Die Mikrostruktur einer radikalischen Copolymerisation vorhersagen.",
108
109
  "site.3d.tagline": "Konformere in 3D aus einer Struktur, die Sie zeichnen.",
@@ -345,6 +346,21 @@
345
346
  "formula.wholeCounts": "„{formula}“ gibt einem Atom die Anzahl {count}; Anzahlen sind ganze Zahlen.",
346
347
  "formula.unknownNotation": "„{formula}“ verwendet eine Schreibweise, die dieses Werkzeug nicht zählt.",
347
348
  "formula.unreadable": "„{formula}“ ist keine Formel, die dieses Werkzeug lesen kann ({reason}).",
349
+ "formula.lookup.title": "Formelsuche",
350
+ "formula.lookup.reset": "Masse und Elementbereiche zurücksetzen",
351
+ "formula.lookup.mass": "Monoisotopische Masse",
352
+ "formula.lookup.massPlaceholder": "z. B. 128.0626",
353
+ "formula.lookup.neutral": "Die des neutralen Moleküls: Ziehen Sie die Protonen ab, die das Spektrum hinzugefügt hat.",
354
+ "formula.lookup.fewest": "Mindestzahl der {element}-Atome",
355
+ "formula.lookup.most": "Höchstzahl der {element}-Atome",
356
+ "formula.lookup.typeMass": "Geben Sie eine Masse ein, um die Formeln aufzulisten, die diese Bereiche zulassen.",
357
+ "formula.lookup.closest": "Die {count} nächsten von {total} Formeln",
358
+ "formula.lookup.countOne": "{count} Formel",
359
+ "formula.lookup.countMany": "{count} Formeln",
360
+ "formula.lookup.nothingFits": "Keine Formel passt. Erweitern Sie einen Bereich oder prüfen Sie die Masse.",
361
+ "formula.lookup.tooWide": "Diese Bereiche lassen zu viele Formeln für die Suche zu. Schränken Sie einen ein.",
362
+ "formula.lookup.ppm": "{value} ppm",
363
+ "formula.lookup.detail": "{mass} Da, Unsättigungsgrad {unsaturation}",
348
364
  "slides.exit": "Beenden",
349
365
  "slides.previousSlide": "Vorige Folie",
350
366
  "slides.nextSlide": "Nächste Folie",
@@ -583,6 +599,28 @@
583
599
  "structure.export.noStereoIdCodeHelp": "Jedes Stereoisomer ergibt diese, womit ein ohne Stereozentren gezeichneter Eintrag gefunden wird.",
584
600
  "structure.export.noStereoTautomerIdCode": "idCode ohne Stereochemie und Tautomerie",
585
601
  "structure.export.noStereoTautomerIdCodeHelp": "Jedes Tautomer und jedes Stereoisomer ergibt diese: Keto- und Enolform haben denselben Schlüssel.",
602
+ "structure.search.title": "Nach Struktur filtern",
603
+ "structure.search.apply": "Anwenden",
604
+ "structure.search.examples": "Ausgehend von",
605
+ "structure.search.substructure": "Enthält",
606
+ "structure.search.substructureHelp": "Jedes Molekül, das die Zeichnung enthält.",
607
+ "structure.search.similarity": "Ähnlich zu",
608
+ "structure.search.similarityHelp": "Geordnet danach, wie viel der Zeichnung sie teilen.",
609
+ "structure.search.exact": "Ist genau",
610
+ "structure.search.exactHelp": "Genau das gezeichnete Molekül.",
611
+ "structure.search.exactNoStereo": "Beliebiges Stereoisomer",
612
+ "structure.search.exactNoStereoHelp": "Dieselbe Verbindung, wie auch immer ihre Stereochemie gezeichnet ist.",
613
+ "structure.search.exactNoStereoTautomer": "Beliebiges Tautomer",
614
+ "structure.search.exactNoStereoTautomerHelp": "Dieselbe Verbindung in jeder tautomeren Form, ohne ihre Stereochemie.",
615
+ "structure.search.similarityLabel": "Ähnlichkeit",
616
+ "structure.search.threshold": "Mindestähnlichkeit",
617
+ "structure.search.thresholdHelp": "Der kleinste Tanimoto-Koeffizient, den ein Treffer mit der Zeichnung erreicht.",
618
+ "structure.search.thresholdOption": "≥ {percent}",
619
+ "structure.search.exampleBenzene": "Benzol",
620
+ "structure.search.examplePyridine": "Pyridin",
621
+ "structure.search.exampleAmide": "Amid",
622
+ "structure.search.exampleSulfonamide": "Sulfonamid",
623
+ "structure.search.exampleTrifluoromethyl": "Trifluormethyl",
586
624
  "molecule3d.view": "Ansicht",
587
625
  "molecule3d.resetView": "Ansicht zurücksetzen",
588
626
  "molecule3d.spin": "Drehen lassen",
@@ -103,6 +103,7 @@
103
103
  "site.regexp.tagline": "Learn regular expressions in a live playground.",
104
104
  "site.pdb.tagline": "A fast look at any Protein Data Bank entry.",
105
105
  "site.elucidation.tagline": "A structure from a 1H NMR spectrum and a formula.",
106
+ "site.spectroscopy.tagline": "Find a structure from its IR, mass and NMR spectra.",
106
107
  "site.equilibrium.tagline": "Chemical equilibria: pH, speciation and titration curves.",
107
108
  "site.polycarp.tagline": "Predict the microstructure of a radical copolymerisation.",
108
109
  "site.3d.tagline": "Conformers in 3D from a structure you draw.",
@@ -345,6 +346,21 @@
345
346
  "formula.wholeCounts": "“{formula}” gives an atom a count of {count}; counts are whole numbers.",
346
347
  "formula.unknownNotation": "“{formula}” uses a notation this tool does not count.",
347
348
  "formula.unreadable": "“{formula}” is not a formula this tool can read ({reason}).",
349
+ "formula.lookup.title": "Formula lookup",
350
+ "formula.lookup.reset": "Reset the mass and the element ranges",
351
+ "formula.lookup.mass": "Monoisotopic mass",
352
+ "formula.lookup.massPlaceholder": "e.g. 128.0626",
353
+ "formula.lookup.neutral": "Of the neutral molecule: take the protons the spectrum added back off.",
354
+ "formula.lookup.fewest": "Fewest {element} atoms",
355
+ "formula.lookup.most": "Most {element} atoms",
356
+ "formula.lookup.typeMass": "Type a mass to list the formulas these ranges allow.",
357
+ "formula.lookup.closest": "The {count} closest of {total} formulas",
358
+ "formula.lookup.countOne": "{count} formula",
359
+ "formula.lookup.countMany": "{count} formulas",
360
+ "formula.lookup.nothingFits": "Nothing fits. Widen a range, or check the mass.",
361
+ "formula.lookup.tooWide": "These ranges allow too many formulas to search. Narrow one.",
362
+ "formula.lookup.ppm": "{value} ppm",
363
+ "formula.lookup.detail": "{mass} Da, unsaturation {unsaturation}",
348
364
  "slides.exit": "Exit",
349
365
  "slides.previousSlide": "Previous slide",
350
366
  "slides.nextSlide": "Next slide",
@@ -527,6 +543,28 @@
527
543
  "structure.export.noStereoIdCodeHelp": "Every stereoisomer of this structure gives this one, so it finds a record drawn without its stereocentres.",
528
544
  "structure.export.noStereoTautomerIdCode": "idCode without stereochemistry or tautomer",
529
545
  "structure.export.noStereoTautomerIdCodeHelp": "Every tautomer and every stereoisomer gives this one, so the keto and the enol form key together.",
546
+ "structure.search.title": "Filter by structure",
547
+ "structure.search.apply": "Apply",
548
+ "structure.search.examples": "Start from",
549
+ "structure.search.substructure": "Contains",
550
+ "structure.search.substructureHelp": "Every molecule the drawing is part of.",
551
+ "structure.search.similarity": "Similar to",
552
+ "structure.search.similarityHelp": "Ranked by how much of the drawing they share.",
553
+ "structure.search.exact": "Is exactly",
554
+ "structure.search.exactHelp": "The one molecule drawn.",
555
+ "structure.search.exactNoStereo": "Any stereoisomer",
556
+ "structure.search.exactNoStereoHelp": "The same compound however its stereochemistry is drawn.",
557
+ "structure.search.exactNoStereoTautomer": "Any tautomer",
558
+ "structure.search.exactNoStereoTautomerHelp": "The same compound in any tautomeric form, its stereochemistry ignored.",
559
+ "structure.search.similarityLabel": "Similarity",
560
+ "structure.search.threshold": "Minimum similarity",
561
+ "structure.search.thresholdHelp": "The least Tanimoto coefficient a hit reaches with the drawing.",
562
+ "structure.search.thresholdOption": "≥ {percent}",
563
+ "structure.search.exampleBenzene": "Benzene",
564
+ "structure.search.examplePyridine": "Pyridine",
565
+ "structure.search.exampleAmide": "Amide",
566
+ "structure.search.exampleSulfonamide": "Sulfonamide",
567
+ "structure.search.exampleTrifluoromethyl": "Trifluoromethyl",
530
568
  "periodic.table": "Periodic table",
531
569
  "periodic.group": "Group {group}",
532
570
  "periodic.period": "Period {period}",
@@ -103,6 +103,7 @@
103
103
  "site.regexp.tagline": "Aprender expresiones regulares en un entorno interactivo.",
104
104
  "site.pdb.tagline": "Una mirada rápida a cualquier entrada del Protein Data Bank.",
105
105
  "site.elucidation.tagline": "Una estructura a partir de un espectro de RMN de 1H y una fórmula.",
106
+ "site.spectroscopy.tagline": "Encontrar una estructura a partir de sus espectros IR, de masas y de RMN.",
106
107
  "site.equilibrium.tagline": "Equilibrios químicos: pH, especiación y curvas de valoración.",
107
108
  "site.polycarp.tagline": "Predecir la microestructura de una copolimerización radicalaria.",
108
109
  "site.3d.tagline": "Confórmeros en 3D a partir de una estructura que usted dibuja.",
@@ -345,6 +346,21 @@
345
346
  "formula.wholeCounts": "«{formula}» da a un átomo la cantidad {count}; las cantidades son números enteros.",
346
347
  "formula.unknownNotation": "«{formula}» usa una notación que esta herramienta no cuenta.",
347
348
  "formula.unreadable": "«{formula}» no es una fórmula que esta herramienta sepa leer ({reason}).",
349
+ "formula.lookup.title": "Búsqueda de fórmulas",
350
+ "formula.lookup.reset": "Restablecer la masa y los intervalos de elementos",
351
+ "formula.lookup.mass": "Masa monoisotópica",
352
+ "formula.lookup.massPlaceholder": "p. ej. 128.0626",
353
+ "formula.lookup.neutral": "La de la molécula neutra: reste los protones que el espectro añadió.",
354
+ "formula.lookup.fewest": "Mínimo de átomos de {element}",
355
+ "formula.lookup.most": "Máximo de átomos de {element}",
356
+ "formula.lookup.typeMass": "Escriba una masa para listar las fórmulas que permiten estos intervalos.",
357
+ "formula.lookup.closest": "Las {count} más cercanas de {total} fórmulas",
358
+ "formula.lookup.countOne": "{count} fórmula",
359
+ "formula.lookup.countMany": "{count} fórmulas",
360
+ "formula.lookup.nothingFits": "Ninguna fórmula encaja. Amplíe un intervalo o compruebe la masa.",
361
+ "formula.lookup.tooWide": "Estos intervalos permiten demasiadas fórmulas para buscarlas. Reduzca uno.",
362
+ "formula.lookup.ppm": "{value} ppm",
363
+ "formula.lookup.detail": "{mass} Da, insaturación {unsaturation}",
348
364
  "slides.exit": "Salir",
349
365
  "slides.previousSlide": "Diapositiva anterior",
350
366
  "slides.nextSlide": "Diapositiva siguiente",
@@ -583,6 +599,28 @@
583
599
  "structure.export.noStereoIdCodeHelp": "Todos los estereoisómeros dan este, así que encuentra un registro dibujado sin sus centros estereogénicos.",
584
600
  "structure.export.noStereoTautomerIdCode": "idCode sin estereoquímica ni tautomería",
585
601
  "structure.export.noStereoTautomerIdCodeHelp": "Todos los tautómeros y estereoisómeros dan este: la forma cetónica y la enólica comparten clave.",
602
+ "structure.search.title": "Filtrar por estructura",
603
+ "structure.search.apply": "Aplicar",
604
+ "structure.search.examples": "Partir de",
605
+ "structure.search.substructure": "Contiene",
606
+ "structure.search.substructureHelp": "Toda molécula de la que el dibujo forma parte.",
607
+ "structure.search.similarity": "Similar a",
608
+ "structure.search.similarityHelp": "Ordenadas por cuánto del dibujo comparten.",
609
+ "structure.search.exact": "Es exactamente",
610
+ "structure.search.exactHelp": "La molécula dibujada.",
611
+ "structure.search.exactNoStereo": "Cualquier estereoisómero",
612
+ "structure.search.exactNoStereoHelp": "El mismo compuesto, sea cual sea su estereoquímica dibujada.",
613
+ "structure.search.exactNoStereoTautomer": "Cualquier tautómero",
614
+ "structure.search.exactNoStereoTautomerHelp": "El mismo compuesto en cualquier forma tautomérica, sin su estereoquímica.",
615
+ "structure.search.similarityLabel": "Similitud",
616
+ "structure.search.threshold": "Similitud mínima",
617
+ "structure.search.thresholdHelp": "El menor coeficiente de Tanimoto que alcanza un resultado con el dibujo.",
618
+ "structure.search.thresholdOption": "≥ {percent}",
619
+ "structure.search.exampleBenzene": "Benceno",
620
+ "structure.search.examplePyridine": "Piridina",
621
+ "structure.search.exampleAmide": "Amida",
622
+ "structure.search.exampleSulfonamide": "Sulfonamida",
623
+ "structure.search.exampleTrifluoromethyl": "Trifluorometilo",
586
624
  "molecule3d.view": "Vista",
587
625
  "molecule3d.resetView": "Reiniciar la vista",
588
626
  "molecule3d.spin": "Giro continuo",
@@ -103,6 +103,7 @@
103
103
  "site.regexp.tagline": "Apprendre les expressions régulières dans un bac à sable interactif.",
104
104
  "site.pdb.tagline": "Un coup d'œil rapide sur n'importe quelle entrée de la Protein Data Bank.",
105
105
  "site.elucidation.tagline": "Une structure à partir d'un spectre RMN 1H et d'une formule.",
106
+ "site.spectroscopy.tagline": "Trouver une structure à partir de ses spectres IR, de masse et RMN.",
106
107
  "site.equilibrium.tagline": "Équilibres chimiques : pH, spéciation et courbes de titrage.",
107
108
  "site.polycarp.tagline": "Prédire la microstructure d'une copolymérisation radicalaire.",
108
109
  "site.3d.tagline": "Les conformères en 3D d'une structure que vous dessinez.",
@@ -345,6 +346,21 @@
345
346
  "formula.wholeCounts": "« {formula} » compte {count} pour un atome ; un nombre d’atomes est entier.",
346
347
  "formula.unknownNotation": "« {formula} » utilise une notation que cet outil ne compte pas.",
347
348
  "formula.unreadable": "« {formula} » n’est pas une formule que cet outil sait lire ({reason}).",
349
+ "formula.lookup.title": "Recherche de formule",
350
+ "formula.lookup.reset": "Réinitialiser la masse et les intervalles d’éléments",
351
+ "formula.lookup.mass": "Masse monoisotopique",
352
+ "formula.lookup.massPlaceholder": "p. ex. 128.0626",
353
+ "formula.lookup.neutral": "Celle de la molécule neutre : retirez les protons que le spectre a ajoutés.",
354
+ "formula.lookup.fewest": "{element} : nombre minimal d’atomes",
355
+ "formula.lookup.most": "{element} : nombre maximal d’atomes",
356
+ "formula.lookup.typeMass": "Tapez une masse pour lister les formules que ces intervalles permettent.",
357
+ "formula.lookup.closest": "Les {count} plus proches sur {total} formules",
358
+ "formula.lookup.countOne": "{count} formule",
359
+ "formula.lookup.countMany": "{count} formules",
360
+ "formula.lookup.nothingFits": "Aucune formule ne convient. Élargissez un intervalle, ou vérifiez la masse.",
361
+ "formula.lookup.tooWide": "Ces intervalles permettent trop de formules pour les parcourir. Resserrez-en un.",
362
+ "formula.lookup.ppm": "{value} ppm",
363
+ "formula.lookup.detail": "{mass} Da, insaturation {unsaturation}",
348
364
  "slides.exit": "Quitter",
349
365
  "slides.previousSlide": "Diapositive précédente",
350
366
  "slides.nextSlide": "Diapositive suivante",
@@ -583,6 +599,28 @@
583
599
  "structure.export.noStereoIdCodeHelp": "Tous les stéréoisomères donnent celui-ci, ce qui retrouve un enregistrement dessiné sans ses centres stéréogènes.",
584
600
  "structure.export.noStereoTautomerIdCode": "idCode sans stéréochimie ni tautomérie",
585
601
  "structure.export.noStereoTautomerIdCodeHelp": "Tous les tautomères et tous les stéréoisomères donnent celui-ci : la forme cétone et la forme énol ont la même clé.",
602
+ "structure.search.title": "Filtrer par structure",
603
+ "structure.search.apply": "Appliquer",
604
+ "structure.search.examples": "Partir de",
605
+ "structure.search.substructure": "Contient",
606
+ "structure.search.substructureHelp": "Toutes les molécules dont le dessin fait partie.",
607
+ "structure.search.similarity": "Similaire à",
608
+ "structure.search.similarityHelp": "Classées selon la part du dessin qu’elles partagent.",
609
+ "structure.search.exact": "Exactement",
610
+ "structure.search.exactHelp": "La seule molécule dessinée.",
611
+ "structure.search.exactNoStereo": "Tout stéréoisomère",
612
+ "structure.search.exactNoStereoHelp": "Le même composé, quelle que soit sa stéréochimie dessinée.",
613
+ "structure.search.exactNoStereoTautomer": "Tout tautomère",
614
+ "structure.search.exactNoStereoTautomerHelp": "Le même composé sous toute forme tautomère, sa stéréochimie ignorée.",
615
+ "structure.search.similarityLabel": "Similarité",
616
+ "structure.search.threshold": "Similarité minimale",
617
+ "structure.search.thresholdHelp": "Le plus petit coefficient de Tanimoto qu’un résultat atteint avec le dessin.",
618
+ "structure.search.thresholdOption": "≥ {percent}",
619
+ "structure.search.exampleBenzene": "Benzène",
620
+ "structure.search.examplePyridine": "Pyridine",
621
+ "structure.search.exampleAmide": "Amide",
622
+ "structure.search.exampleSulfonamide": "Sulfonamide",
623
+ "structure.search.exampleTrifluoromethyl": "Trifluorométhyle",
586
624
  "molecule3d.view": "Vue",
587
625
  "molecule3d.resetView": "Réinitialiser la vue",
588
626
  "molecule3d.spin": "Rotation continue",
@@ -103,6 +103,7 @@
103
103
  "site.regexp.tagline": "Imparare le espressioni regolari in un ambiente interattivo.",
104
104
  "site.pdb.tagline": "Uno sguardo rapido a qualsiasi voce della Protein Data Bank.",
105
105
  "site.elucidation.tagline": "Una struttura da uno spettro NMR 1H e da una formula.",
106
+ "site.spectroscopy.tagline": "Trovare una struttura dai suoi spettri IR, di massa e NMR.",
106
107
  "site.equilibrium.tagline": "Equilibri chimici: pH, speciazione e curve di titolazione.",
107
108
  "site.polycarp.tagline": "Prevedere la microstruttura di una copolimerizzazione radicalica.",
108
109
  "site.3d.tagline": "Conformeri in 3D a partire da una struttura che si disegna.",
@@ -345,6 +346,21 @@
345
346
  "formula.wholeCounts": "«{formula}» dà a un atomo il numero {count}; i numeri di atomi sono interi.",
346
347
  "formula.unknownNotation": "«{formula}» usa una notazione che questo strumento non conta.",
347
348
  "formula.unreadable": "«{formula}» non è una formula che questo strumento sa leggere ({reason}).",
349
+ "formula.lookup.title": "Ricerca di formule",
350
+ "formula.lookup.reset": "Reimposta la massa e gli intervalli degli elementi",
351
+ "formula.lookup.mass": "Massa monoisotopica",
352
+ "formula.lookup.massPlaceholder": "es. 128.0626",
353
+ "formula.lookup.neutral": "Quella della molecola neutra: togli i protoni che lo spettro ha aggiunto.",
354
+ "formula.lookup.fewest": "Numero minimo di atomi di {element}",
355
+ "formula.lookup.most": "Numero massimo di atomi di {element}",
356
+ "formula.lookup.typeMass": "Scrivi una massa per elencare le formule che questi intervalli consentono.",
357
+ "formula.lookup.closest": "Le {count} più vicine su {total} formule",
358
+ "formula.lookup.countOne": "{count} formula",
359
+ "formula.lookup.countMany": "{count} formule",
360
+ "formula.lookup.nothingFits": "Nessuna formula corrisponde. Allarga un intervallo o controlla la massa.",
361
+ "formula.lookup.tooWide": "Questi intervalli consentono troppe formule per la ricerca. Restringine uno.",
362
+ "formula.lookup.ppm": "{value} ppm",
363
+ "formula.lookup.detail": "{mass} Da, insaturazione {unsaturation}",
348
364
  "slides.exit": "Esci",
349
365
  "slides.previousSlide": "Diapositiva precedente",
350
366
  "slides.nextSlide": "Diapositiva successiva",
@@ -527,6 +543,28 @@
527
543
  "structure.export.noStereoIdCodeHelp": "Ogni stereoisomero dà questo, così trova una voce disegnata senza i suoi centri stereogenici.",
528
544
  "structure.export.noStereoTautomerIdCode": "idCode senza stereochimica né tautomeria",
529
545
  "structure.export.noStereoTautomerIdCodeHelp": "Ogni tautomero e ogni stereoisomero dà questo: la forma chetonica e quella enolica hanno la stessa chiave.",
546
+ "structure.search.title": "Filtra per struttura",
547
+ "structure.search.apply": "Applica",
548
+ "structure.search.examples": "Parti da",
549
+ "structure.search.substructure": "Contiene",
550
+ "structure.search.substructureHelp": "Ogni molecola di cui il disegno fa parte.",
551
+ "structure.search.similarity": "Simile a",
552
+ "structure.search.similarityHelp": "Ordinate per quanta parte del disegno condividono.",
553
+ "structure.search.exact": "È esattamente",
554
+ "structure.search.exactHelp": "La molecola disegnata.",
555
+ "structure.search.exactNoStereo": "Qualsiasi stereoisomero",
556
+ "structure.search.exactNoStereoHelp": "Lo stesso composto, qualunque sia la stereochimica disegnata.",
557
+ "structure.search.exactNoStereoTautomer": "Qualsiasi tautomero",
558
+ "structure.search.exactNoStereoTautomerHelp": "Lo stesso composto in qualsiasi forma tautomerica, senza la sua stereochimica.",
559
+ "structure.search.similarityLabel": "Similarità",
560
+ "structure.search.threshold": "Similarità minima",
561
+ "structure.search.thresholdHelp": "Il più piccolo coefficiente di Tanimoto che un risultato raggiunge con il disegno.",
562
+ "structure.search.thresholdOption": "≥ {percent}",
563
+ "structure.search.exampleBenzene": "Benzene",
564
+ "structure.search.examplePyridine": "Piridina",
565
+ "structure.search.exampleAmide": "Ammide",
566
+ "structure.search.exampleSulfonamide": "Sulfonammide",
567
+ "structure.search.exampleTrifluoromethyl": "Trifluorometile",
530
568
  "periodic.table": "Tavola periodica",
531
569
  "periodic.group": "Gruppo {group}",
532
570
  "periodic.period": "Periodo {period}",
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A SMILES turned into the idCode a query editor holds.
3
+ *
4
+ * **Not exported from `core/index.ts`**: it value-imports `openchemlib`, so
5
+ * the structure search dialog loads it with a dynamic import, only once an
6
+ * example is clicked.
7
+ */
8
+
9
+ import { Molecule } from 'openchemlib';
10
+
11
+ /**
12
+ * Read a SMILES as a query fragment.
13
+ * @param smiles - The fragment.
14
+ * @returns Its idCode, flagged as a fragment so any substituent may hang off
15
+ * it; empty when the SMILES cannot be read.
16
+ */
17
+ export function fragmentIdCode(smiles: string): string {
18
+ try {
19
+ const fragment = Molecule.fromSmiles(smiles);
20
+ fragment.setFragment(true);
21
+ return fragment.getIDCode();
22
+ } catch {
23
+ return '';
24
+ }
25
+ }
@@ -77,6 +77,16 @@ export type {
77
77
  StructurePictureOptions,
78
78
  } from './structurePicture.ts';
79
79
  export { STRUCTURE_BOND_LENGTH, structurePicture } from './structurePicture.ts';
80
+ export type {
81
+ StructureSearchExample,
82
+ StructureSearchMode,
83
+ StructureSearchQuery,
84
+ } from './structureSearch.ts';
85
+ export {
86
+ SIMILARITY_THRESHOLDS,
87
+ STRUCTURE_SEARCH_MODES,
88
+ similarityThresholds,
89
+ } from './structureSearch.ts';
80
90
  export type {
81
91
  StructureSource,
82
92
  StructureSourceInput,
@@ -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}