The Auto-Arrange Function

Summary

In this chapter, we will write the auto-arrange function using the bounding box information for each node in the model tree.

Concepts

  • Working with node bounding boxes

  • Arranging nodes by size


Perhaps you would like to give the capability to arrange each leaf node of your model tree in an orderly manner on the printing plane. You could have the user interact with your handle operator to drag things around and position them, but a smarter way would be to use the model information to automate the placement of nodes. Review the main class and look for member functions arrangeOnPlane() and _gatherLeavesAndClearMats().

arrangeOnPlane(boundarySize) {
}
_gatherLeavesAndClearMats(node, leafArray, promiseArr) {
}

It is important to remember that the model tree nodes are affected by their parents above them, so to avoid any odd behavior from the parents, you should reset their transforms back to the identity matrix. Starting with the absolute root node of the model, we will traverse the tree recursively, storing the nodes without children (leaves) in an array, and resetting the parent objects back to their identity matrix. Note that we are not overwriting the nodes default matrix, since we are providing false for the set default argument in our Communicator.Model.setNodeMatrix function. With all the parent nodes at identity, the leaf nodes can be arranged without any unexpected net matrix calculations.

The recursive tree traversal will be abstracted into the _gatherLeavesAndClearMats() function. Add the following code to src/js/app.js:

_gatherLeavesAndClearMats(node, leafArray, promiseArr) {
        let mainViewer = this._viewerList[0];
        var children = mainViewer.model.getNodeChildren(node);
        if (children.length == 0)
                leafArray.push(node);

        for (var i = 0; i < children.length; i++) {
                var ident = new Communicator.Matrix;
                ident.loadIdentity();
                promiseArr.push(mainViewer.model.setNodeMatrix(children[i], ident, false));
                this._gatherLeavesAndClearMats(children[i], leafArray, promiseArr);
        }
}

Many of the HOOPS Communicator functions return promises, so it is good to wait for all these promises to resolve before making the next HOOPS Communicator call. To do this, you can store the returned promises in an array and use Promise.all() to return a single promise that resolves when all provided promises have resolved.

With that set, we can begin work on arrangeOnPlane().

arrangeOnPlane(boundarySize){
  let mainViewer = this._viewerList[0];

  return new Promise((resolve, reject) => {
        let leafArray = [];
        let setEmptyMatsPromises = [];
        let rootNode = mainViewer.model.getAbsoluteRootNode();
        let ident = new Communicator.Matrix;
        ident.loadIdentity();

        // Set the root node of the model tree to its identity matrix
        // and recursively do the same for all children
        mainViewer.model.setNodeMatrix(rootNode, ident, false);
        this._gatherLeavesAndClearMats(rootNode, leafArray, setEmptyMatsPromises);
  });
}

With all the nodes matrices reset we can begin to gather the node bounding box information, which will be used to arrange the nodes. We can query the bounding box information by using the Communicator.Model.getNodesBounding function. The Communicator.Model.getNodesBounding function takes an array of node IDs as input and returns a promise with a resulting value of type Communicator.Box. Again, it is good to wait for all these promises to resolve before continuing. Once the bounding boxes have been obtained, we can use that information to gather an idea of the size and spacing needed between nodes.

arrangeOnPlane(boundarySize){
  let mainViewer = this._viewerList[0];

  return new Promise((resolve, reject) => {
        let leafArray = [];
        let setEmptyMatsPromises = [];
        let rootNode = mainViewer.model.getAbsoluteRootNode();
        let ident = new Communicator.Matrix;
        ident.loadIdentity();

        // Set the root node of the model tree to its identity matrix
        // and recursively do the same for all children
        mainViewer.model.setNodeMatrix(rootNode, ident, false);
        this._gatherLeavesAndClearMats(rootNode, leafArray, setEmptyMatsPromises);

        // Once all nodes have been reset, we can get the node boundings
        Promise.all(setEmptyMatsPromises).then((values) => {
                let getBoundingPromises = new Array();
                for (var i = 0; i < leafArray.length; i++) {
                        getBoundingPromises.push(mainViewer.model.getNodesBounding([leafArray[i]]));
                }

        });
  });
}

We will start by looping through each nodes X and Y bounding box extent to determine the largest space in the X and Y direction. This will provide us numbers to define the amount of offset we need between elements in the arrangement. Next, we can define the area of the plane that we want the objects to be arranged to. You may set this as you wish, but I will be using 70% of the plane surface. The idea is that we will query each bounding box space, increment our starting X, Y position (we will start in the [-X,-Y] quadrant) by the spacing amount (plus some padding), and update the X and Y positions for the next part. Once we have the X and Y positions, we can set the node matrix. Again, since this is a returned promise, we will store all the returned promises in an array and return one promise using Promise.all() to finally resolve the function.

arrangeOnPlane(boundarySize){
  let mainViewer = this._viewerList[0];

  return new Promise((resolve, reject) => {
        let leafArray = [];
        let setEmptyMatsPromises = [];
        let rootNode = mainViewer.model.getAbsoluteRootNode();
        let ident = new Communicator.Matrix;
        ident.loadIdentity();

        // Set the root node of the model tree to its identity matrix
        // and recursively do the same for all children
        mainViewer.model.setNodeMatrix(rootNode, ident, false);
        this._gatherLeavesAndClearMats(rootNode, leafArray, setEmptyMatsPromises);

        // Once all nodes have been reset, we can get the node boundings
        Promise.all(setEmptyMatsPromises).then((values) => {
                let getBoundingPromises = new Array();
                for (var i = 0; i < leafArray.length; i++) {
                        getBoundingPromises.push(mainViewer.model.getNodesBounding([leafArray[i]]));
                }

                // Once all node bounding have been gathered, start arranging
                Promise.all(getBoundingPromises).then((values) => {
                        let partSpacingX = 0;
                        let partSpacingY = 0;
                        for (let bb of values) {
                                if (bb.extents().x > partSpacingX) {
                                        partSpacingX = bb.extents().x;
                                }
                                if (bb.extents().y > partSpacingY) {
                                        partSpacingY = bb.extents().y;
                                }
                        }

                        let setNewMatPromises = [];
                        let extent = boundarySize * 0.7;
                        let x = -extent;
                        let y = -extent;

                        for (let i = 0; i < values.length; i++) {
                                let m = new Communicator.Matrix;
                                m.loadIdentity();
                                let bb = values[i];
                                let c = bb.center();
                                m.m[12] = x + bb.extents().x;
                                m.m[13] = y - c.y;
                                m.m[14] = -bb.min.z;
                                setNewMatPromises.push(mainViewer.model.setNodeMatrix(leafArray[i], m, false));
                                x += (bb.extents().x + partSpacingX);
                                if (x > extent) {
                                        x = -extent;
                                        y += partSpacingY * 1.5;
                                }
                        }

                        Promise.all(setNewMatPromises).then(() => resolve());
                });
        });
  });
}

The last step is to hook up this new code with our UI skeleton. Return back to app.js and find the arrange-button callback. We will call the arrangeOnPlane function, which operates on our main viewer, then call our SyncHelper object to sync the nodes in the attached viewer.

document.getElementById("arrange-button").onclick = () => {
        // One plane for each viewer - need to call for each plane
        this.arrangeOnPlane(this._printSurfaces[0].getDimensions().planeSize)
                .then((results) => this._viewSync.syncNodeTransforms());
};

Looking back at your application and running the auto-arrange function, you should now see this:

../../_images/additive-manufacturing-auto-arrange.png