diff --git a/.gitignore b/.gitignore index 9a48072..afa3cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ lib node_modules tsconfig.tsbuildinfo +public/RDKit_minimal.* +public/rdkit-worker.js +storybook-static diff --git a/package-lock.json b/package-lock.json index 662f84e..385c7ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@iktos-oss/molecule-representation", - "version": "1.2.3", + "version": "1.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@iktos-oss/molecule-representation", - "version": "1.2.3", + "version": "1.3.0", "devDependencies": { "@babel/core": "^7.20.12", - "@iktos-oss/rdkit-provider": "^1.2.3", + "@iktos-oss/rdkit-provider": "^2.0.0", "@rdkit/rdkit": "^2022.9.3-1.0.0", "@storybook/addon-actions": "^6.5.16", "@storybook/addon-essentials": "^6.5.16", @@ -42,7 +42,7 @@ "typescript": "^4.9.4" }, "peerDependencies": { - "@iktos-oss/rdkit-provider": "^1.2.3", + "@iktos-oss/rdkit-provider": "^2.0.0", "@rdkit/rdkit": "^2022.9.3-1.0.0", "react": ">=17.0.2", "react-dom": ">=17.0.2" @@ -2261,13 +2261,12 @@ "dev": true }, "node_modules/@iktos-oss/rdkit-provider": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@iktos-oss/rdkit-provider/-/rdkit-provider-1.2.3.tgz", - "integrity": "sha512-3lKtZSolup+tbJMLLFj6ae7moMUQECKvY6hsd+dlcA/UWQSx6I7Pys45rWmQY2fsTIJmvfpFMvzVlRH831r6ng==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iktos-oss/rdkit-provider/-/rdkit-provider-2.0.0.tgz", + "integrity": "sha512-IelPlGnyoZ4ar973v5eO3CAo7eN0yUM3jrRdjBDauP8dPeEU2+gInIW54IvKKzyuHXwdh7J0p5FYUZeaNsreMA==", "dev": true, "peerDependencies": { "@rdkit/rdkit": "^2022.9.3-1.0.0", - "lodash": "^4.17.21", "react": ">=17.0.2", "react-dom": ">=17.0.2" } @@ -38886,9 +38885,9 @@ "dev": true }, "@iktos-oss/rdkit-provider": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@iktos-oss/rdkit-provider/-/rdkit-provider-1.2.3.tgz", - "integrity": "sha512-3lKtZSolup+tbJMLLFj6ae7moMUQECKvY6hsd+dlcA/UWQSx6I7Pys45rWmQY2fsTIJmvfpFMvzVlRH831r6ng==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iktos-oss/rdkit-provider/-/rdkit-provider-2.0.0.tgz", + "integrity": "sha512-IelPlGnyoZ4ar973v5eO3CAo7eN0yUM3jrRdjBDauP8dPeEU2+gInIW54IvKKzyuHXwdh7J0p5FYUZeaNsreMA==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index 0d9532f..65628de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@iktos-oss/molecule-representation", - "version": "1.2.3", + "version": "1.3.0", "description": "exports interactif molecule represnetations as react components", "main": "lib/cjs/index.js", "module": "lib/esm/index.js", @@ -36,13 +36,14 @@ "tsc-check": "tsc --noEmit --skipLibCheck", "dist-package": "./tools/dist-package.sh", "test-dist-package": "./tools/test-dist-package.sh", - "storybook": "start-storybook -p 6006", + "storybook": "npm run prerun:cp-assets && start-storybook -s ./public -p 6006", + "prerun:cp-assets": "cp node_modules/@rdkit/rdkit/dist/RDKit_minimal.js node_modules/@rdkit/rdkit/dist/RDKit_minimal.wasm node_modules/@iktos-oss/rdkit-provider/lib/rdkit-worker.js ./public", "build-storybook": "build-storybook" }, "author": "Ramzi Oueslati ", "devDependencies": { "@babel/core": "^7.20.12", - "@iktos-oss/rdkit-provider": "^1.2.3", + "@iktos-oss/rdkit-provider": "^2.0.0", "@rdkit/rdkit": "^2022.9.3-1.0.0", "@storybook/addon-actions": "^6.5.16", "@storybook/addon-essentials": "^6.5.16", @@ -75,9 +76,9 @@ "typescript": "^4.9.4" }, "peerDependencies": { + "@iktos-oss/rdkit-provider": "^2.0.0", "@rdkit/rdkit": "^2022.9.3-1.0.0", "react": ">=17.0.2", - "react-dom": ">=17.0.2", - "@iktos-oss/rdkit-provider": "^1.2.3" + "react-dom": ">=17.0.2" } } diff --git a/src/components/MoleculeRepresentation/MoleculeRepresentation.tsx b/src/components/MoleculeRepresentation/MoleculeRepresentation.tsx index 21c84b2..b92abca 100644 --- a/src/components/MoleculeRepresentation/MoleculeRepresentation.tsx +++ b/src/components/MoleculeRepresentation/MoleculeRepresentation.tsx @@ -1,8 +1,7 @@ -import React, { CSSProperties, memo, useEffect, useMemo, useRef, useState } from 'react'; -import { useRDKit } from '@iktos-oss/rdkit-provider'; +import React, { CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { getMoleculeDetails, isValidSmiles, useRDKit } from '@iktos-oss/rdkit-provider'; import { ClickableAtoms, DrawSmilesSVGProps, get_svg, get_svg_from_smarts } from '../../utils/draw'; import { appendRectsToSvg, Rect } from '../../utils/html'; -import { get_molecule_details, is_valid_smiles } from '../../utils/molecule'; import { CLICKABLE_MOLECULE_CLASSNAME, @@ -30,56 +29,74 @@ export const MoleculeRepresentation: React.FC = mem alignmentDetails, style, showLoadingSpinner = false, + showSmartsAsSmiles = false, width, ...restOfProps }: MoleculeRepresentationProps) => { - const { RDKit } = useRDKit(); + const { worker } = useRDKit(); const moleculeRef = useRef(null); const [svgContent, setSvgContent] = useState(''); const [rects, setRects] = useState>([]); const isClickable = useMemo(() => !!onAtomClick, [onAtomClick]); + const [shouldComputeRects, setShouldComputeRects] = useState(false); - useEffect(() => { - if (!RDKit) return; + const computeClickingRects = useCallback(async () => { + if (!worker) return; if (!isClickable) return; const structureToDraw = smiles || (smarts as string); - const moleculeDetails = get_molecule_details(structureToDraw, RDKit); + const moleculeDetails = await getMoleculeDetails(worker, { smiles: structureToDraw }); if (!moleculeDetails) return; setTimeout( // do this a better way, the issue is when highlighting there is a moment when the atom-0 is rendered at the wrong position (0-0) - () => + () => { computeClickingAreaForAtoms({ numAtoms: moleculeDetails.numAtoms, parentDiv: moleculeRef.current, clickableAtoms: clickableAtoms?.clickableAtomsIds, - }).then(setRects), + }).then(setRects); + }, 100, ); - }, [smiles, smarts, RDKit, isClickable, clickableAtoms]); + setShouldComputeRects(false); + }, [smiles, smarts, isClickable, clickableAtoms, worker]); + + useEffect(() => { + if (!shouldComputeRects) return; + computeClickingRects(); + }, [shouldComputeRects, computeClickingRects]); useEffect(() => { - if (!RDKit) return; - const drawingDetails: DrawSmilesSVGProps = { - smiles: smarts || (smiles as string), - width, - height, - details: { ...details, addAtomIndices }, - alignmentDetails, - atomsToHighlight, - bondsToHighlight, - isClickable, - clickableAtoms, + if (!worker) return; + const computeSvg = async () => { + const drawingDetails: DrawSmilesSVGProps = { + smiles: (smarts || smiles) as string, + width, + height, + details: { ...details, addAtomIndices }, + alignmentDetails, + atomsToHighlight, + bondsToHighlight, + isClickable, + clickableAtoms, + }; + const isSmartsAValidSmiles = + showSmartsAsSmiles && !!smarts && (await isValidSmiles(worker, { smiles: smarts })).isValid; + const svg = + smarts && !isSmartsAValidSmiles + ? await get_svg_from_smarts({ smarts, width, height }, worker) + : await get_svg(drawingDetails, worker); + if (!svg) return; + const svgWithHitBoxes = rects.length ? appendRectsToSvg(svg, rects) : svg; + if (svgWithHitBoxes) { + setSvgContent(svgWithHitBoxes); + } + if (!rects.length && isClickable) { + setShouldComputeRects(true); + } }; - setTimeout(() => { - // put workload in a settimeout 0 to avoid blocking the main thread when rendering lots of molecules - const svg = smarts - ? is_valid_smiles(smarts, RDKit) - ? get_svg(drawingDetails, RDKit) - : get_svg_from_smarts({ smarts, width, height }, RDKit) - : get_svg(drawingDetails, RDKit); - if (svg) setSvgContent(appendRectsToSvg(svg, rects)); - }, 0); + computeSvg(); }, [ + showSmartsAsSmiles, smiles, smarts, rects, @@ -90,7 +107,7 @@ export const MoleculeRepresentation: React.FC = mem bondsToHighlight, width, height, - RDKit, + worker, clickableAtoms, alignmentDetails, ]); @@ -135,6 +152,7 @@ interface MoleculeRepresentationBaseProps { onAtomClick?: (atomId: string) => void; style?: CSSProperties; showLoadingSpinner?: boolean; + showSmartsAsSmiles?: boolean; width: number; } diff --git a/src/stories/MoleculeRepresentation.stories.tsx b/src/stories/MoleculeRepresentation.stories.tsx index b08d7d1..8abf9d6 100644 --- a/src/stories/MoleculeRepresentation.stories.tsx +++ b/src/stories/MoleculeRepresentation.stories.tsx @@ -138,8 +138,21 @@ export const FromSmarts = Template.bind({}); FromSmarts.args = { moleculeRepresetnationProps: { ...PROPS, - smarts: undefined, - smiles: '****CO', + smarts: '[#6][CH1](=O)', + smiles: undefined, + alignmentDetails: undefined, + }, + rdkitProviderProps: RDKitProviderCachingProps, +}; + +export const DrawSmartsAsSmiles = Template.bind({}); +DrawSmartsAsSmiles.args = { + moleculeRepresetnationProps: { + ...PROPS, + smarts: '[#6][CH1](=O)', + showSmartsAsSmiles: true, + smiles: undefined, + alignmentDetails: undefined, }, rdkitProviderProps: RDKitProviderCachingProps, }; @@ -180,6 +193,8 @@ Clickable.args = { export const BigClickableMoleculeWithLoadingSpinner = TemplateWithOnAtomClick.bind({}); BigClickableMoleculeWithLoadingSpinner.args = { ...PROPS, + width: 800, + height: 800, smiles: BIG_MOLECULE, showLoadingSpinner: true, }; diff --git a/src/utils/draw.ts b/src/utils/draw.ts index ee35bdc..8a78936 100644 --- a/src/utils/draw.ts +++ b/src/utils/draw.ts @@ -1,12 +1,18 @@ -import { get_molecule, release_molecule } from '@iktos-oss/rdkit-provider'; -import { JSMol, RDKitModule } from '@rdkit/rdkit'; +import { getSvgFromSmarts } from '@iktos-oss/rdkit-provider'; +import { + getSvg, + getMoleculeDetails, + getCanonicalFormForStructure, + getMatchingSubstructure, +} from '@iktos-oss/rdkit-provider'; import { AlignmentDetails } from '../components'; import { HIGHLIGHT_RDKIT_COLORS, RDKitColor, TRANSPARANT_RDKIT_COLOR } from '../constants'; -import { get_canonical_form_for_structure, get_molecule_details } from './molecule'; -export const get_svg = (params: DrawSmilesSVGProps, RDKit: RDKitModule) => { +export const get_svg = async (params: DrawSmilesSVGProps, worker: Worker) => { if (!params.smiles) return null; - const canonicalSmiles = get_canonical_form_for_structure(params.smiles, RDKit); + const { canonicalForm: canonicalSmiles } = await getCanonicalFormForStructure(worker, { + structure: params.smiles, + }); if (!canonicalSmiles) return null; const { @@ -21,7 +27,7 @@ export const get_svg = (params: DrawSmilesSVGProps, RDKit: RDKitModule) => { } = params; const highlightBondColors = getHighlightColors(bondsToHighlight); const highlightAtomColors = getHighlightColors(atomsToHighlight); - const moleculeDetails = isClickable ? get_molecule_details(canonicalSmiles, RDKit) : null; + const moleculeDetails = isClickable ? await getMoleculeDetails(worker, { smiles: canonicalSmiles }) : null; if (isClickable && moleculeDetails) { setHighlightColorForClickableMolecule({ nbAtoms: moleculeDetails.numAtoms, @@ -34,19 +40,16 @@ export const get_svg = (params: DrawSmilesSVGProps, RDKit: RDKitModule) => { isClickable && moleculeDetails ? [...Array(moleculeDetails.numAtoms).keys()] : atomsToHighlight?.flat() ?? []; const bondsToDrawWithHighlight = bondsToHighlight?.flat() ?? []; - let mol = null; try { - mol = get_molecule(canonicalSmiles, RDKit); - if (!mol) return null; if (alignmentDetails) { - addAlignmentFromMolBlock({ - mol, + await addAlignmentFromMolBlock({ + smiles: canonicalSmiles, alignmentDetails, highlightAtomColors, atomsToDrawWithHighlight, highlightBondColors, bondsToDrawWithHighlight, - RDKit, + worker, }); } const rdkitDrawingOptions = JSON.stringify({ @@ -59,32 +62,31 @@ export const get_svg = (params: DrawSmilesSVGProps, RDKit: RDKitModule) => { highlightAtomColors, highlightBondColors, }); - const svg = mol.get_svg_with_highlights(rdkitDrawingOptions); + const { svg } = await getSvg(worker, { + smiles: canonicalSmiles, + drawingDetails: rdkitDrawingOptions, + alignmentDetails, + }); return svg; } catch (error) { console.error(error); return null; - } finally { - if (mol) { - if (alignmentDetails) { - // reset coords as mol could be in cache - mol.set_new_coords(); - } - release_molecule(mol); - } } }; -export const get_svg_from_smarts = (params: DrawSmartsSVGProps, RDKit: RDKitModule): string | null => { - if (!RDKit) return null; +export const get_svg_from_smarts = async (params: DrawSmartsSVGProps, worker: Worker) => { + if (!worker) return null; if (!params.smarts) return null; - const canonicalSmarts = get_canonical_form_for_structure(params.smarts, RDKit); + const { canonicalForm: canonicalSmarts } = await getCanonicalFormForStructure(worker, { + structure: params.smarts, + }); if (!canonicalSmarts) return null; - const smartsMol = RDKit.get_qmol(canonicalSmarts); - const svg = smartsMol.get_svg(params.width, params.height); - smartsMol.delete(); + const { svg } = await getSvgFromSmarts(worker, { + ...params, + smarts: canonicalSmarts, + }); return svg; }; @@ -103,50 +105,49 @@ const getHighlightColors = (items?: number[][]) => { return highlightColors; }; -const addAlignmentFromMolBlock = ({ - mol, +const addAlignmentFromMolBlock = async ({ + smiles, alignmentDetails, highlightAtomColors, atomsToDrawWithHighlight, highlightBondColors, bondsToDrawWithHighlight, - RDKit, + worker, }: { - mol: JSMol; + smiles: string; alignmentDetails: AlignmentDetails; highlightAtomColors: HighlightColors; highlightBondColors: HighlightColors; atomsToDrawWithHighlight: number[]; bondsToDrawWithHighlight: number[]; - RDKit: RDKitModule; + worker: Worker; }) => { - const molToAlignWith = get_molecule(alignmentDetails.molBlock, RDKit); - if (!molToAlignWith) return; - mol.generate_aligned_coords(molToAlignWith, true); if (!alignmentDetails.highlightColor) { - release_molecule(molToAlignWith); return; } - const { atoms: molblockAtomsToHighlight, bonds: molblockBondsToHighlight } = JSON.parse( - mol.get_substruct_match(molToAlignWith), - ); - if (molblockAtomsToHighlight) { + const matchDetails = await getMatchingSubstructure(worker, { + structure: smiles, + substructure: alignmentDetails.molBlock, + }); + if (!matchDetails) return; + const { matchingAtoms, matchingBonds } = matchDetails; + + if (matchingAtoms) { addAtomsOrBondsToHighlight({ - indicies: molblockAtomsToHighlight, + indicies: matchingAtoms, highlightColors: highlightAtomColors, indiciesToHighlight: atomsToDrawWithHighlight, color: alignmentDetails.highlightColor, }); } - if (molblockBondsToHighlight) { + if (matchingBonds) { addAtomsOrBondsToHighlight({ - indicies: molblockBondsToHighlight, + indicies: matchingBonds, highlightColors: highlightBondColors, indiciesToHighlight: bondsToDrawWithHighlight, color: alignmentDetails.highlightColor, }); } - release_molecule(molToAlignWith); }; const addAtomsOrBondsToHighlight = ({ diff --git a/src/utils/html.ts b/src/utils/html.ts index 3b5d0dd..e4ff31c 100644 --- a/src/utils/html.ts +++ b/src/utils/html.ts @@ -38,6 +38,7 @@ export const appendRectsToSvg = (svg: string, rects: Rect[]) => { const temp = document.createElement('div'); temp.innerHTML = svg; const svgParsed = temp.getElementsByTagName('svg')[0]; + if (!svgParsed) return; for (const rect of rects) { // @ts-ignore const rectElem = document.createElementNS(svgParsed.attributes['xmlns'].nodeValue, 'rect'); diff --git a/src/utils/molecule.ts b/src/utils/molecule.ts deleted file mode 100644 index e637677..0000000 --- a/src/utils/molecule.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { RDKitModule } from '@rdkit/rdkit'; -import { get_molecule, release_molecule } from '@iktos-oss/rdkit-provider'; - -export const get_molecule_details = ( - smiles: string, - RDKit: RDKitModule, -): { numAtoms: number; numRings: number } | null => { - const mol = get_molecule(smiles, RDKit); - if (!mol) return null; - - const details = JSON.parse(mol.get_descriptors()); - release_molecule(mol); - return { - numAtoms: details.NumHeavyAtoms, - numRings: details.NumRings, - }; -}; - -export const is_valid_smiles = (smiles: string, RDKit: RDKitModule): boolean => { - if (!smiles) return false; - const mol = get_molecule(smiles, RDKit); - if (!mol) return false; - const isValid = mol.is_valid(); - release_molecule(mol); - return isValid; -}; - -export const get_canonical_form_for_structure = (structure: string, RDKit: RDKitModule): string | null => { - if (is_valid_smiles(structure, RDKit)) return get_canonical_smiles(structure, RDKit); - const mol = get_molecule(structure, RDKit); - if (!mol) return null; - const cannonicalForm = mol.get_smarts(); - release_molecule(mol); - return cannonicalForm; -}; - -const get_canonical_smiles = (smiles: string, RDKit: RDKitModule): string | null => { - const mol = get_molecule(smiles, RDKit); - if (!mol) return null; - const cannonicalSmiles = mol.get_smiles(); - release_molecule(mol); - return cannonicalSmiles; -};