Skip to content

Feat sdf loader #31140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions docs/examples/en/loaders/SDFLoader.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="../../../" />
<script src="page.js"></script>
<link type="text/css" rel="stylesheet" href="page.css" />
</head>
<body>
[page:Loader] &rarr;

<h1>[name]</h1>

<p class="desc">
A loader for visualising molecules contained in <a href="https://en.wikipedia.org/wiki/Chemical_table_file#Molfile">MDL Molfile / SDF</a> files.
The loader creates two <a href="/docs/#api/en/objects/InstancedMesh">InstancedMesh</a> objects – spheres for atoms and cylinders for bonds – and returns them in a single [page:Group].
</p>

<h2>Import</h2>
<p>
[name] is an add-on, and must therefore be imported explicitly. See
[link:#manual/introduction/Installation Installation / Addons].
</p>

<code>
import { SDFLoader } from 'three/addons/loaders/SDFLoader.js';
</code>

<h2>Code Example</h2>

<code>
const loader = new SDFLoader();
loader.load( 'models/sdf/benzene.sdf', function ( group ) {

// `group` is a THREE.Group containing two InstancedMesh children:
// * atoms – spheres
// * bonds – cylinders
scene.add( group );

} );
</code>

<h2>Examples</h2>
<p>
[example:webgl_loader_sdf]
</p>

<h2>Constructor</h2>

<h3>[name]( [param:LoadingManager manager] )</h3>
<p>
[page:LoadingManager manager] — The [page:LoadingManager loadingManager] for the loader to use. Default is [page:LoadingManager THREE.DefaultLoadingManager].
</p>
<p>Creates a new [name].</p>

<h2>Properties</h2>
<p>See the base [page:Loader] class for common properties.</p>
<p>
[property:Object elementColors] — Map of element symbol → atom colour (hex). Can be customised via [page:setElementData].<br />
[property:Object elementRadii] — Map of element symbol → van-der-Waals radius (Å). Can be customised via [page:setElementData].
</p>

<h2>Methods</h2>
<p>See the base [page:Loader] class for common methods.</p>

<h3>[method:undefined load]( [param:String url], [param:Function onLoad], [param:Function onProgress], [param:Function onError] )</h3>
<p>
Begin loading the file from <code>url</code>. The <code>onLoad</code> callback is passed the returned [page:Group].
</p>

<h3>[method:Group parse]( [param:String text] )</h3>
<p>Parse a Molfile / SDF molecule string and return a [page:Group] as described above.</p>

<h3>[method:this setElementData]( [param:Object colors], [param:Object radii] )</h3>
<p>Merge <code>colors</code> and/or <code>radii</code> into the default element data.</p>

<h2>Source</h2>
<p>[link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/SDFLoader.js examples/jsm/loaders/SDFLoader.js]</p>
</body>
</html>
1 change: 1 addition & 0 deletions examples/jsm/Addons.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export * from './loaders/NRRDLoader.js';
export * from './loaders/OBJLoader.js';
export * from './loaders/PCDLoader.js';
export * from './loaders/PDBLoader.js';
export * from './loaders/SDFLoader.js';
export * from './loaders/PLYLoader.js';
export * from './loaders/PVRLoader.js';
export * from './loaders/RGBELoader.js';
Expand Down
195 changes: 195 additions & 0 deletions examples/jsm/loaders/SDFLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import {
Loader,
FileLoader,
Group,
Vector3,
Color,
SphereGeometry,
CylinderGeometry,
MeshLambertMaterial,
InstancedMesh,
Matrix4,
Quaternion
} from 'three';

const DEFAULT_VDW_RADIUS = {
H: 0.31, C: 0.76, N: 0.71, O: 0.66, F: 0.57,
P: 1.07, S: 1.05, Cl: 1.02, Br: 1.2, I: 1.39
};

const DEFAULT_ELEMENT_COLOR = {
H: 0xffffff, C: 0x909090, N: 0x3050f8, O: 0xff0d0d, F: 0x90e050,
P: 0xff8000, S: 0xffff30, Cl: 0x1ff01f, Br: 0xa62929, I: 0x940094
};

/**
* SDFLoader — A loader for MDL Molfile / SDF chemical structure files.
*
* The loader parses the textual SDF data and returns a `THREE.Group` containing two
* `THREE.InstancedMesh` children: one for the atoms (spheres) and one for the bonds (cylinders).
*
* In addition to efficient rendering via instancing, the loader allows customisation of element
* colours and van-der-Waals radii via the `setElementData` method.
*
* Example usage:
* ```js
* import { SDFLoader } from 'three/addons/loaders/SDFLoader.js';
*
* const loader = new SDFLoader();
* loader.load( 'models/sdf/benzene.sdf', group => {
* scene.add( group );
* } );
* ```
*
* @see examples/webgl_loader_sdf
*/
class SDFLoader extends Loader {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vote to remove SDFLoader and implement the JSON parsing right in the example. The loader contains too much application specific code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SDF parsing library could be imported like in other examples:

"three-mesh-bvh": "https://cdn.jsdelivr.net/npm/[email protected]/build/index.module.js",
"web-ifc": "https://cdn.jsdelivr.net/npm/[email protected]/web-ifc-api.js",
"web-ifc-three": "https://cdn.jsdelivr.net/npm/[email protected]/IFCLoader.js"


constructor( manager ) {

super( manager );
this.elementRadii = { ...DEFAULT_VDW_RADIUS };
this.elementColors = { ...DEFAULT_ELEMENT_COLOR };

}

setElementData( colors, radii ) {

if ( colors ) Object.assign( this.elementColors, colors );
if ( radii ) Object.assign( this.elementRadii, radii );
return this;

}

load( url, onLoad, onProgress, onError ) {

const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.setResponseType( 'text' );
loader.load( url, text => {

try {

onLoad( this.parse( text ) );

} catch ( e ) {

if ( onError ) {

onError( e );

} else {

console.error( e );

}

this.manager.itemError( url );

}

}, onProgress, onError );

}

parse( text ) {

// Use internal lightweight parser so loader is self-contained
/* eslint-disable padded-blocks */
const { atoms, bonds } = ( () => {

const lines = text.split( /\r?\n/ );
let i = 3;
const counts = lines[ i ++ ].trim().split( /\s+/ );
const natoms = parseInt( counts[ 0 ] );
const nbonds = parseInt( counts[ 1 ] );
if ( isNaN( natoms ) || isNaN( nbonds ) ) throw new Error( 'SDFLoader: invalid counts line' );
const atoms = [];

for ( let k = 0; k < natoms; k ++, i ++ ) {
const l = lines[ i ];
if ( ! l || l.length < 31 ) throw new Error( `SDFLoader: invalid atom line ${ i + 1 }` );
const x = parseFloat( l.substr( 0, 10 ) );
const y = parseFloat( l.substr( 10, 10 ) );
const z = parseFloat( l.substr( 20, 10 ) );
const element = l.substr( 31, 3 ).trim() || 'C';
atoms.push( { position: new Vector3( x, y, z ), element } );
}

const bonds = [];
while ( bonds.length < nbonds && i < lines.length ) {
const l = lines[ i ++ ];
if ( ! l ) continue;
if ( l.startsWith( 'M' ) ) break;
if ( l.length < 9 ) continue;
const a1 = parseInt( l.slice( 0, 3 ) ) - 1;
const a2 = parseInt( l.slice( 3, 6 ) ) - 1;
const type = parseInt( l.slice( 6, 9 ) ) || 1;
if ( isNaN( a1 ) || isNaN( a2 ) || a1 < 0 || a2 < 0 || a1 >= natoms || a2 >= natoms ) continue;
bonds.push( [ a1, a2, type ] );
}

return { atoms, bonds };

} )();
/* eslint-enable padded-blocks */

return this._buildSceneGraph( atoms, bonds );

}

_buildSceneGraph( atoms, bonds ) {

const group = new Group();

const sphereGeo = new SphereGeometry( 1, 16, 16 );
const atomMat = new MeshLambertMaterial();
const atomMesh = new InstancedMesh( sphereGeo, atomMat, atoms.length );
const m = new Matrix4();

atoms.forEach( ( atom, idx ) => {

const r = ( this.elementRadii[ atom.element ] || 0.75 ) * 0.2;
m.makeScale( r, r, r );
m.setPosition( atom.position );
atomMesh.setMatrixAt( idx, m );
const color = this.elementColors[ atom.element ] || 0xcccccc;
atomMesh.setColorAt( idx, new Color( color ) );

} );
atomMesh.instanceMatrix.needsUpdate = true;
atomMesh.instanceColor.needsUpdate = true;
group.add( atomMesh );

const cylGeo = new CylinderGeometry( 0.05, 0.05, 1, 8 );
const bondMat = new MeshLambertMaterial( { color: 0xaaaaaa } );
const bondMesh = new InstancedMesh( cylGeo, bondMat, bonds.length );
const up = new Vector3( 0, 1, 0 );
const q = new Quaternion();

bonds.forEach( ( [ a, b, bondType ], idx ) => {

const v1 = atoms[ a ].position;
const v2 = atoms[ b ].position;
const mid = v1.clone().add( v2 ).multiplyScalar( 0.5 );
const dir = v2.clone().sub( v1 );
const len = dir.length();
q.setFromUnitVectors( up, dir.clone().normalize() );
m.makeRotationFromQuaternion( q );
m.setPosition( mid );
m.scale( new Vector3( 1, len, 1 ) );
bondMesh.setMatrixAt( idx, m );

} );
bondMesh.instanceMatrix.needsUpdate = true;
group.add( bondMesh );

return group;

}

}

export { SDFLoader };
17 changes: 17 additions & 0 deletions examples/models/sdf/benzene.sdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
benzene
threejs 2025

6 6 0 0 0 0 0 0 0 0999 V2000
0.0000 1.3960 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.2094 0.6980 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.2094 -0.6980 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
0.0000 -1.3960 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
1.2094 -0.6980 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
1.2094 0.6980 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
1 2 1 0 0 0 0
2 3 2 0 0 0 0
3 4 1 0 0 0 0
4 5 2 0 0 0 0
5 6 1 0 0 0 0
6 1 2 0 0 0 0
M END
54 changes: 54 additions & 0 deletions examples/models/sdf/caffeine.sdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Caffeine
ChemDraw03112406102D

24 25 0 0 0 0 0 0 0 0999 V2000
0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
0.7145 0.4125 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
0.0000 0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.7145 0.4125 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
-0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
0.0000 -0.8250 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
1.4289 -0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
-1.4289 -0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
1.4289 0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.4289 0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
0.0000 1.6500 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
0.0000 2.4750 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
0.7145 2.8875 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
0.7145 3.7125 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1.4289 2.4750 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
0.7145 2.0625 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
2.1434 0.4125 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1.4289 1.6500 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
2.1434 0.8250 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.1434 0.4125 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.4289 1.6500 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.1434 0.8250 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
0.0000 -1.6500 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.7145 2.8875 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1 2 1 0 0 0 0
2 3 1 0 0 0 0
3 4 1 0 0 0 0
4 5 1 0 0 0 0
5 6 1 0 0 0 0
6 1 1 0 0 0 0
1 7 2 0 0 0 0
5 8 2 0 0 0 0
2 9 1 0 0 0 0
4 10 1 0 0 0 0
3 11 1 0 0 0 0
11 12 1 0 0 0 0
12 13 1 0 0 0 0
13 14 1 0 0 0 0
13 15 1 0 0 0 0
13 16 1 0 0 0 0
9 17 1 0 0 0 0
9 18 1 0 0 0 0
9 19 1 0 0 0 0
10 20 1 0 0 0 0
10 21 1 0 0 0 0
10 22 1 0 0 0 0
6 23 1 0 0 0 0
12 24 1 0 0 0 0
M END
$$$$
Loading
Loading