Auto-arrange
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 easier way would be to use the model information to automate the placement of nodes. Review the Application
class and look for member functions arrangeOnPlane
and _gatherLeavesAndClearMats
. This is an example of the result of the auto-arrange operation we’ll be discussing:

The first thing we’ll do to make this happen is we’ll hook up to the button with the application logic:
document.getElementById("arrange-button")!.onclick = () =>
this.arrangeOnPlane(this.printingPlane.getDimensions().planeSize);
Next, we’ll start our calculations. It is important to remember that the model tree nodes are affected by their parents, 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.
The recursive tree traversal will be abstracted into the _gatherLeavesAndClearMats
function.
async _gatherLeavesAndClearMats(node: number): Promise<number[]> {
const result: number[] = [];
const children = this.hwv.model.getNodeChildren(node);
if (children.length == 0) {
result.push(node);
}
const ident = new Matrix();
const promiseArr: Promise<void>[] = [];
for (let i = 0; i < children.length; i++) {
promiseArr.push(this.hwv.model.setNodeMatrix(children[i], ident, false));
const leafs = await this._gatherLeavesAndClearMats(children[i]);
result.push(...leafs);
}
await Promise.all(promiseArr);
return result;
}
Note
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.
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.
async arrangeOnPlane(boundarySize: number) {
let mainViewer = this.hwv;
let rootNode = mainViewer.model.getAbsoluteRootNode();
let ident = new Matrix();
ident.loadIdentity();
// Set the root node of the model tree to its identity matrix
// and recursively do the same for all children
await mainViewer.model.setNodeMatrix(rootNode, ident, false);
const leafArray = await this._gatherLeavesAndClearMats(rootNode);
// Once all nodes have been reset, we can get the node boundings
const boundingBoxes = await Promise.all(
leafArray.map(
(current): Promise<Box> => mainViewer.model.getNodesBounding([current])
)
);
// ...
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 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.
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.
// Once all node bounding have been gathered, start arranging
let partSpacingX = 0;
let partSpacingY = 0;
for (let bb of boundingBoxes) {
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 < boundingBoxes.length; i++) {
let m = new Matrix();
m.loadIdentity();
let bb = boundingBoxes[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;
}
}
return Promise.all(setNewMatPromises);
}