three.js: Editor - broken LDRAW support and missing PDB support

Description

Using online editor to load LDRAW file ends in error (both Firefox and Chrome browsers on Windows).

The 1st code below is actually what works for loading LDRAW files, with additional .DAT and .L3B formats + using @gkjohnson repository + removed undefined parameter from the loader.parse function call.

The 2nd code shows what works if PDB format is to be considered for inclusion, which is mainly the code extracted from the webgl_loader_pdb.html example.

I can submit a PR if this code is acceptable.

Reproduction steps

  1. Use either Firefox or Chrome browser and go to https://threejs.org/editor/
  2. Try to load any LDRAW file

Code

			case 'dat':
			case 'l3b':
			case 'ldr':
			case 'mpd':

			{

				reader.addEventListener( 'load', async function ( event ) {

					const { LDrawLoader } = await import( 'three/addons/loaders/LDrawLoader.js' );

					const loader = new LDrawLoader();

					// The path to preload color definitions from.
					await loader.preloadMaterials( 'https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/colors/ldcfgalt.ldr' );
					// The path to load parts from the LDraw parts library from.
					loader.setPartsLibraryPath( 'https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/complete/ldraw/' );
					loader.setPath( '../../examples/models/ldraw/officialLibrary/' );

					loader.parse( event.target.result, function ( group ) {

						group.name = filename;
						// Convert from LDraw coordinates: rotate 180 degrees around OX
						group.rotation.x = Math.PI;

						// Scale and add model groups to the scene
						group.scale.multiplyScalar( 0.1 );

						editor.execute( new AddObjectCommand( editor, group ) );

					} );

				}, false );
				reader.readAsText( file );

				break;

			}

			case 'pdb':

			{

				reader.addEventListener( 'load', async function ( event ) {

					const atomGeometry = new THREE.IcosahedronGeometry( 1, 2 );
					const bondGeometry = new THREE.BoxGeometry( 1, 1, 1 );
					let position = new THREE.Vector3();
					let offset = new THREE.Vector3();
					let start = new THREE.Vector3();
					let end = new THREE.Vector3();
					let color = new THREE.Color();

					let root = new THREE.Group();
					root.name = filename;

					let atoms = [];
					let bonds = [];
			
					const contents = event.target.result;

					const { PDBLoader } = await import( '../../examples/jsm/loaders/PDBLoader.js' );

					let pdb = new PDBLoader().parse( contents );

					// Get atom related data
					let geometryAtoms = pdb.geometryAtoms;
					// Get the data of the bond between atoms
					let geometryBonds = pdb.geometryBonds;
					// Get data of atomic text
					let json = pdb.json;

					// Center the model
					geometryAtoms.computeBoundingBox();
					geometryAtoms.boundingBox.getCenter( offset ).negate();
					geometryAtoms.translate( offset.x, offset.y, offset.z );
					geometryBonds.translate( offset.x, offset.y, offset.z );

					// Add atoms to the model group
					let positions = geometryAtoms.getAttribute( 'position' );
					let colors = geometryAtoms.getAttribute( 'color' );

					for ( var i = 0; i < positions.count; i ++ ) {

						position.x = positions.getX( i );
						position.y = positions.getY( i );
						position.z = positions.getZ( i );

						color.r = colors.getX( i );
						color.g = colors.getY( i );
						color.b = colors.getZ( i );

						let object = new THREE.Mesh( atomGeometry, new THREE.MeshStandardMaterial( { color: color } ) );
						object[ 'name' ] = 'atom_' + i;

						object.position.copy( position );
						object.position.multiplyScalar( 75 );
						object.scale.multiplyScalar( 25 );

						// Add atomic text to the atom's userData
						let atom = json.atoms[ i ];
						object.userData[ 'Element' ] = atom[ 4 ];

						atoms.push( object );
						root.add( object );

					}

					// Add bonds between atoms to the model group
					positions = geometryBonds.getAttribute( 'position' );

					for ( var i = 0; i < positions.count; i += 2 ) {

						start.x = positions.getX( i );
						start.y = positions.getY( i );
						start.z = positions.getZ( i );

						end.x = positions.getX( i + 1 );
						end.y = positions.getY( i + 1 );
						end.z = positions.getZ( i + 1 );

						start.multiplyScalar( 75 );
						end.multiplyScalar( 75 );

						let object = new THREE.Mesh( bondGeometry, new THREE.MeshStandardMaterial( { color: 0xFFFFFF } ) );
						object[ 'name' ] = 'bond_' + parseInt( i / 2 );

						object.position.copy( start );
						object.position.lerp( end, 0.5 );
						object.scale.set( 5, 5, start.distanceTo( end ) );
						object.lookAt( end );

						bonds.push( object );
						root.add( object );
					}

					// Scale and add model groups to the scene
					root.scale.multiplyScalar( 0.05 );

					editor.execute( new AddObjectCommand( editor, root ) );

				}, false );
				reader.readAsText( file );

				break;

			}

Live example

Screenshots

No response

Version

r153

Device

Desktop

Browser

Chrome, Firefox

OS

Windows

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 23 (2 by maintainers)

Most upvoted comments

I’ll do a PR with the change.

Ok, I’ll do the wipe thing at the end of parse in some days.

I’ll tried it first however it didn’t completely solve the issue. I still got runtime errors during serialization.

All right then, go ahead.

although the former solution seems to me simpler and more readable.

I’ll tried it first however it didn’t completely solve the issue. I still got runtime errors during serialization.

I was unsure if delete could garbage-collect the two members. They won’t, since they are still referenced by the two local variables, right?

Yeah they’ll be kept around because you’ve assigned them to other local variables.

It was easy. I’ve made it by cloning the material (before cloning I unhang temporarily the non-serializable fields, there are only 2 of them) This has the benefit of not touching the materials in the loader library, so the loader is not affected and can be used to parse more files.

In LDrawLoader.applyMaterialsToMesh(), inside the local getMaterial() function, at the end, I’ve changed return material; by: (edited)

	let result = material;
	if ( finalMaterialPass ) {

		// Clone the material without non-serializable fields

		const edgeMaterial = material.userData.edgeMaterial;
		const conditionalEdgeMaterial = material.userData.conditionalEdgeMaterial;

		delete material.userData.edgeMaterial;
		delete material.userData.conditionalEdgeMaterial;

		result = material.clone();

		material.userData.edgeMaterial = edgeMaterial;
		material.userData.conditionalEdgeMaterial = conditionalEdgeMaterial;

	}

	return result;

cc @gkjohnson Do you see it okay?

Okay, thanks for the explanation!

Storing things in userData is of course fine as long as it does not break serialization/deserialization. But if the contents of userData created by LDRAWLoader have no relevance for the app, it’s indeed best to clean things up.

A model material related to a mesh has associated the material for the edge lines of the same object. This one in turn can also have associated a conditional edges material (edges with a sort of back-face auto-culling)

This is only needed for building the model while parsing so perhaps the second solution I proposed is the best one (I can give it a try)

I did put them in userData because at the time the use of this field was more common. In SVGLoader for example the path style is also in the path userData.

If the class ConditionalLineSegments is not serializable itself, then I think the conditional segments can’t be saved but I’m not sure about this.

If I am not wrong, without using @gkjohnson parts repository will have the editor be limited to loading only pre-packed mpd files. This is quick and efficient since loading parts from the repo does produce lots of traffic.

I only created this issue so it could be looked at. You guys can handle it and close this at any time.