Threejs with Angular 10 Flex Layout

Hi guys,

So I am trying to build an application that is using Angular 10 with threejs.

I am loading an obj file into scene by selecting a file to load through File input. Upon selecting the file I can load the obj into a scene. This is working but it is not in the center of it’s respective div element that I am loading it into. See component.ts and component.html below

Edit 1

OK so the problem I am having is that the #rendererContainer I am passing to create the scene is not conforming to the angular flex layout rules. It seems to be doing it’s own thing. What is the correct way to set this up inside the component.ts file so that when it creates the scene it stays inside the bounds of my div element?

So a couple of questions

  • I am unsure what window.innerWidth & window.innerHeight are? Do these relate to the div container window size or the full window of the application?
  • I am not sure if this is the correct way of accessing the div container in question @ViewChild(‘rendererContainer’) rendererContainer: ElementRef;
  • Am I calling the resize event correctly i.e is this passing the size of the container element or am I passing the over all application window size?
  • How should I pass the correct size of the container window to my component.ts file?
  • My scene background is black. How do I change this?
  • How do I limit the size of the scene window - is that just a html thing using FxLayout?

Thanks in advance

I have the following code in my component.ts file

import {Component, ViewChild, ElementRef} from '@angular/core';
import * as THREE from 'three';
import { ResizedEvent } from 'angular-resize-event';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { OBJLoader2 } from 'three/examples/jsm/loaders/OBJLoader2';
import * as ROSLIB from 'roslib';

@Component({
  selector: 'app-part-view',
  templateUrl: './part-view.component.html',
  styleUrls: ['./part-view.component.css']
})

export class PartViewComponent 
{
  @ViewChild('rendererContainer') rendererContainer: ElementRef;

  axisHelper = new THREE.AxesHelper(500);

  renderer = new THREE.WebGLRenderer({antialias: true});
  scene = null;
  camera = null;
  mesh = null;
  controls = null;  
  ambientLight = null;
  object = null;
  objtext = null;

  ros: any;
  clr_cal_publisher:any;
  
  
  constructor() {        

      this.scene = new THREE.Scene();
      this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
      this.camera.position.z = 1000;
      
      this.ambientLight = new THREE.AmbientLight( 0xcccccc, 1 );
      this.scene.add( this.ambientLight );      

      this.controls = new OrbitControls( this.camera, this.renderer.domElement );
      this.controls.enableZoom = true;
      this.controls.enablePan = false;

      if (this.object != null )
      {
        this.scene.add(this.object);
      }

      this.scene.add(this.axisHelper);
  }

  
  ngAfterViewInit() {
      this.renderer.setSize(window.innerWidth, window.innerHeight);
      this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
      this.animate();
  }

  animate() {
      window.requestAnimationFrame(() => this.animate());
      if (this.object != null)
      this.renderer.render(this.scene, this.camera);
      this.controls.update();
  }
  
  onResize(event) {
      this.camera.aspect = (event.target.innerWidth)  / (event.target.innerHeight);
      this.renderer.setSize( (event.target.innerWidth), (event.target.innerHeight) );  
      this.camera.updateProjectionMatrix();
  }  

  loadObject(fileContents,rotationX,rotationY,rotationZ) {
    console.log("loadObject called "+ fileContents);
    this.objtext = fileContents;
    if (this.object != null )
      this.scene.remove(this.object);
      
    var loader = new OBJLoader2();   
    loader.setLogging(true, true);     
    loader.setUseIndices(true);
    loader.setDisregardNormals(true);
    this.object = loader.parse(this.objtext);
  
  
    var boxObj = new THREE.Box3();
  
    this.object.rotation.x = rotationX;
    this.object.rotation.y = rotationY;
    this.object.rotation.z = rotationZ;    
  
    var boxHelper = new THREE.BoxHelper( this.object );  
    boxObj.setFromObject( boxHelper );
  
    var cent = new THREE.Vector3();
    boxObj.getCenter(cent);      
      
    this.camera.position.set(0,0,-Math.max(Math.abs(cent.x),Math.abs(cent.y),Math.abs(cent.z)) -50);
  
    this.object.position.x = -cent.x;
    this.object.position.y = -cent.y;
    this.object.position.z = -cent.z;
     

    this.scene.add(this.object);
        
  }

