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
Communicator.MeshData
objectCreating 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 Communicator.Model.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 Communicator.Model.createMesh
function. The Communicator.Model.createMesh
function will then create a mesh based off the Communicator.MeshData
provided, returning a promise with a Communicator.meshId
to identify the mesh. This Communicator.meshId
can then be passed to the Communicator.MeshInstanceData
object, which is passed to Communicator.Model.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 Communicator.MeshData
into the Communicator.Model.createMesh
function, to allow for subsequent instancing.
The function Communicator.Model.createMesh
returns a promise with an assigned mesh ID of our created mesh. We will use this mesh ID to 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 Communicator.Model.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: