4. Creating the Printing Plane

Summary

In this chapter, we will walk through how to use HC to dynamically create the printing plane object in the scene.

Concepts


In the previous section, we skipped over the step that creates the printing plane that our models will sit atop in our scene. We will address that in this section.

To begin, create a new file in src/js called printingPlane.js. In this file, we will create a new class printingPlane that builds a printing plane of a given size. We want to make this class accessible to our main class, so we will export the class as default.

The constructor for this function should define three parameters, and the last two will be optional. The first parameter will be the WebViewer the printing plane will be added to. The second parameter will define the length and width of the square plane, and the last parameter will be the depth of the plane. Our printingPlane class will need an additional property to reference the nodeId returned when the MeshData is eventually created. We will set the initial value to null. Once all the properties for our class have been set, we will create the printing plane through and abstracted function called _createPrintingPlane, which the constructor will invoke.

Your printingPlane class should now look like this:

export default class printingPlane {
constructor(viewerInstance, size = 300, depth = 10) {
this._planeSize = size;
this._planeDepth = depth;
this._viewer = viewerInstance;
this._nodeId = null;
this._createPrintingPlane();
}
}

The _createPrintingPlane function will author the data for our printing plane, create the mesh, and add it to our scene.

There are four main parts to dynamically creating a mesh at run-time. If we look at the Communicator.Model class, we can see there is a createMesh function that receives an input parameter of type Communicator.MeshData. We must first populate the Communicator.MeshData object we instantiate, then pass this object to the createMesh function. The createMesh function will then create a mesh based off the MeshData provided, returning a promise with a MeshId to identify the mesh. This MeshId can then be passed to the meshInstanceData object, which is passed to createMeshInstance function, finally instancing the mesh and rendering it to the scene. Instancing a mesh returns a promise with a nodeId, that we can then store to track the nodeId of the printing plane in our printingPlane class member.

Let’s begin by defining our mesh data with the Communicator.MeshData object.

let gridSize = this._planeSize;
let d = this._planeDepth;
let meshData = new Communicator.MeshData();

We also assigned the plane size and depth properties to new variables for this scope, so we do not have to type this over and over again when defining our mesh vertices.

We will set the face winding for our mesh to look for the clockwise declaration of vertices. This simply defines the order in which the vertices are specified relative to the face normal. We will also enable backfaces which renders the face of each facet on both sides, making the the correct ordering of vertices somewhat less important (since if we specify a triangle of vertices backwards, the face will still be rendered).

meshData.setFaceWinding(Communicator.FaceWinding.Clockwise);
meshData.setBackfacesEnabled(true);

Next, we will specify the faces and normal for our rectangular mesh. Because each face is actually made of two triangles, and there are 6 vertices per face, we need to define a total of 36 vertices in space. Each point will also have its own normal vector defined, though for vertices on the same face, the normal should be the same.

meshData.addFaces([
// +Z Normal Plane
-gridSize, -gridSize, 0,
-gridSize, gridSize, 0,
gridSize, gridSize, 0,
-gridSize, -gridSize, 0,
gridSize, gridSize, 0,
gridSize, -gridSize, 0,
// -Z Normal Plane
-gridSize, -gridSize, -d,
-gridSize, gridSize, -d,
gridSize, gridSize, -d,
-gridSize, -gridSize, -d,
gridSize, gridSize, -d,
gridSize, -gridSize, -d,
// +X Normal Plane
gridSize, -gridSize, 0,
gridSize, -gridSize, -d,
gridSize, gridSize, -d,
gridSize, -gridSize, 0,
gridSize, gridSize, -d,
gridSize, gridSize, 0,
// -X Normal Plane
-gridSize, -gridSize, 0,
-gridSize, -gridSize, -d,
-gridSize, gridSize, -d,
-gridSize, -gridSize, 0,
-gridSize, gridSize, -d,
-gridSize, gridSize, 0,
// +Y Normal Plane
-gridSize, gridSize, 0,
gridSize, gridSize, 0,
-gridSize, gridSize, -d,
gridSize, gridSize, 0,
gridSize, gridSize, -d,
-gridSize, gridSize, -d,
// -Y Normal Plane
-gridSize, -gridSize, 0,
gridSize, -gridSize, 0,
-gridSize, -gridSize, -d,
gridSize, -gridSize, 0,
gridSize, -gridSize, -d,
-gridSize, -gridSize, -d,
], [
// +Z Normals
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
// -Z Normals
0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1,
// +X Normals
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
// -X Normals
-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0,
// +Y Normals
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
// -Y Normals
0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0,
]);

