3. Setting up Views

Summary

In this chapter, we will walk through setting the main focus of our application: the viewers.

Concepts


The src/js/app.js file will contain our main application logic, as well as manage the setup and handling of our WebViewer objects.

To better organize our code, we will be breaking up our code and using the external module approach for this development. We will be using the ES6 class syntax to encapsulate functionality. Let us start by making the main class in our app.js file. This is where we will instantiate our viewers and build our application through composition from other objects we will create later. Right now, we will just leave the class and constructor blank and return to it later.

class main {
constructor() {
}
} // End main class

As discussed in the Building A Basic Application tutorial, HOOPS Communicator requires that the DOM content and structure be loaded before trying to instantiate any WebViewer objects, since they will look for a <div> id to populate. Because of this, we will instantiate our main class within the onload browser event callback, once all the DOM content has been loaded.

Lastly, since we have provided the CSS file for you, you can import the CSS at the top of the app.js file. Your app.js file should now look something like this:

import '../css/tutorial-transforms.css';
// Application logic will begin once DOM content is loaded
window.onload = () => {
const app = new main();
};
class main {
constructor() {
}
} // End main class

At this point, you are welcome to run npm run build in the command line to start our webpack server and serve the frontend framework that we have provided. You can leave the development server running, and it will update your frontend as we write more code. You should see something like this before writing any additional Javascript:

As you can see, our template has both a "Main Viewer" and an "Overhead Viewer". The main viewer will provide user interaction with the scene and provide the canvas for user control. The overhead viewer will serve as another camera in the scene, fixed above the scene, pointing straight down the -Z scene axis. The overhead viewer will not have user interaction (though you could enable it, if so desired).

Because we have provided the HTML and styling, we should see <div> elements present that we can instantiate our WebViewers into. We will use these div IDs when setting up our viewers.

<!-- Main Viewer Component w/ Interaction -->
<div id="canvas" class="comp-container">
<div id="viewer"></div>
<div id="viewer-label" class="comp-label">Main View</div>
</div>
<!-- Overhead Viewer Component for a Top-Down View -->
<div id="subcanvas" class="comp-container">
<div id="subviewer"></div>
<div id="subviewer-label" class="comp-label">Overhead View</div>
</div>

We can see that the div IDs we are interested in are called "viewer" and "subviewer". We will do the same procedure documented in Building A Basic Application to instantiate two empty viewers, but this time twice – once for each viewer. The following code will be added to the constructor of your main class, using the "viewer" and "subviewer" div IDs. We will store each of our WebViewer objects in an array belonging to the main class, so we can keep track of them and more efficiently make calls to both of them.

class main {
constructor() {
// Instantiate two viewers for two different views
const mainViewer = new Communicator.WebViewer({
containerId: "viewer",
empty: true
});
const overheadViewer = new Communicator.WebViewer({
containerId: "subviewer",
empty: true
});
this._viewerList = [mainViewer, overheadViewer];
} // End main constructor
} // End main class

If you look back at your web application, you will see nothing has changed. This is because we have not yet started the viewers, just instantiated them in memory. Since a lot of the initial setup between both viewers is going to be the same, we can use the Array.prototype.map() function in ES6 to write the code once, but apply it to each viewer. Use the map() function to start both viewers within the constructor of our main class.

this._viewerList.map( (viewer) => {
viewer.start();
viewer.setCallbacks({
modelStructureReady: () => {
// Create Printing Plane
// Load Model
// Set initial cameras
// Set viewer backgrounds
}
}); // End Callbacks on Both Viewers
}); // End map

You can see from the comments that we will be doing several things once this modelStructureReady callback is called. Let’s work through each one, though we will skip the printing plane for now – we will implement that in the next section.

Since we will potentially be loading an arbitrary number of models in our application, we can abstract the loading into its own loadModel function, as a part of the main class. We can track the models that have been loaded in an array attached to the main class. The loadModel function will take two parameters – the model name to be loaded, and the WebViewer the model should be loaded into.

