8. Syncing Multiple Viewers

Summary

In this chapter, we will create a viewer helper class to keep our viewer states synchronized.

Concepts


There are a few ways you could approach synchronizing both viewers, like passing in the second viewer to the operator and making the same calls again on that viewer, but that would create high coupling between separate WebViewer objects. Therefore, you can abstract that same concept into an external helper class that manages any viewers in the application. While you only have two viewers in this application, this can be extended to an arbitrary number of viewers.

In your src/js folder, create a new file called syncHelper.js. Open the file, and create a class called syncHelper and make it the default export for the module. The purpose of this class is to query the model state in the main viewer (the source of truth), and then update all other viewers attached. The constructor will take in an array of WebViewer objects and assign the first element as the mainViewer. All subsequent viewers are "attached viewers" that are updated to reflect the main viewer's state. We will also track the nodes of the main viewer, so we don't need to iterate through the model tree every time to find them. Lastly, we will store a Boolean indicating if we need to update the model nodes or not.

export default class syncHelper {
constructor(viewerList) {
// Copy the array, so we do not modify the reference values
let tfViewerList = viewerList.slice(0);
// Assign the first element to the main viewer
this._mainViewer = tfViewerList.shift();
// All remaining viewers are attached
this._attachedViewers = tfViewerList;
this._needsUpdate = true;
this._modelTreeNodes = [];
}
}

We want to have a function that executes when we have changed any node transforms in our main viewer, like we do with the transformOperator class. The function will gather all the transforms for the node IDs provided (an argument to the function), and make sure the attached viewers set their model nodes to the values. You can use a Map to store the nodeId and its transformation matrix as a key-value pair. Then use that Map to set the node matrices for the attached viewers.

syncNodeTransforms(nodeIds = []) {
let matMap = new Map();
for (let node of nodeIds) {
matMap.set(node, this._mainViewer.model.getNodeMatrix(node));
}
for (let [node, matrix] of matMap.entries()) {
this._attachedViewers.map( (viewer) => {
viewer.model.setNodeMatrix(node, matrix);
});
}
}

Lastly, let's write some accessor member functions for later use.

setNeedsUpdate(option) {
this._needsUpdate = option;
}
getModelTreeNodes() {
return this._modelTreeNodes;
}
getMainViewer() {
return this._mainViewer;
}
getAttachedViewers() {
return this._attachedViewers;
}

With this in place, let's go back and instantiate this syncHelper class in our main application. Once again, include the syncHelper.js file.

import '../css/tutorial-am.css';
import printingPlane from "./printingPlane.js";
import transformOperator from "./transformOperator.js";
import syncHelper from "./syncHelper";

Once you have instantiated all your WebViewer objects and stored them in an array, you can instantiate a syncHelper object. Add the following code to the constructor in app.js.

this._viewerList = [mainViewer, overheadViewer];
this._viewSync = new syncHelper(this._viewerList);

Now any object that plans to modify the scene in the main viewer can be passed this syncHelper object, and it will manage updating the scene in the attached viewers. To illustrate, let's modify our transformOpertator constructor to take a syncHelper object instead. Our instantiation in the app.js file will now look like this:

this._transformOp = new transformOperator(this._viewSync);

Revisiting transformOperator.js, we will modify the constructor to reflect these changes.

constructor(viewSync) {
super(viewSync.getMainViewer());
this._mainViewer = viewSync.getMainViewer();
this._viewSync = viewSync;
}

Now that you have the viewSync object available in this function, we can pass the selection results we used to add the handles to the syncNodeTransforms method of the viewSync object. The onMouseMove function would now look like this:

onMouseMove(event) {
super.onMouseMove(event);
if (this.isDragging()) {
const selectionItems = this._mainViewer.selectionManager.getResults();
let nodeIds = [];
selectionItems.map((selectionItem) => {
nodeIds.push(selectionItem.getNodeId());
});
this._viewSync.syncNodeTransforms(nodeIds);
transformOperator.setMatrixText(this._mainViewer.model.getNodeNetMatrix(nodeIds[0]));
}
}

As you can see we are telling the helper object to check the transforms on the provided nodeIds of selected nodes. Let's see how the handle operator now works in the main application.

Let's build out our syncHelper a bit more. We could expand the syncNodeTransforms function to check against the entire model scene too, and not just the provided node IDs. By keeping track of the nodes of the master model tree in the main viewer, we can always sync all corresponding nodes in each attached viewer. To do this, we will build up a flattened array of all model node IDs and store them as a syncHelper property. When the model nodes change, we will request an update to the nodes.

syncNodeTransforms(nodeIds = []) {
if (this._needsUpdate) {
this._modelTreeNodes = [];
this._gatherAllNodeIds(this._mainViewer.model.getAbsoluteRootNode(), this._modelTreeNodes);
this._modelTreeNodes = this._modelTreeNodes.filter(Boolean);
this._needsUpdate = false;
}
nodeIds = nodeIds.length == 0 ? this._modelTreeNodes : nodeIds;
let matMap = new Map();
for (let node of nodeIds) {
matMap.set(node, this._mainViewer.model.getNodeMatrix(node));
}
for (let [node, matrix] of matMap.entries()) {
this._attachedViewers.map((viewer) => {
viewer.model.setNodeMatrix(node, matrix);
});
}
}
_gatherAllNodeIds(parent, nodeIds) {
nodeIds.push(parent);
let children = this._mainViewer.model.getNodeChildren(parent);
if (children.length !== 0) {
for (let child of children) {
this._gatherAllNodeIds(child, nodeIds);
}
}
}

top_level:1 tutorials:1