  changeListener($event) : void {
    this.readThis($event.target);
  }

  readThis(inputValue: any) : void {
    var file:File = inputValue.files[0]; 
    var myReader:FileReader = new FileReader();
    var txt;
    let that =  this;
    myReader.onloadend = function(e){
      // you can perform an action with readed data here
      //console.log(myReader.result);
      console.log("Loading obj file");
      txt = myReader.result;
      that.loadObject(txt,0,0,0);            

    }

    myReader.readAsText(file);
  }

}

I have the following code inside my component.html

 <div>
  Select file:
  <input type="file" (change)="changeListener($event)">
</div>


<div class="container">
<div id="" class="flex-container" fxLayoutGap="50px" fxLayout="row" fxLayoutAlign="space-evenly none">
 
  <div id="visual_id" fxFlex="1 1 0" fxLayout >        
    <div #rendererContainer (window:resize)="onResize($event)" >            
    </div>
  </div>

 <div id="sidebar_id" fxFlex fxLayout fxLayoutGap="50px" >
            
  <mat-tab-group>
    <mat-tab label="First"> Content 1 </mat-tab>
    <mat-tab label="Second"> Content 2 </mat-tab>
    <mat-tab label="Third"> Content 3 </mat-tab>
  </mat-tab-group>

</div>  

Some of these questions can be found easily via search engines…

Using a ViewChild is one of many ways this can be achieved. You could also use normal JS document.getElementByID('id').append(renderer.domElement)

You can call your event like that, this will work, but to reduce performance issues when using Eventlisteners in general, you should add and also remove your events when needed, because when the component is destroyed and revoked, you could end up having two of the same event registered. To remove a Eventlistener, you have to store the instance in a variable.

this.resizeEvent = this.resizeEventFunction.bind(this)
window.addEventListener('resize', this.resizeEvent)
window.removeEventListener('resize', this.resizeEvent)

I dont know what you want exactly. Do you want to specify your size? In px or percent? Or same as window size? You can call window.innerWidth anywhere in your app or store it globally and reuse it.
Also your container can be styled with css

this.scene.backround = new THREE.Color(0xFFFFFF)
or 
this.renderer.setClearColor(0xFFFFFF)

This will basically set the size of the canvas element
this.renderer.setSize(w, h)

Also one big thing to keep in mind is that Angular’s change detection is always triggered when any Event, RequestAnimationFrame or setTimeout/setInterval is called. That means, if you have a requestAnimationFrame running, with each call, Angular will update the whole app. This behavior can really fast lead to really bad performance. Imagin running a AnimationFrame and also having various mouse events in your app. Change detection will run over and over again recalculating values and change DOM to display.

To prevent some of this behavior you want to disable change detection on certain events

ngzone-flags.ts:

(window as any).__Zone_disable_requestAnimationFrame = true; // WILL DISABLED CHANGE DETECTION ON ANIMATION FRAME
(window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'pointermove', 'wheel']; // BLACKLIST OF EVENTS TO NOT TRIGGER CHANGE DETECTION

include this file in your polyfills.ts
import './ngzone-flags

Another way is to inject NgZone and put specific events outside of Angulars zone like so

this.ngZone.runOutsideAngular( ()=> {

    // This event will not trigger change detection
    window.addEventListener('resize', this.resizeEvent) 
})

Thanks for the inputs! Some great insights there that I didn’t know about

The one that has me stumped at the moment is how to get the width and height of the div I pass into the component - in my case ‘rendererContainer’ .I was using the window itself which was totally wrong based upon your feedback

  ngAfterViewInit() {
      this.renderer.setSize(window.innerWidth, window.innerHeight);
      this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
      this.animate();
  }

