CertainlyNotAdrian
CertainlyNotAdrian

Reputation: 377

Angular 7 ngZone throwing undefined error

I'm trying to render a 3d object using three.js. However, when I try to initialise the animation loop (within ngAfterViewInit) I keep getting the following error:

TypeError: Cannot read property 'ngZone' of undefined

In order to reduce cpu cost, I'm using ngZone to trigger requestAnimationFrame outside of Angular. Even when I remove the code for ngZone I still get the following error:

TypeError: Cannot read property 'animate' of undefined

This all occurs after the appropriate resources have been loaded. To avoid any confusion, there is no class-scope variable for ngZone, simply its call as a parameter in the constructor.

Code:

export class ProductComponent{
//setup variables
// shirt model and texutre are pulled from firebase storage
constructor(private storage : AngularFireStorage, public ngZone: NgZone) {

        this.modelUrl = this.storage.ref('path/to/model.obj').getDownloadURL();
        this.textureUrl = this.storage.ref('path/to/texture.jpg').getDownloadURL();

        this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
        this.scene = new THREE.Scene();
        this.controls = new THREE.OrbitControls(this.camera,this.renderer.domElement);
        this.clock = new THREE.Clock();
        this.manager = new THREE.LoadingManager();
        this.loader = new THREE.OBJLoader(this.manager);
    }

ngAfterViewInit(){
    //setup
    this.loadResources(this.modelValue, this.texture, this.scene, this.renderer, this.container, this.animate);
 }
private loadResources(model, texture, scene, renderer, container, callback){

    this.camera.position.set(0, 0, 50);
    this.camera.lookAt(new THREE.Vector3(0, 0, 0));

    // scene

    scene.fog = new THREE.FogExp2(0xffffff, 0.0003);

    const ambientLight = new THREE.AmbientLight(0xcccccc, 0.4);
    scene.add(ambientLight);

    const pointLight = new THREE.PointLight(0xffffff, 0.8);
    this.camera.add(pointLight);
    scene.add(this.camera);

    this.loader.load(model, function (object) {
        object.traverse(function (child) {

            if (child instanceof THREE.Mesh) {

                child.material.map = texture;

                // repeat image on model
                child.material.map.wrapS = child.material.map.wrapT = THREE.RepeatWrapping;
                child.material.map.repeat.set(4, 4);

                child.material.needsUpdate = true;

            }

        });

        object.scale.set(1.5, 1, 1.5);
        scene.add(object);
        console.log('PARTS:', object.children);

        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setClearColor(scene.fog.color);
        renderer.setSize(window.innerWidth, window.innerHeight);
        container.appendChild(renderer.domElement);

        callback();
    }); //onProgress, onError
}

animate() : void {
        this.ngZone.runOutsideAngular(() => {
            requestAnimationFrame(this.animate);
        });
        this.render();
        this.update();

    }

}

Upvotes: 0

Views: 2162

Answers (1)

Vadim
Vadim

Reputation: 3439

this.animate is called inside of the method loadResources. Here you're passing it as the last argument:

this.loadResources(this.modelValue, this.texture, this.scene, this.renderer, this.container, this.animate);

The problem is that this.animate will be called inside of callback of this.loader.load and this callback is a regular function, so this inside of animate will not have ngZone or animate. Possible solution is to use arrow function for callback of this.loader.load (because this.animate will be called inside of it):

private loadResources(model, texture, scene, renderer, container, callback) {

  this.camera.position.set(0, 0, 50);
  this.camera.lookAt(new THREE.Vector3(0, 0, 0));

  // scene
  scene.fog = new THREE.FogExp2(0xffffff, 0.0003);

  const ambientLight = new THREE.AmbientLight(0xcccccc, 0.4);
  scene.add(ambientLight);

  const pointLight = new THREE.PointLight(0xffffff, 0.8);
  this.camera.add(pointLight);
  scene.add(this.camera);

  // use arrow function for callback of this.loader.load
  this.loader.load(model, (object) => {

    object.traverse(function (child) {

      if (child instanceof THREE.Mesh) {

        child.material.map = texture;

        // repeat image on model
        child.material.map.wrapS = child.material.map.wrapT = THREE.RepeatWrapping;
        child.material.map.repeat.set(4, 4);

        child.material.needsUpdate = true;

      }
    });

    object.scale.set(1.5, 1, 1.5);
    scene.add(object);
    console.log('PARTS:', object.children);

    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setClearColor(scene.fog.color);
    renderer.setSize(window.innerWidth, window.innerHeight);
    container.appendChild(renderer.domElement);

    // this.animate
    callback();
  }); //onProgress, onError
}

Or, if you want to use a regular function as a callback for this.loader.load, you can bind this to callback:

// assign this to const that,
// so it can be used for binding
const that = this;
this.loader.load(model, function(object) {

  ...

  // this.animate
  const callbackWithThisBinding = callback.bind(that);
  callbackWithThisBinding();
}

Upvotes: 1

Related Questions