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.