Syncing Multiple Viewers
Summary
In this chapter, we will create a viewer helper class to keep our viewer states synchronized.
Concepts
Managing multiple viewers
Traversing the model tree
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 we will only have two viewers in this application, the helper class can be extended to an arbitrary number of viewers.
In this section, we will be building the SyncHelper
class located in the file src/js/SyncHelper.js. 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.
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._nodeMapping = new Map();
}
}
We need a function that executes when a node transformation occurs in the mainViewer
. The function will gather all the transforms for the node IDs provided, and make sure the attachedViewers
set their model’s respective nodes to the same values.
We will use a Map
to store the nodeId
and its transformation matrix as a key-value pair. Then we use that Map
to set the node matrices for the attached viewers.
Add syncNodeTransforms()
as a member function of SyncHelper
:
syncNodeTransforms(nodeIds = []) {
let matMap = new Map();
for (let node of nodeIds) {
matMap.set(node, this._mainViewer.model.getNodeMatrix(node));
}
// Find a matching node in any attached viewer and update its matrix
for (let [node, matrix] of matMap.entries()) {
this._attachedViewers.map((viewer, index) => {
if (this._nodeMapping.has(node)) {
node = this._nodeMapping.get(node)[index];
}
viewer.model.setNodeMatrix(node, matrix);
});
}
}
In the event nodeIds
is empty, we should gather all the nodes of the mainViewer
starting at the root node. First, we will add a helper function to recursively gather all nodeIds
of the mainViewer
.
_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);
}
}
}
Now we can prepend syncNodeTransforms()
with the following:
syncNodeTransforms(nodeIds = []) {
// Gather all nodes of the mainViewer
if (nodeIds.length == 0) {
nodeIds = [];
this._gatherAllNodeIds(this._mainViewer.model.getAbsoluteRootNode(), nodeIds);
nodeIds = nodeIds.filter(Boolean);
}
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, index) => {
if (this._nodeMapping.has(node)) {
node = this._nodeMapping.get(node)[index];
}
viewer.model.setNodeMatrix(node, matrix);
});
}
}
Last, let’s write some accessor member functions for later use.
setNodesMapping(masterNode, mappedNodes) {
this._nodeMapping.set(masterNode, mappedNodes);
}
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. In our main
constructor, we will instantiate a SyncHelper
object after we have created both WebViewer objects. Add this._viewSync = new SyncHelper(this._viewerList);
to the constructor
in app.js:
// Set class properties
this._viewerList = [mainViewer, overheadViewer];
this._viewSync = new SyncHelper(this._viewerList);
this._modelList = [];
this._printSurfaces = [];
Recall the handleEvent
callback function in app.js from the previous section. This callback will be a great place to add our new syncNodeTransforms()
function. Update the handleEvent
callback with the following:
handleEvent: (eventType, nodeIds, initialMatrices, newMatrices) => {
this.setMatrixText(mainViewer.model.getNodeNetMatrix(nodeIds[0]));
this._viewSync.syncNodeTransforms(nodeIds);
}
You should now be able to select a part in the Main View, add handles with the “Show Handles” button, and watch the respective part in the “Overhead View” move.