Repository: mrdoob/three.js (JavaScript)
Date: 2019-11-08 14:13:48+00:00
Task type: Bug Fix

Issue:
Exception if LOD has no levels defined
##### Description of the problem
This is a very simple problem in the code of `getObjectForDistance` in `LOD.js`:
```
	getObjectForDistance: function ( distance ) {
		var levels = this.levels;
		for ( var i = 1, l = levels.length; i < l; i ++ ) {
			if ( distance < levels[ i ].distance ) {
				break;
			}
		}
		return levels[ i - 1 ].object;
	},
```

If there's no level defined, the last line `return levels[i-1].object` will throw an exception. The code should look something like `return (i > 0) ? levels[i-1].object : undefined`.

I ran into this in a corner case when using `react-three-fiber` and I understand this might not be an issue for other use cases. But I would appreciate if we could fix it. I can create a simple pull request if desired and helpful.

##### Three.js version

- [ ] Dev
- [x] r109

##### Browser

- [x] All of them
- [ ] Chrome
- [ ] Firefox
- [ ] Internet Explorer

##### OS

- [x] All of them
- [ ] Windows
- [ ] macOS
- [ ] Linux
- [ ] Android
- [ ] iOS

##### Hardware Requirements (graphics card, VR Device, ...)



Issue discussion:
In this case, it's also necessary to change `LOD.raycast()`. Even with your proposed change, the method would throw an exception because of the following line.
```js
this.getObjectForDistance( distance ).raycast( raycaster, intersects );
```
If no levels are defined, `getObjectForDistance()` would return `undefined` which causes a runtime error.

In any event, there are arguments for both approaches (leaving `LOD` as it is and assume levels are defined or make the methods more robust). I personally would prefer when the methods do not fail if an instance of `LOD` is in an initial state.