Within the function, we want to be sure we are adding our model to the model tree in a smart manner. Much like in the Getting Starting guide, we will create a new model node off the root node, name it, and load our model to that node.

// Function to load models and translate them so they are loaded
// at the origin and above the printing plane
loadModel(modelName, viewer) {
const modelNum = viewer.model.getNodeChildren(viewer.model.getAbsoluteRootNode()).length;
const nodeName = "Model-" + (modelNum + 1);
const modelNodeId = viewer.model.createNode(null, nodeName);
this._modelList.push(modelName); // Instantiated in next code snippet
viewer.model.loadSubtreeFromScsFile(modelNodeId, "/data/" + modelName + ".scs");
}

With this function written, we can call it in our modelStructureReady callback. Also note the newly created property _modelList to track the list of loaded models.

this._viewerList = [mainViewer, overheadViewer];
this._modelList = [];
this._viewerList.map( (viewer) => {
viewer.start();
viewer.setCallbacks({
modelStructureReady: () => {
// Create Printing Plane
// Load Model
this.loadModel("microengine", viewer);
// Set initial cameras
// Set viewer backgrounds
}
}); // End Callbacks on Both Viewers
}); // End map

Recall from Building A Basic Application using the modelStructureReady callback. For more information, consult our API Reference for the CallbackMap.

At last! If you take a look over at our application, you will see both viewers with the model loaded and rendered!

By default, both viewers will load the model identically, but we know we would like to have two separate camera views for these viewers, so let’s set those next.

To make a new Camera view, we need to specify all the parameters that make up the Camera state. This consists of three main vectors, specifying the position in world space, the target at which to point, and what direction should be considered "up". We also specify the projection mode, which we make perspective. We also will specify a camera view width and height. Lastly, we can optionally provide a near clipping limit, where anything within that distance to the camera will be clipped. For the sake of this tutorial, we have provided the camera parameters for you, but you can adjust these as you wish. Under the comment "Set initial camera" within the modelStructureReady callback, write the following code to set the cameras for each viewer. You should see your application update to reflect these camera changes.

// Set the cameras for the two viewers
let camPos, target, upVec;
switch (viewer) {
case mainViewer:
camPos = new Communicator.Point3(-1000, -1000, 1000);
target = new Communicator.Point3(0, 0, 0);
upVec = new Communicator.Point3(0, 0, 1);
break;
case overheadViewer:
camPos = new Communicator.Point3(0, 0, 1000);
target = new Communicator.Point3(0, 0, 0);
upVec = new Communicator.Point3(0, 1, 0);
break;
default:
alert('Error: No WebViewer Objects Detected.');
}
const defaultCam = Communicator.Camera.create(camPos, target, upVec, 1, 720, 720, 0.01);
viewer.view.setCamera(defaultCam);

You may be wondering why we set the camera so far away from our model, but that’s okay. When we add the printing plane, our default cameras will be appropriately scaled.

Finally, we will spice up our viewers by adding a colored background consistent with our UI theme. This is as simple as calling setBackgroundColor on the Communicator.View object. You can specify a single background color, or make a gradient from top to bottom by providing two colors. We will choose to make the gradient. Add the following below the "Set viewer backgrounds" comment.

// Background color for viewers
viewer.view.setBackgroundColor(new Communicator.Color(0, 153, 220), new Communicator.Color(218, 220, 222));

This should round out the modelStructureReady callback functions that apply both viewers (except for the printing plane, which we will address in the next section). Your "map" function should look something like this now:

this._viewerList.map( (viewer) => {
viewer.start();
viewer.setCallbacks({
modelStructureReady: () => {
// Create Printing Plane
// Load Model
this.loadModel("microengine", viewer);
// Set the cameras for the two viewers
let camPos, target, upVec;
switch (viewer) {
case mainViewer:
camPos = new Communicator.Point3(-1000, -1000, 1000);
target = new Communicator.Point3(0, 0, 0);
upVec = new Communicator.Point3(0, 0, 1);
break;
case overheadViewer:
camPos = new Communicator.Point3(0, 0, 1000);
target = new Communicator.Point3(0, 0, 0);
upVec = new Communicator.Point3(0, 1, 0);
break;
default:
alert('Error: No WebViewer Objects Detected. Report to TS3D.');
}
const defaultCam = Communicator.Camera.create(camPos, target, upVec, 1, 720, 720, 0.01);
viewer.view.setCamera(defaultCam);
// Background color for viewers
viewer.view.setBackgroundColor(new Communicator.Color(0, 153, 220), new Communicator.Color(218, 220, 222));
}
}); // End Callbacks on Both Viewers
}); // End map

