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 perhaps just 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. Let's add a member function to our transformOperator class called arrangeOnPlane, that takes a single argument – the size of the plane boundary.

arrangeOnPlane(boundarySize) {
}

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 setNodeMatrix function. With all the parent nodes at identity, the leaf nodes can be arranged without any unexpected net matrix calculations. You can declare and initialize a few variables within your arrangeOnPlane function, but the recursive tree traversal should be abstracted into its own function, called _gatherLeavesAndClearMats.

Many of the HOOPS Communicator functions you will use 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.

arrangeOnPlane(){
return new Promise((resolve, reject) => {
let leafArray = []
let setEmptyMatsPromises = []
let rootNode = this._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
this._mainViewer.model.setNodeMatrix(rootNode, ident, false)
this._gatherLeavesAndClearMats(rootNode, leafArray, setEmptyMatsPromises)
_gatherLeavesAndClearMats(node, leafArray, promiseArr) {
var children = this._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()
setEmptyMatsPromises.push(this._mainViewer.model.setNodeMatrix(children[i], ident, false))
this._gatherLeavesAndClearMats(children[i], leafArray, promiseArr)
}
}
})
}

With all the nodes matrices reset, you can begin to gather the node bounding box information, which you will use to arrange the nodes. We can query the bounding box information by using the getNodesBounding function. The 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.

Now that the bounding boxes have been obtained, you can use that information to gather an idea of the size and spacing needed between nodes.

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) {
return new Promise((resolve) => {
let leafArray = []
let setEmptyMatsPromises = []
let rootNode = this._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
this._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(this._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(this._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._transformOp.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:

top_level:1 tutorials:1