25 Nov 2019

Working with 2D and 3D Scenes and Geometry in Forge Viewer

When building advanced applications around Forge Viewer, it is often necessary to traverse the hierarchy of model elements, access or manipulate individual geometries, or compute their bounds. Let's take a look at how these tasks can be implemented for both 3D and 2D models.

Note: some of the code samples below use not-exactly-public APIs (typically the classes and functions under Autodesk.Viewing.Private namespace, and methods and properties under viewer.impl) which are subject to change.

Before we look at the individual tasks, let's clarify two important terms: fragments and objects. When Model Derivative service translates your 2D or 3D designs for Forge Viewer, the output includes, among other things:

  • fragments representing individual meshes with properties like transform matrices, bounding boxes, or materials
  • objects representing logical, selectable entities that can have properties attached to them

Generally, the relationship between objects and fragments is many-to-many: one object may consist of multiple fragments (usually for 3D objects with large number of triangles or multiple materials), and many objects may be consolidated in a single fragment (usually in 2D models). When working with Viewer APIs, you typically reference fragments using fragment IDs (or fragIds), and objects using database IDs (or dbIds).

3D

Hierarchy Traversal

Model hierarchy of 3D models is encapsulated in Autodesk.Viewing.Private.InstanceTree class. Instances of this class - retrieved from individual models in the viewer using model.getInstanceTree() - can be queried for information like parent-children relationships between objects, or object-fragment mappings.

Note: the instance tree is loaded asynchronously, independently from actual geometry data, so wait until it's available by waiting for the OBJECT_TREE_CREATED_EVENT event.

Here's an example of recursively traversing the entire hierarchy starting from the root object, and logging the ID and child count of each object along the way:

viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, function () {
  const tree = viewer.model.getInstanceTree();
  const rootId = tree.getRootId();
  tree.enumNodeChildren(
    rootId,
    function (dbId) {
      console.log('dbId:', dbId, 'childCount:', tree.getChildCount(dbId));
    },
    true
  );
});

See the Pen Forge Viewer: Enumerate Objects by Petr Broz (@petrbroz) on CodePen.

 

Another example, this time listing the IDs of all fragments directly linked to objects that are currently selected:

viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, function () {
  const tree = viewer.model.getInstanceTree();
  if (tree) { // Could be null if the tree hasn't been loaded yet
    const selectedIds = viewer.getSelection();
    for (const dbId of selectedIds) {
      const fragIds = [];
      tree.enumNodeFragments(
        dbId,
        function (fragId) { fragIds.push(fragId); },
        false
      );
      console.log('dbId:', dbId, 'fragIds:', fragIds);
    }
  }
});

See the Pen Forge Viewer v7: Enumerate Fragments by Petr Broz (@petrbroz) on CodePen.

 

Geometry Properties

Fragments in both 3D and 2D models are encapsulated in Autodesk.Viewing.Private.FragmentList class. Similarly to the instance tree, fragment lists are retrieved from individual models loaded in the viewer using model.getFragmentList(), and can be used to read or modify various fragment properties such as visibility flags, transformations, linked materials, etc.

Note: the FragmentList class unfortunately isn't documented on https://forge.autodesk.com (yet), but you can explore the various methods it provides in the non-minified source code of Forge Viewer, for example, in https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.js.

Here's an example of reading various fragment properties:

const frags = viewer.model.getFragmentList();
function listFragmentProperties(fragId) {
  console.log('Fragment ID:', fragId);
  // Get IDs of all objects linked to this fragment
  const objectIds = frags.getDbIds(fragId);
  console.log('Linked object IDs:', objectIds);
  // Get the fragment's world matrix
  let matrix = new THREE.Matrix4();
  frags.getWorldMatrix(fragId, matrix);
  console.log('World matrix:', JSON.stringify(matrix));
  // Get the fragment's world bounds
  let bbox = new THREE.Box3();
  frags.getWorldBounds(fragId, bbox);
  console.log('World bounds:', JSON.stringify(bbox));
}

See the Pen Forge Viewer: Get Fragment Properties by Petr Broz (@petrbroz) on CodePen.

 

Geometry Manipulation

Here's an example showing how one can modify fragment's position, scale, or rotation:

const frags = viewer.model.getFragmentList();
function modifyFragmentTransform(fragId) {
  let scale = new THREE.Vector3();
  let rotation = new THREE.Quaternion();
  let translation = new THREE.Vector3();
  frags.getAnimTransform(fragId, scale, rotation, translation);
  translation.z += 10.0;
  scale.x = scale.y = scale.z = 1.0;
  frags.updateAnimTransform(fragId, scale, rotation, translation);
}

