Setting Up Views
Summary
In this chapter, we will walk through setting the main focus of our application: the viewers.
Concepts
Instantiating and starting the WebViewers
Setting callbacks
Configuring additional WebViewer options
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 start, we have provided the starter code and will implement each function in the following steps.
At this point, you can run npm start
on the command line to start the development server provided with the tutorial. The server will automatically update the browser as you save changes. You should see this:
Note
Make sure you run npm install
before starting the server!
As you can see, our template has both a “Main View” and an “Overhead View”. 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).
In the HTML provided, you can see <div>
elements present that will serve as containers for our Web 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>
The container IDs we are interested in are viewer
and subviewer
. The following code will be added to the constructor of your main class, using the viewer
and subviewer
div IDs. We will store each WebViewer object in an array.
Add the following to the beginning of the constructor()
starter code provided in the file app.js:
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
});
// Set class properties
this._viewerList = [mainViewer, overheadViewer];
// Start viewers
this._viewerList.map( (viewer) => viewer.start() );
// Set up viewers
this.setViewerCallbacks();
this.configureOperators();
this.setEventListeners();
} // End main constructor
} // End main class
If you look back at your web application, you will see nothing has changed. At this point we have started the viewers but have not loaded a model. A lot of the initial setup between both viewers is going to be the same, so we can use Array.prototype.map()
to apply the common logic to each viewer. We have provided the function setViewerCallbacks()
in the starter code, which is where we will add this logic.
To start, we will create named instances of our viewers within setViewerCallbacks()
:
setViewerCallbacks() {
let mainViewer = this._viewerList[0];
let overheadViewer = this._viewerList[1];
}
Next, we will set up the map()
callback function. For each viewer, we will set a Web Viewer callback for modelStructureReady
and sceneReady
. By using callbacks, we can ensure our set up and configuration logic is executed at the appropriate time during the Web Viewer lifecycle. You can find additional details in our Programming Guide article about callbacks.
Append the following to setViewerCallbacks()
:
this._viewerList.map( (viewer) => {
viewer.setCallbacks({
modelStructureReady: () => {
// Create Printing Plane
// Load Model
},
sceneReady: () => {
// Set the cameras for the two viewers
// Background color for viewers
}
}); // End Callbacks on Both Viewers
}); // End map
You can see from the comments what we intend for each callback. Let’s work through each one, though we will skip the printing plane for now – we will implement that in the next section.
We can abstract the loading process into its own loadModel()
function, as a class method of main
. 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. We will create a new model node off the root node, name it, and load our model to that node.
Add the following to the loadModel()
function provided in the starter code:
// 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);
viewer.model.loadSubtreeFromScsFile(modelNodeId, "./data/" + modelName + ".scs")
.then(() => {
let loadMatrix = viewer.model.getNodeNetMatrix(modelNodeId);
viewer.model.getNodeRealBounding(modelNodeId)
.then((box) => {
loadMatrix.setTranslationComponent(box.min.x * -1, box.min.y * -1, box.min.z * -1);
viewer.model.setNodeMatrix(modelNodeId, loadMatrix, true);
});
});
}
We can call now call our new function in the modelStructureReady
callback:
this._viewerList.map( (viewer) => {
viewer.setCallbacks({
modelStructureReady: () => {
// Create Printing Plane
// Load Model
this.loadModel("microengine", viewer);
},
sceneReady: () => {
// Set the cameras for the two viewers
// Background color for viewers
}
}); // End Callbacks on Both Viewers
}); // End map
Note, the newly created class property _modelList
to track the list of loaded models. Add the new class property to the constructor()
:
// Set class properties
this._viewerList = [mainViewer, overheadViewer];
this._modelList = [];
Recall from Building a Basic Application using the modelStructureReady
callback. For more information, consult our API Reference for the CallbackMap.
At last! Reload your browser and you will see both viewers with the model loaded and rendered! But notice the camera position is not very helpful, so let’s address that next.
By default, both viewers will load the model identically, but we know we would like to have two separate camera views for these viewers. 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:
Position in world space
Target at which to point
What direction should be considered “up”
Also, we specify the projection mode and the camera view width and height. Last, 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. Add the following to the sceneReady
callback because we do not want to update the scene until it is ready, which may happen after the modelStructureReady
event. You should see your application update to reflect these camera changes.
sceneReady: () => {
// 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);
}
Finally, we will spice up our viewers by adding a colored background consistent with our UI theme. This can be done by 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 this last line to sceneReady
:
// Background color for viewers
viewer.view.setBackgroundColor(new Communicator.Color(0, 153, 220), new Communicator.Color(218, 220, 222));
This completes the callback functions that apply to both viewers (except for the printing plane, which we will address in the next section). Your setViewerCallbacks()
should look like this:
setViewerCallbacks() {
let mainViewer = this._viewerList[0];
let overheadViewer = this._viewerList[1];
this._viewerList.map((viewer) => {
viewer.setCallbacks({
modelStructureReady: () => {
// Create Printing Plane
// Load Model
this.loadModel("microengine", viewer);
},
sceneReady: () => {
// 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);
// 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
}
We can add additional callbacks for our main viewer, that we don’t want to show in the overhead viewer. We can do this a number of ways, but here we will write another modelStructureReady
callback outside our map function for just the main viewer. You can set multiple callback functions on the same event without issue, so let’s demonstrate that.
Append the following to setViewerCallbacks()
below this._viewerList.map(...)
function:
// Set additional callbacks for main viewer only
mainViewer.setCallbacks({
sceneReady: () => {
// Additional options for sceneReady that we do not want in both viewers
mainViewer.view.getAxisTriad().enable();
mainViewer.view.getNavCube().enable();
mainViewer.view.getNavCube().setAnchor(Communicator.OverlayAnchor.LowerRightCorner);
}
}); // End Callbacks on main viewer
When the sceneReady
callback is executed, 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.
Notice we are only setting this callback on the mainViewer
object, not both like before.
With that, we should have a good start to our application. We will revisit this code as needed throughout the rest of the tutorial.