Technically, this would be enough information to render a mesh to our scene, but let’s add one more element to our mesh data: polylines. Adding polylines will give our printingPlane mesh a nice uniform grid look. We will specify the number of grid lines we want, and then equally divide the spacing by the size of the printing plane.

let gridCount = 15;
let gridUnit = (gridSize / gridCount) * 2;
for (let i = -gridCount / 2; i <= gridCount / 2; ++i) {
let position = (gridUnit * i);
meshData.addPolyline([
-gridSize, position, 0,
gridSize, position, 0,
]);
meshData.addPolyline([
position, -gridSize, 0,
position, gridSize, 0,
]);
}

This concludes the authoring of our mesh data. You can now pass this MeshData into the createMesh function, to allow for subsequent instancing.

this._viewer.model.createMesh(meshData)

createMesh returns a promise with an assigned MeshId of our created mesh. We will use this MeshId to then instance a mesh into our scene. Before instancing the mesh, we are able to set options for our mesh instance. Because the printingPlane is serving as a supplementary model, we do not want to give it the same interaction as other models loaded into our scene (for example, we don’t want to be able to select or move it around the scene, as opposed to our other loaded models on the plane).

Once the createMesh function has returned its promise and MeshId, we can use the then() function of the promise to execute our next section of code that will set the mesh instance flags and instantiate the mesh. The returned nodeId of this promise will then be assigned to the property value _nodeId of the printingPlane class.

this._viewer.model.createMesh(meshData).then((meshId) => {
let flags = Communicator.MeshInstanceCreationFlags.DoNotOutlineHighlight |
Communicator.MeshInstanceCreationFlags.ExcludeBounding |
Communicator.MeshInstanceCreationFlags.DoNotCut |
Communicator.MeshInstanceCreationFlags.DoNotExplode |
Communicator.MeshInstanceCreationFlags.DoNotLight;
let meshInstanceData = new Communicator.MeshInstanceData(meshId, null, "printingPlane", null, null, null, flags);
meshInstanceData.setLineColor(new Communicator.Color(150, 150, 150));
meshInstanceData.setFaceColor(new Communicator.Color(75, 75, 75));
// Do not provide a node id since this will be out of hierarchy
this._viewer.model.createMeshInstance(meshInstanceData, null, null, true)
.then((nodeId) => {
this._nodeId = nodeId;
});
});

This wraps up the _createPrintingPlane function. We can also add a couple accessor functions to get a couple properties of the printingPlane object. Our final printingPlane class should look something like this (note the addFaces data has been omitted for readability):