See the Pen Forge Viewer: Modify Fragment Transform by Petr Broz (@petrbroz) on CodePen.

 

As a side note, most of the fragment functionality can also be accessed through viewer.impl. For example:

  • calling viewer.impl.getFragmentProxy(model, fragId) will return a new instance of Autodesk.Viewing.Private.FragmentPointer class which then provides similar methods to the fragment list: fragPointer.getWorldMatrix(matrix), fragPointer.getWorldBounds(bbox), etc.
  • calling viewer.impl.getRenderProxy(model, fragId) will, similarly to fragList.getVizmesh(fragId), return a THREE.Mesh instance representing the specific fragment; be careful with using the mesh object, however, as in some cases the viewer may reuse the existing mesh instance and populate it with different values during another call to fragList.getVizmesh(fragId)

2D

As mentioned earlier, in 2D models, multiple objects are typically consolidated into a single fragment. This makes operations like geometry traversal or bounds retrieval a bit more involved, however the viewer provides several utility classes that we can make use of.

Geometry Traversal

In order to traverse individual primitives (such as line segments, circular arcs, or elliptical arcs) of all objects in a fragment, we can use the Autodesk.Viewing.Private.VertexBufferReader class and its enumGeoms(filter, callbackObject) method like so:

const frags = viewer.model.getFragmentList();
function listFragmentPrimitives(fragId) {
  const mesh = frags.getVizmesh(fragId);
  const vbr = new Autodesk.Viewing.Private.VertexBufferReader(mesh.geometry, viewer.impl.use2dInstancing);
  vbr.enumGeoms(null, {
    onLineSegment: function (x1, y1, x2, y2, vpId) {
      console.log('found line', x1, y1, x2, y2);
    },
    onCircularArc: function (cx, cy, start, end, radius, vpId) {
      console.log('found circular arc', cx, cy, start, end, radius);
    },
    onEllipticalArc: function (cx, cy, start, end, major, minor, tilt, vpId) {
      console.log('found elliptical arc', cx, cy, start, end, major, minor, tilt);
    },
    onOneTriangle: function (x1, y1, x2, y2, x3, y3, vpId) {
      console.log('found triangle', x1, y1, x2, y2, x3, y3);
    },
    onTexQuad: function (cx, cy, width, height, rotation, vpId) {
      console.log('found quad', cx, cy, width, height, rotation);
    }
  });
}

See the Pen Forge Viewer: Enumerate 2D Primitives by Petr Broz (@petrbroz) on CodePen.

 

If you're only interested in a specific object, layer, or viewport, you can provide a filter function as the first argument to the enumGeoms method. The filter function should accept object ID, layer ID, and viewport ID as its three arguments, and return true for primitives that you wish to include in the callback. Alternatively, you can also use additional methods of the VertexBufferReader class such as enumGeomsForObject(dbId, callbackObject), or enumGeomsForVisibleLayer(layerId, callbackObject).

Object Bounds

Computing the bounds of an object from its 2D primitives would be a tedious task. Luckily, even here we can leverage an existing, albeit _private_, utility class Autodesk.Viewing.Private.BoundsCallback. An instance of this class can be passed to VertexBufferReader's enumeration methods mentioned earlier. After the enumeration, the THREE.Box3 object that you pass to BoundsCallback's constructor will be populated with the bounding box of all encountered 2D primitives:

const frags = viewer.model.getFragmentList();
function getBounds2D(dbId) {
  let bounds = new THREE.Box3();
  let boundsCallback = new Autodesk.Viewing.Private.BoundsCallback(bounds);
  const fragIds = frags.fragments.dbId2fragId[dbId]; // Find all fragments including this object's primitives
  if (!Array.isArray(fragIds)) {
    fragIds = [fragIds];
  }
  for (const fragId of fragIds) {
    // Get the actual mesh with all geometry data for given fragment
    const mesh = frags.getVizmesh(fragId);
    const vbr = new Autodesk.Viewing.Private.VertexBufferReader(mesh.geometry, viewer.impl.use2dInstancing);
    vbr.enumGeomsForObject(dbId, boundsCallback); // Update bounds based on all primitives linked to our dbId
  }
  return bounds;
}

See the Pen Forge Viewer: Get 2D Bounds by Petr Broz (@petrbroz) on CodePen.

 

Related Article