diff --git a/docs/examples/en/loaders/SDFLoader.html b/docs/examples/en/loaders/SDFLoader.html new file mode 100644 index 00000000000000..b308618e457375 --- /dev/null +++ b/docs/examples/en/loaders/SDFLoader.html @@ -0,0 +1,80 @@ + + + + + + + + + + [page:Loader] → + +

[name]

+ +

+ A loader for visualising molecules contained in MDL Molfile / SDF files. + The loader creates two InstancedMesh objects – spheres for atoms and cylinders for bonds – and returns them in a single [page:Group]. +

+ +

Import

+

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

+ + +import { SDFLoader } from 'three/addons/loaders/SDFLoader.js'; + + +

Code Example

+ + +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 ); + +} ); + + +

Examples

+

+ [example:webgl_loader_sdf] +

+ +

Constructor

+ +

[name]( [param:LoadingManager manager] )

+

+ [page:LoadingManager manager] — The [page:LoadingManager loadingManager] for the loader to use. Default is [page:LoadingManager THREE.DefaultLoadingManager]. +

+

Creates a new [name].

+ +

Properties

+

See the base [page:Loader] class for common properties.

+

+ [property:Object elementColors] — Map of element symbol → atom colour (hex). Can be customised via [page:setElementData].
+ [property:Object elementRadii] — Map of element symbol → van-der-Waals radius (Å). Can be customised via [page:setElementData]. +

+ +

Methods

+

See the base [page:Loader] class for common methods.

+ +

[method:undefined load]( [param:String url], [param:Function onLoad], [param:Function onProgress], [param:Function onError] )

+

+ Begin loading the file from url. The onLoad callback is passed the returned [page:Group]. +

+ +

[method:Group parse]( [param:String text] )

+

Parse a Molfile / SDF molecule string and return a [page:Group] as described above.

+ +

[method:this setElementData]( [param:Object colors], [param:Object radii] )

+

Merge colors and/or radii into the default element data.

+ +

Source

+

[link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/SDFLoader.js examples/jsm/loaders/SDFLoader.js]

+ + diff --git a/examples/jsm/Addons.js b/examples/jsm/Addons.js index f2d8fc7f16465b..34da53be5866a7 100644 --- a/examples/jsm/Addons.js +++ b/examples/jsm/Addons.js @@ -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'; diff --git a/examples/jsm/loaders/SDFLoader.js b/examples/jsm/loaders/SDFLoader.js new file mode 100644 index 00000000000000..63570e62264539 --- /dev/null +++ b/examples/jsm/loaders/SDFLoader.js @@ -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 { + + 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 }; diff --git a/examples/models/sdf/benzene.sdf b/examples/models/sdf/benzene.sdf new file mode 100644 index 00000000000000..123a0621349a4f --- /dev/null +++ b/examples/models/sdf/benzene.sdf @@ -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 diff --git a/examples/models/sdf/caffeine.sdf b/examples/models/sdf/caffeine.sdf new file mode 100644 index 00000000000000..e0a0e2766b88dc --- /dev/null +++ b/examples/models/sdf/caffeine.sdf @@ -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 +$$$$ diff --git a/examples/webgl_loader_sdf.html b/examples/webgl_loader_sdf.html new file mode 100644 index 00000000000000..1492b7cca95957 --- /dev/null +++ b/examples/webgl_loader_sdf.html @@ -0,0 +1,256 @@ + + + + three.js webgl - loaders - SDF loader + + + + + + +
+ three.js - SDFLoader test +
+ + + + + + diff --git a/test/unit/addons/loaders/SDFLoader.tests.js b/test/unit/addons/loaders/SDFLoader.tests.js new file mode 100644 index 00000000000000..5a8909e459ceb7 --- /dev/null +++ b/test/unit/addons/loaders/SDFLoader.tests.js @@ -0,0 +1,66 @@ +import { SDFLoader } from '../../../../examples/jsm/loaders/SDFLoader.js'; +import { Group, Color } from 'three'; + +export default QUnit.module( 'Loaders', () => { + + QUnit.module( 'SDFLoader', () => { + + // INSTANCING + QUnit.test( 'constructor', ( assert ) => { + + assert.ok( new SDFLoader(), 'Can instantiate loader.' ); + + } ); + + QUnit.test( 'parse', ( assert ) => { + + const loader = new SDFLoader(); + + const sdfContent = +` + -ISIS- 08231509562D + + 6 6 0 0 0 0 0 0 0 0999 V2000 + 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.0000 1.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.0000 1.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.0000 0.0000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + 0.0000 -1.0000 0.0000 N 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 1 1 0 0 0 0 + 4 5 1 0 0 0 0 + 1 6 1 0 0 0 0 +M END`; + + const result = loader.parse( sdfContent ); + + assert.ok( result instanceof Group, 'Parser returns a Group.' ); + assert.equal( result.children.length, 2, 'Group has correct number of children (atoms and bonds).' ); + + const atomMesh = result.children[ 0 ]; + const bondMesh = result.children[ 1 ]; + + assert.equal( atomMesh.count, 6, 'Correct number of atoms.' ); + assert.equal( bondMesh.count, 6, 'Correct number of bonds.' ); + + } ); + + QUnit.test( 'setElementData', ( assert ) => { + + const loader = new SDFLoader(); + const customColors = { H: 0x123456 }; + const customRadii = { H: 0.5 }; + + loader.setElementData( customColors, customRadii ); + + assert.equal( loader.elementColors.H, 0x123456, 'Custom colors are set.' ); + assert.equal( loader.elementRadii.H, 0.5, 'Custom radii are set.' ); + + } ); + + } ); + +} ); diff --git a/test/unit/three.addons.unit.js b/test/unit/three.addons.unit.js index 3b1862431ab7e0..2b067944efe65a 100644 --- a/test/unit/three.addons.unit.js +++ b/test/unit/three.addons.unit.js @@ -1,5 +1,5 @@ - //addons/utils import './addons/utils/BufferGeometryUtils.tests.js'; import './addons/math/ColorSpaces.tests.js'; import './addons/curves/NURBSCurve.tests.js'; +import './addons/loaders/SDFLoader.tests.js';