Solution patch:
diff --git a/src/objects/LOD.d.ts b/src/objects/LOD.d.ts
--- a/src/objects/LOD.d.ts
+++ b/src/objects/LOD.d.ts
@@ -10,9 +10,10 @@ export class LOD extends Object3D {
 	type: 'LOD';
 
 	levels: { distance: number; object: Object3D }[];
+	autoUpdate: boolean;
 
 	addLevel( object: Object3D, distance?: number ): this;
-	getObjectForDistance( distance: number ): Object3D;
+	getObjectForDistance( distance: number ): Object3D | null;
 	raycast( raycaster: Raycaster, intersects: Intersection[] ): void;
 	update( camera: Camera ): void;
 	toJSON( meta: any ): any;
diff --git a/src/objects/LOD.js b/src/objects/LOD.js
--- a/src/objects/LOD.js
+++ b/src/objects/LOD.js
@@ -47,6 +47,8 @@ LOD.prototype = Object.assign( Object.create( Object3D.prototype ), {
 
 		}
 
+		this.autoUpdate = source.autoUpdate;
+
 		return this;
 
 	},
@@ -81,27 +83,39 @@ LOD.prototype = Object.assign( Object.create( Object3D.prototype ), {
 
 		var levels = this.levels;
 
-		for ( var i = 1, l = levels.length; i < l; i ++ ) {
+		if ( levels.length > 0 ) {
+
+			for ( var i = 1, l = levels.length; i < l; i ++ ) {
 
-			if ( distance < levels[ i ].distance ) {
+				if ( distance < levels[ i ].distance ) {
 
-				break;
+					break;
+
+				}
 
 			}
 
+			return levels[ i - 1 ].object;
+
 		}
 
-		return levels[ i - 1 ].object;
+		return null;
 
 	},
 
 	raycast: function ( raycaster, intersects ) {
 
-		_v1.setFromMatrixPosition( this.matrixWorld );
+		var levels = this.levels;
+
+		if ( levels.length > 0 ) {
 
-		var distance = raycaster.ray.origin.distanceTo( _v1 );
+			_v1.setFromMatrixPosition( this.matrixWorld );
 
-		this.getObjectForDistance( distance ).raycast( raycaster, intersects );
+			var distance = raycaster.ray.origin.distanceTo( _v1 );
+
+			this.getObjectForDistance( distance ).raycast( raycaster, intersects );
+
+		}
 
 	},
 


Test patch:
diff --git a/test/unit/src/objects/LOD.tests.js b/test/unit/src/objects/LOD.tests.js
--- a/test/unit/src/objects/LOD.tests.js
+++ b/test/unit/src/objects/LOD.tests.js
@@ -3,6 +3,8 @@
  */
 /* global QUnit */
 
+import { Object3D } from '../../../../src/core/Object3D';
+import { Raycaster } from '../../../../src/core/Raycaster';
 import { LOD } from '../../../../src/objects/LOD';
 
 export default QUnit.module( 'Objects', () => {
@@ -10,50 +12,114 @@ export default QUnit.module( 'Objects', () => {
 	QUnit.module( 'LOD', () => {
 
 		// INHERITANCE
-		QUnit.todo( "Extending", ( assert ) => {
+		QUnit.test( "Extending", ( assert ) => {
 
-			assert.ok( false, "everything's gonna be alright" );
+			var lod = new LOD();
+
+			assert.strictEqual( ( lod instanceof Object3D ), true, "LOD extends from Object3D" );
 
 		} );
 
-		// INSTANCING
-		QUnit.todo( "Instancing", ( assert ) => {
+		// PROPERTIES
+		QUnit.test( "levels", ( assert ) => {
 
-			assert.ok( false, "everything's gonna be alright" );
+			var lod = new LOD();
+			var levels = lod.levels;
+
+			assert.strictEqual( Array.isArray( levels ), true, "LOD.levels is of type array." );
+			assert.strictEqual( levels.length, 0, "LOD.levels is empty by default." );
 
 		} );
 
-		// PROPERTIES
-		QUnit.todo( "levels", ( assert ) => {
+		QUnit.test( "autoUpdate", ( assert ) => {
 
-			assert.ok( false, "everything's gonna be alright" );
+			var lod = new LOD();
+
+			assert.strictEqual( lod.autoUpdate, true, "LOD.autoUpdate is of type boolean and true by default." );
 
 		} );
 
 		// PUBLIC STUFF
-		QUnit.todo( "isLOD", ( assert ) => {
+		QUnit.test( "isLOD", ( assert ) => {
 
-			assert.ok( false, "everything's gonna be alright" );
+			var lod = new LOD();
+
+			assert.strictEqual( lod.isLOD, true, ".isLOD property is defined." );
 
 		} );
-		QUnit.todo( "copy", ( assert ) => {
+		QUnit.test( "copy", ( assert ) => {
 
-			assert.ok( false, "everything's gonna be alright" );
+			var lod1 = new LOD();
+			var lod2 = new LOD();
+
+			var high = new Object3D();
+			var mid = new Object3D();
+			var low = new Object3D();
+
+			lod1.addLevel( high, 5 );
+			lod1.addLevel( mid, 25 );
+			lod1.addLevel( low, 50 );
+
+			lod1.autoUpdate = false;
+
+			lod2.copy( lod1 );
+
+			assert.strictEqual( lod2.autoUpdate, false, "LOD.autoUpdate is correctly copied." );
+			assert.strictEqual( lod2.levels.length, 3, "LOD.levels has the correct length after the copy." );
 
 		} );
-		QUnit.todo( "addLevel", ( assert ) => {
+		QUnit.test( "addLevel", ( assert ) => {
 
-			assert.ok( false, "everything's gonna be alright" );
+			var lod = new LOD();
+
+			var high = new Object3D();
+			var mid = new Object3D();
+			var low = new Object3D();
+
+			lod.addLevel( high, 5 );
+			lod.addLevel( mid, 25 );
+			lod.addLevel( low, 50 );
+
+			assert.strictEqual( lod.levels.length, 3, "LOD.levels has the correct length." );
+			assert.deepEqual( lod.levels[ 0 ], { distance: 5, object: high }, "First entry correct." );
+			assert.deepEqual( lod.levels[ 1 ], { distance: 25, object: mid }, "Second entry correct." );
+			assert.deepEqual( lod.levels[ 2 ], { distance: 50, object: low }, "Third entry correct." );
 
 		} );
-		QUnit.todo( "getObjectForDistance", ( assert ) => {
+		QUnit.test( "getObjectForDistance", ( assert ) => {
 
-			assert.ok( false, "everything's gonna be alright" );
+			var lod = new LOD();
+
+			var high = new Object3D();
+			var mid = new Object3D();
+			var low = new Object3D();
+
+			assert.strictEqual( lod.getObjectForDistance( 5 ), null, "Returns null if no LOD levels are defined." );
+
+			lod.addLevel( high, 5 );
+
+			assert.strictEqual( lod.getObjectForDistance( 0 ), high, "Returns always the same object if only one LOD level is defined." );
+			assert.strictEqual( lod.getObjectForDistance( 10 ), high, "Returns always the same object if only one LOD level is defined." );
+
+			lod.addLevel( mid, 25 );
+			lod.addLevel( low, 50 );
+
+			assert.strictEqual( lod.getObjectForDistance( 0 ), high, "Returns the high resolution object." );
+			assert.strictEqual( lod.getObjectForDistance( 10 ), high, "Returns the high resolution object." );
+			assert.strictEqual( lod.getObjectForDistance( 25 ), mid, "Returns the mid resolution object." );
+			assert.strictEqual( lod.getObjectForDistance( 50 ), low, "Returns the low resolution object." );
+			assert.strictEqual( lod.getObjectForDistance( 60 ), low, "Returns the low resolution object." );
 
 		} );
-		QUnit.todo( "raycast", ( assert ) => {
+		QUnit.test( "raycast", ( assert ) => {
 
-			assert.ok( false, "everything's gonna be alright" );
+			var lod = new LOD();
+			var raycaster = new Raycaster();
+			var intersections = [];
+
+			lod.raycast( raycaster, intersections );
+
+			assert.strictEqual( intersections.length, 0, "Does not fail if raycasting is used with a LOD object without levels." );
 
 		} );
 		QUnit.todo( "update", ( assert ) => {