As best I can tell I should be doing something like this but I’m bot sure how to get the correct width and height if the value is not valid at ngAfterViewInit. (It seems to be only valid when I wait a second or two). I presume there must be a better way to handle this scenario

 ngAfterViewInit() {
     
        setTimeout(() => {
       
        console.log("height is " + this.rendererContainer.nativeElement.offsetHeight); // this is correct
        console.log("width is " + this.rendererContainer.nativeElement.offsetWidth); // this is correct

       
      }, 2);

      this.renderer.setSize(this.rendererContainer.nativeElement.offsetWidth, this.rendererContainer.nativeElement.offsetHeight); // this ir NOT correct as there is no delay as above

      this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
      this.animate();
  }

Does this help? https://jsfiddle.net/5unsqg3w/

You have to set width and height of the div in CSS. Auto wont work I think.
I would declare some variables for width and height to have them consistent.
Also set the render size as well as the camera aspect.

Thanks for the inputs again !

Are saying that I have to hard code the size of the div element that I want to create the scene in ? And also hard code the values I pass to renderer.setSize() etc…?

If I went this route how would I handle a resize event if the values are hardcoded?

No i dont mean to hardcode it. You just have to calculate your wished width and height and set them in css and threejs. If you want half of the browser window to be the scene view, you would do something like that:

let container = document.querySelector(’#webGL’);
w = window.innerWidth / 2
h = window.innerHeight

container.style.width = w + ‘px’
container.style.height = h + ‘px’

renderer.setSize(w, h)

camera.aspect = w / h
camera.updateProjectionMatrix()

example: https://jsfiddle.net/53atkfbx/

Maybe you have to be more clear about what you actually want. The less I know, the less I can help

Apologies - I picked you up wrong

So basically I would like to set the area that the scene will be created by as default to be 2/3 of the width of the overall window. The other third of the screen will have a tab control to interact with the loaded mesh. If the user resizes the browser window then each component will resize based upon what fxLayout has set up to be - in this case

Where I think I am getting confused is having to set the div# rendererContainer. vs using the size of the window vs what does fxLayout need in Angular to make it responsive?

I have set the page up like this in component.html and in component.ts I get a reference to #rendererContainer and create my scene on it. (See code above)

  <div class="container" fxLayoutGap="10px" fxLayout="row">
      <div class="child-1" fxFlex="1 1 0">1. One
        <div #rendererContainer (window:resize)="onResize($event)" >            
        </div>             
      </div> 

      <div class="child-3" fxFlex>Tab section
        <mat-tab-group >
          <mat-tab label="First"> Content 1 </mat-tab>
          <mat-tab label="Second"> Content 2 </mat-tab>
          <mat-tab label="Third"> Content 3 </mat-tab>
        </mat-tab-group>
      </div>
  </div>

So my questions I think are

  • Do I need to set a width and height of a #div rendererContainer in css if I am using fxLayout?
  • How should I pass the size of the #div rendererContainer that fxLayout creates to this.renderer.setSize() in both ngAfterViewInit() and resize()

Does this make sense or am I approaching this incorrectly?

Ah okay now I see the problem. Unfortunately I didnt use angular fxLayout, but if its basic behavior is as flex box’s, I can give it a try.

To answer your questions:

Yes I think so, if you dont wanna set something specific, try to use percentages. The renderer needs to know its size, you cannot say stretch to fit its containing div.

this example does have a resizable/responsive 2/3 to 1/3 layout with flexbox:

Dont know how to make a gap between with flex tho.
Maybe someone else can help here

Hope this helps

Again appreciate your help to date

Yeah I think the real problem is integration of three js with Angular Flex Layout. As best I can tell when I use Angular Flex Layout and if I set the flex layout to be ‘row’ for div rendererContainer it’s only the width that is known and that is dynamic based upon the window size - not what I set the div rendererContainer width & height too in css etc… It doesn’t know height at the time it passes the div rendererContainer to the component.ts file to render the scene and it seems to ignore anything I put in the css etc…

So the only way I know how to get something working is ignoring trying to set the div size and instead, scaling the ‘window’ size in component.ts by a factor and using this sizes in renderer.setSize(0) but I’m not convinced this is a great way of doing it?.

So basically I am doing this and hard coding scale which is not great.

this.renderer.setSize(window.innerWidth/this.scale, window.innerHeight/this.scale);

Really I would like something that is more intuitive and integrates well with Angular Flex layout. It would be interesting to hear if anyone else has integrated threejs with angular Flex layout.