export default class printingPlane {
constructor(viewerInstance, size = 300, depth = 10) {
this._planeSize = size;
this._planeDepth = depth;
this._viewer = viewerInstance;
this._nodeId = null;
this._createPrintingPlane();
}
_createPrintingPlane() {
let gridSize = this._planeSize;
let d = this._planeDepth;
let meshData = new Communicator.MeshData();
meshData.setFaceWinding(Communicator.FaceWinding.Clockwise);
meshData.setBackfacesEnabled(true);
let gridCount = 15;
let gridUnit = (gridSize / gridCount) * 2;
for (let i = -gridCount / 2; i <= gridCount / 2; ++i) {
let position = (gridUnit * i);
meshData.addPolyline([
-gridSize, position, 0,
gridSize, position, 0,
]);
meshData.addPolyline([
position, -gridSize, 0,
position, gridSize, 0,
]);
}
meshData.addFaces([]);
this._viewer.model.createMesh(meshData).then((meshId) => {
let flags = Communicator.MeshInstanceCreationFlags.DoNotOutlineHighlight |
Communicator.MeshInstanceCreationFlags.ExcludeBounding |
Communicator.MeshInstanceCreationFlags.DoNotCut |
Communicator.MeshInstanceCreationFlags.DoNotExplode |
Communicator.MeshInstanceCreationFlags.DoNotLight;
let meshInstanceData = new Communicator.MeshInstanceData(meshId, null, "printingPlane", null, null, null, flags);
meshInstanceData.setLineColor(new Communicator.Color(150, 150, 150));
meshInstanceData.setFaceColor(new Communicator.Color(75, 75, 75));
// Do not provide a node id since this will be out of hierarchy
this._viewer.model.createMeshInstance(meshInstanceData, null, null, true)
.then((nodeId) => {
this._nodeId = nodeId;
});
});
}
getDimensions() {
return ({
planeSize: this._planeSize,
planeDepth: this._planeDepth,
});
}
getNodeId() {
return this._nodeId;
}
}

Now that we have our printingPlane class defined, let’s bring it into our main application. First, we need to import it. At the top of your app.js file, import the printingPlane class.

import printingPlane from "./printingPlane.js";

We had a placeholder comment in our main class constructor (under the map() and modelStructureReady blocks). We can now instantiate our printingPlane, passing in each viewer and the dimensions we want. To track each plane, we will store them in an array property of the main class, much like we did with the model names and viewers.

this._viewerList = [mainViewer, overheadViewer];
this._modelList = [];
this._printSurfaces = [];
this._viewerList.map((viewer) => {
viewer.start();
viewer.setCallbacks({
modelStructureReady: () => {
// Create Printing Plane
this._printSurfaces.push(new printingPlane(viewer, 300, 10));
// Load Model
this.loadModel("microengine", viewer);

Once this code is integrated and successfully run, your application should look like this:

Unfortunately for this model, it looks like the default transformation matrix on our model loads the model below our printingPlane object. This can happen depending on how the model was output and saved in the native CAD system. Let’s revisit our loadModel function to ensure the loaded objects are flush with the plane.

We can do this by querying the transformation matrix and bounding box of the loaded model. Since we cannot query a model that has not been loaded, we can immediately use the promise returned by loadSubtreeFromSCSFile to then immediately query model info and translate it appropriately once it has been loaded. We will obtain the models transformation matrix, but will be modifying the translation component to translate it to the origin. Querying the bounding box of the model returns a Communicator.Box object specifying the minimum and maximum extents of the corners of the bounding box. By taking the difference in the minimum values to the origin, we can determine how we need to translate the model to be flush with the plane at the origin.

We need to modify our loadModel function as follows:

// Function to load models and translate them so they are loaded
// at the origin and above the printing plane
loadModel(modelName, viewer) {
const modelNum = viewer.model.getNodeChildren(viewer.model.getAbsoluteRootNode()).length;
const nodeName = "Model-" + (modelNum + 1);
const modelNodeId = viewer.model.createNode(null, nodeName);
this._modelList.push(modelName);
viewer.model.loadSubtreeFromScsFile(modelNodeId, "/data/" + modelName + ".scs")
.then(() => {
let loadMatrix = viewer.model.getNodeNetMatrix(modelNodeId);
viewer.model.getNodeRealBounding(modelNodeId)
.then((box) => {
loadMatrix.setTranslationComponent(box.min.x * -1, box.min.y * -1, box.min.z * -1);
viewer.model.setNodeMatrix(modelNodeId, loadMatrix, true);
});
});
}

In your application, you should see our models loading at the origin and flush with the plane:

top_level:1 tutorials:1