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

  • Instantiating and populating the MeshData object
  • Creating the mesh

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, review the file src/js/PrintingPlane.js. We provide a new class PrintingPlane that builds a printing plane of a given size.

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 with a function called _createPrintingPlane, which the constructor will invoke.

Your PrintingPlane class should now look like this:

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. Add the following function as a class method:

_createPrintingPlane() {
}

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. Add the following to our nearly created function _createPrintingPlane():

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.

The function 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):

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;
}
}

You'll notice a couple new helper functions as well, getDimensions() and getNodeId(). These will be used later in this tutorial, go ahead and add those now.

Find the placeholder comment "Create Printing Plane" in our main class. 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.

Let's create a new PrintingPlane during the modelStructureReady event:

viewer.start();
viewer.setCallbacks({
modelStructureReady: () => {
// Create Printing Plane
this._printSurfaces.push(new PrintingPlane(viewer, 300, 10));
// Load Model
this.loadModel("microengine", viewer);
}

Notice we are pushing the newly created PrintingPlane into an array. Let's make sure we add that array to our main's constructor:

// Set class properties
this._viewerList = [mainViewer, overheadViewer];
this._modelList = [];
this._printSurfaces = [];

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

top_level:1 tutorials:1