However, we can add some additional calls for our main viewer, that we don’t want to show in the overhead viewer. We can either add a conditional in our "mapped" modelStructureReady to check which viewer we are using, and then set options if it is the main viewer, or we can just write another modelStructureReady callback outside our map function on the first viewer alone. You can set multiple callback functions on the same event without issue, so let’s demonstrate that.

When the modelStructureReady callback is fired, we should be able to add some additional elements to our scene, like a navigation cube and axis triad. By default, you can access the navigation cube and axis triad via the Communicator.View object in the WebViewer. We will retrieve these objects, enable them, and move the navigation cube from its default position to the lower right corner of the scene.

// Set additional callbacks for main viewer only
mainViewer.setCallbacks({
modelStructureReady: () => {
// Additional options for modelStructureReady that we did not want in both viewers
mainViewer.view.getAxisTriad().enable();
mainViewer.view.getNavCube().enable();
mainViewer.view.getNavCube().setAnchor(Communicator.OverlayAnchor.LowerRightCorner);
}
});

Notice we are only setting this callback on the mainViewer object, not both like before.

Lastly, we mentioned we did not want any interaction in the overhead viewer, so let’s disable any operators for that viewer. By default, selection and navigation is enabled. We can simply remove these operators by clearing the operator stack tracked by the operatorManager property on the WebViewer.

overheadViewer.operatorManager.clear();

With that, we should have a good-looking application constructor. We will revisit this constructor as needed.

constructor() {
// Instantiate two viewers for two different views
const mainViewer = new Communicator.WebViewer({
containerId: "viewer",
empty: true
});
const overheadViewer = new Communicator.WebViewer({
containerId: "subviewer",
empty: true
});
this._viewerList = [mainViewer, overheadViewer];
this._modelList = [];
this._viewerList.map((viewer) => {
viewer.start();
viewer.setCallbacks({
modelStructureReady: () => {
// Create Printing Plane
// Load Model
this.loadModel("microengine", viewer);
// Set the cameras for the two viewers
let camPos, target, upVec;
switch (viewer) {
case mainViewer:
camPos = new Communicator.Point3(-1000, -1000, 1000);
target = new Communicator.Point3(0, 0, 0);
upVec = new Communicator.Point3(0, 0, 1);
break;
case overheadViewer:
camPos = new Communicator.Point3(0, 0, 1000);
target = new Communicator.Point3(0, 0, 0);
upVec = new Communicator.Point3(0, 1, 0);
break;
default:
alert('Error: No WebViewer Objects Detected. Report to TS3D.');
}
const defaultCam = Communicator.Camera.create(camPos, target, upVec, 1, 720, 720, 0.01);
viewer.view.setCamera(defaultCam);
// Background color for viewers
viewer.view.setBackgroundColor(new Communicator.Color(0, 153, 220), new Communicator.Color(218, 220, 222));
}
}); // End Callbacks on Both Viewers
}); // End map
// Set additional callbacks for main viewer only
mainViewer.setCallbacks({
modelStructureReady: () => {
// Additional options for modelStructureReady that we did not want in both viewers
mainViewer.view.getAxisTriad().enable();
mainViewer.view.getNavCube().enable();
mainViewer.view.getNavCube().setAnchor(Communicator.OverlayAnchor.LowerRightCorner);
}
}); // End Callbacks
// Do not want any interaction in the overhead viewer, so we will disable all operators
overheadViewer.operatorManager.clear();
} // End main constructor

top_level:1 tutorials:1