Utilities.js

/**
 * Wrapper for {@link https://forge.autodesk.com/en/docs/viewer/v6/reference/javascript/viewer3d|Viewer3D}
 * with a collection of helpful methods that are not (yet) part of the official API.
 * @namespace Autodesk.Viewing
 */
class Utilities {

    /**
     * Callback function used to report access token to the viewer.
     * @callback AccessTokenCallback
     * @param {string} token Access token.
     * @param {int} expires Number of seconds after which the token expires.
     */

    /**
     * Callback function used by the viewer to request new access token.
     * @callback AccessTokenRequest
     * @param {AccessTokenCallback} callback Access token callback.
     */

    /**
     * Initializes new instance of {@link Utilities}, including the initialization
     * of the underlying {@link https://forge.autodesk.com/en/docs/viewer/v6/reference/javascript/viewer3d|Viewer3D}.
     * @param {HTMLElement} container Target container for the viewer canvas.
     * @param {AccessTokenRequest} getAccessToken Function that will be called by the viewer
     * whenever a new access token is required.
     * @returns {Promise<Utilities>} Promise that will be either resolved with {@link Utilities} instance,
     * or rejected with an error message.
     * 
     * @example <caption>Using Promises</caption>
     * function getAccessToken(callback) {
     *   fetch('/api/forge/auth/token')
     *     .then(resp => resp.json())
     *     .then(json => callback(json.access_token, json.expires_in));
     * }
     * Autodesk.Viewing.Utilities.Initialize(document.getElementById('viewer'), getAccessToken)
     *   .then(utils => console.log(utils));
     * 
     * @example <caption>Using Async/Await</caption>
     * async function getAccessToken(callback) {
     *   const resp = await fetch('/api/forge/auth/token');
     *   const json = await resp.json();
     *   callback(json.access_token, json.expires_in);
     * }
     * async function init() {
     *   const utils = await Autodesk.Viewing.Utilities.Initialize(document.getElementById('viewer'), getAccessToken);
     *   console.log(utils);
     * }
     * init();
     */
    static Initialize(container, getAccessToken) {
        return new Promise(function(resolve, reject) {
            const options = {
                getAccessToken
            };
            Autodesk.Viewing.Initializer(options, function() {
                const viewer = new Autodesk.Viewing.Private.GuiViewer3D(container);
                viewer.start();
                resolve(new Autodesk.Viewing.Utilities(viewer));
            });
        });
    }

    /**
     * Initializes {@link Utilities} with existing instance
     * of {@link https://forge.autodesk.com/en/docs/viewer/v6/reference/javascript/viewer3d|Viewer3D}.
     * @param {Viewer3D} viewer Forge viewer.
     */
    constructor(viewer) {
        this.viewer = viewer;
        this.impl = viewer.impl;
    }

    /**
     * Viewable, also referred to as "bubble node", is a singular viewable
     * item from a document that has been generated by {@link https://forge.autodesk.com/en/docs/model-derivative/v2|Model Derivative API}.
     * For example, submitting a Revit file into the service will generate
     * a single document identified by its unique, base-64 encoded *urn*,
     * and the document will include a hierarchy of various *viewables* such
     * as thumbnails, 3D scenes for individual camera views, or 2D scenes
     * for individual Revit sheets.
     * @typedef {object} Viewable
     * @property {Viewable} parent Parent of the viewable in the document hierarchy.
     * @property {Viewable[]} children Children of the viewable in the document hierarchy.
     * @property {number} id Internal viewable ID.
     * @property {boolean} isLeaf Indicates that the viewable has no children.
     * @property {object} data Additional viewable properties such as *guid* (unique, string identifier),
     * *name*, *role*, *type*, etc.
     */

    /**
     * Loads {@link Viewable} into the viewer.
     * @param {string} documentUrn Base64-encoded identifier of the document.
     * @param {string|number} [viewableId=0] Optional GUID (string) or index (number) of the viewable within the document.
     * @returns {Promise<Viewable>} Promise that will be either resolved with {@link Viewable} structure,
     * or rejected with an error message.
     *
     * @example
     * async function loadDocument(urn) {
     *   const viewable = await utils.load(urn);
     *   console.log('Loaded viewable', viewable.data.id);
     * }
     */
    load(documentUrn, viewableId = 0) {
        const viewer = this.viewer;
        return new Promise(function(resolve, reject) {    
            function onDocumentLoadSuccess(doc) {
                if (typeof viewableId === 'string') {
                    const viewable = doc.getRoot().findByGuid(viewableId);
                    if (viewable) {
                        viewer.loadDocumentNode(doc, viewable);
                        resolve(viewable);
                    } else {
                        reject(`Viewable ${viewableId} not found.`);
                    }
                } else {
                    const viewables = doc.getRoot().search({ type: 'geometry' });
                    if (viewableId < viewables.length) {
                        const viewable = viewables[viewableId];
                        viewer.loadDocumentNode(doc, viewable);
                        resolve(viewable);
                    } else {
                        reject(`Viewable ${viewableId} not found.`);
                    }
                }
            }
            function onDocumentLoadError(errorCode, errorMsg) {
                reject(`Document loading error: ${errorMsg} (${errorCode})`);
            }
            Autodesk.Viewing.Document.load('urn:' + documentUrn, onDocumentLoadSuccess, onDocumentLoadError);
        });
    }

    /**
     * Object returned by ray casting methods for each scene object under the given canvas coordinates.
     * @typedef {object} Intersection
     * @property {number} dbId Internal ID of the scene object.
     * @property {number} distance Distance of the intersection point from camera. All intersections
     * returned by the ray casting method are sorted from the smallest distance to the largest.
     * @property {THREE.Face3} face {@link https://threejs.org/docs/#api/en/core/Face3|Face3} object
     * representing the triangular mesh face that has been intersected.
     * @property {number} faceIndex Index of the intersected face, if available.
     * @property {number} fragId ID of Forge Viewer *fragment* that was intersected.
     * @property {THREE.Vector3} intersectPoint {@link https://threejs.org/docs/#api/en/core/Vector3|Vector3} point of intersection.
     * @property {THREE.Vector3} point Same as *intersectPoint*.
     * @property {Model} model Forge Viewer {@link https://forge.autodesk.com/en/docs/viewer/v6/reference/javascript/model|Model} that was intersected.
     */

    /**
     * Finds all scene objects on specific X,Y position on the canvas.
     * @param {number} x X-coordinate, i.e., horizontal distance (in pixels) from the left border of the canvas.
     * @param {number} y Y-coordinate, i.e., vertical distance (in pixels) from the top border of the canvas.
     * @returns {Intersection[]} List of intersections.
     * 
     * @example
     * document.getElementById('viewer').addEventListener('click', function(ev) {
     *   const bounds = ev.target.getBoundingClientRect();
     *   const intersections = utils.rayCast(ev.clientX - bounds.left, ev.clientY - bounds.top);
     *   if (intersections.length > 0) {
     *     console.log('hit', intersections[0]);
     *   } else {
     *     console.log('miss');
     *   }
     * });
     */
    rayCast(x, y) {
        let intersections = [];
        this.impl.castRayViewport(this.impl.clientToViewport(x, y), false, null, null, intersections);
        return intersections;
    }

    /**
     * Inserts custom {@link https://threejs.org/docs/#api/en/objects/Mesh|Mesh} into
     * *overlay* scene of given name. An overlay scene is always rendered *after*
     * the main scene with the Forge Viewer model.
     * @param {THREE.Mesh} mesh Custom {@link https://threejs.org/docs/#api/en/objects/Mesh|Mesh}.
     * @param {string} [overlay='UtilitiesOverlay'] Name of the overlay scene.
     *
     * @example
     * const geometry = new THREE.SphereGeometry(10, 8, 8);
     * const material = new THREE.MeshBasicMaterial({ color: 0x336699 });
     * const mesh = new THREE.Mesh(geometry, material);
     * mesh.position.x = 1.0; mesh.position.y = 2.0; mesh.position.z = 3.0;
     * utils.addCustomMesh(mesh, 'myOverlay');
     */
    addCustomMesh(mesh, overlay = 'UtilitiesOverlay') {
        if (!this.impl.overlayScenes[overlay]) {
            this.impl.createOverlayScene(overlay);
        }
        this.impl.addOverlay(overlay, mesh);
    }

    /**
     * Removes custom {@link https://threejs.org/docs/#api/en/objects/Mesh|Mesh} from
     * *overlay* scene of given name. An overlay scene is always rendered *after*
     * the main scene with the Forge Viewer model.
     * @param {THREE.Mesh} mesh {@link https://threejs.org/docs/#api/en/objects/Mesh|Mesh} to be removed.
     * @param {string} [overlay='UtilitiesOverlay'] Name of the overlay scene.
     * 
     * @example
     * // after adding a mesh using addCustomMesh
     * utils.removeCustomMesh(mesh, 'myOverlay');
     */
    removeCustomMesh(mesh, overlay = 'UtilitiesOverlay') {
        if (!this.impl.overlayScenes[overlay]) {
            this.impl.createOverlayScene(overlay);
        }
        this.impl.removeOverlay(overlay, mesh);
    }

    /**
     * Callback function used when enumerating scene objects.
     * @callback NodeCallback
     * @param {number} id Object ID.
     */

    /**
     * Enumerates IDs of objects in the scene.
     *
     * To make sure the method call is synchronous (i.e., it returns *after*
     * all objects have been enumerated), always wait until the object tree
     * has been loaded.
     *
     * @param {NodeCallback} callback Function called for each object.
     * @param {number?} [parent = undefined] ID of the parent object whose children
     * should be enumerated. If undefined, the enumeration includes all scene objects.
     * @throws Exception if no {@link https://forge.autodesk.com/en/docs/viewer/v6/reference/javascript/model|Model} is loaded.
     *
     * @example
     * viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, function() {
     *   try {
     *     utils.enumerateNodes(function(id) {
     *       console.log('Found node', id);
     *     });
     *   } catch(err) {
     *     console.error('Could not enumerate nodes', err);
     *   }
     * });
     */
    enumerateNodes(callback, parent = undefined) {
        function onSuccess(tree) {
            if (typeof parent === 'undefined') {
                parent = tree.getRootId();
            }
            tree.enumNodeChildren(parent, callback, true);
        }
        function onError(err) { throw new Error(err); }
        this.viewer.getObjectTree(onSuccess, onError);
    }

    /**
     * Lists IDs of objects in the scene.
     * @param {number?} [parentId = undefined] ID of the parent object whose children
     * should be listed. If undefined, the list will include all scene object IDs.
     * @returns {Promise<number[]>} Promise that will be resolved with a list of IDs,
     * or rejected with an error message, for example, if there is no
     * {@link https://forge.autodesk.com/en/docs/viewer/v6/reference/javascript/model|Model}.
     *
     * @example <caption>Using async/await</caption>
     * viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, async function() {
     *   const ids = await utils.listNodes();
     *   console.log('Object IDs', ids);
     * });
     */
    listNodes(parentId = undefined) {
        const viewer = this.viewer;
        return new Promise(function(resolve, reject) {
            function onSuccess(tree) {
                if (typeof parentId === 'undefined') {
                    parentId = tree.getRootId();
                }
                let ids = [];
                tree.enumNodeChildren(parentId, function(id) { ids.push(id); }, true);
                resolve(ids);
            }
            function onError(err) { reject(err); }
            viewer.getObjectTree(onSuccess, onError);
        });
    }

    /**
     * Enumerates IDs of leaf objects in the scene.
     *
     * To make sure the method call is synchronous (i.e., it returns *after*
     * all objects have been enumerated), always wait until the object tree
     * has been loaded.
     *
     * @param {NodeCallback} callback Function called for each object.
     * @param {number?} [parent = undefined] ID of the parent object whose children
     * should be enumerated. If undefined, the enumeration includes all leaf objects.
     * @throws Exception if no {@link https://forge.autodesk.com/en/docs/viewer/v6/reference/javascript/model|Model} is loaded.
     *
     * @example
     * viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, function() {
     *   try {
     *     utils.enumerateLeafNodes(function(id) {
     *       console.log('Found leaf node', id);
     *     });
     *   } catch(err) {
     *     console.error('Could not enumerate nodes', err);
     *   }
     * });
     */
    enumerateLeafNodes(callback, parent = undefined) {
        let tree = null;
        function onNode(id) { if (tree.getChildCount(id) === 0) callback(id); }
        function onSuccess(_tree) {
            tree = _tree;
            if (typeof parent === 'undefined') {
                parent = tree.getRootId();
            }
            tree.enumNodeChildren(parent, onNode, true);
        }
        function onError(err) { throw new Error(err); }
        this.viewer.getObjectTree(onSuccess, onError);
    }

    /**
     * Lists IDs of leaf objects in the scene.
     * @param {number?} [parentId = undefined] ID of the parent object whose children
     * should be listed. If undefined, the list will include all leaf object IDs.
     * @returns {Promise<number[]>} Promise that will be resolved with a list of IDs,
     * or rejected with an error message, for example, if there is no
     * {@link https://forge.autodesk.com/en/docs/viewer/v6/reference/javascript/model|Model}.
     *
     * @example <caption>Using async/await</caption>
     * viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, async function() {
     *   const ids = await utils.listLeafNodes();
     *   console.log('Leaf object IDs', ids);
     * });
     */
    listLeafNodes(parentId = undefined) {
        const viewer = this.viewer;
        return new Promise(function(resolve, reject) {
            let tree = null;
            let ids = [];
            function onSuccess(_tree) {
                tree = _tree;
                if (typeof parentId === 'undefined') {
                    parentId = tree.getRootId();
                }
                tree.enumNodeChildren(parentId, function(id) { if (tree.getChildCount(id) === 0) ids.push(id); }, true);
                resolve(ids);
            }
            function onError(err) { reject(err); }
            viewer.getObjectTree(onSuccess, onError);
        });
    }

    /**
     * Callback function used when enumerating scene fragments.
     * @callback FragmentCallback
     * @param {number} id Fragment ID.
     */

    /**
     * Enumerates fragment IDs of specific object or entire scene.
     *
     * To make sure the method call is synchronous (i.e., it returns *after*
     * all fragments have been enumerated), always wait until the object tree
     * has been loaded.
     *
     * @param {FragmentCallback} callback Function called for each fragment.
     * @param {number?} [parent = undefined] ID of the parent object whose fragments
     * should be enumerated. If undefined, the enumeration includes all scene fragments.
     * @throws Exception if no {@link https://forge.autodesk.com/en/docs/viewer/v6/reference/javascript/model|Model} is loaded.
     *
     * @example
     * viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, function() {
     *   try {
     *     utils.enumerateFragments(function(id) {
     *       console.log('Found fragment', id);
     *     });
     *   } catch(err) {
     *     console.error('Could not enumerate fragments', err);
     *   }
     * });
     */
    enumerateFragments(callback, parent = undefined) {
        function onSuccess(tree) {
            if (typeof parent === 'undefined') {
                parent = tree.getRootId();
            }
            tree.enumNodeFragments(parent, callback, true);
        }
        function onError(err) { throw new Error(err); }
        this.viewer.getObjectTree(onSuccess, onError);
    }

    /**
     * Lists fragments IDs of specific scene object.
     * Should be called *after* the object tree has been loaded.
     * @param {number?} [parentId = undefined] ID of the parent object whose fragments
     * should be listed. If undefined, the list will include all fragment IDs.
     * @returns {Promise<number[]>} Promise that will be resolved with a list of IDs,
     * or rejected with an error message, for example, if there is no
     * {@link https://forge.autodesk.com/en/docs/viewer/v6/reference/javascript/model|Model}.
     *
     * @example <caption>Using async/await</caption>
     * viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, async function() {
     *   const ids = await utils.listFragments();
     *   console.log('Fragment IDs', ids);
     * });
     */
    listFragments(parentId = undefined) {
        const viewer = this.viewer;
        return new Promise(function(resolve, reject) {
            function onSuccess(tree) {
                if (typeof parentId === 'undefined') {
                    parentId = tree.getRootId();
                }
                let ids = [];
                tree.enumNodeFragments(parentId, function(id) { ids.push(id); }, true);
                resolve(ids);
            }
            function onError(err) { reject(err); }
            viewer.getObjectTree(onSuccess, onError);
        });
    }

    /**
     * Gets transformation matrix of scene fragment.
     * @param {number} fragId Fragment ID.
     * @returns {THREE.Matrix4} Transformation {@link https://threejs.org/docs/#api/en/math/Matrix4|Matrix4}.
     * @throws Exception when the fragments are not yet available.
     *
     * @example
     * viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, function() {
     *   try {
     *     const transform = utils.getFragmentTransform(1);
     *     console.log('Fragment transform', transform);
     *   } catch(err) {
     *     console.error('Could not retrieve fragment transform', err);
     *   }
     * });
     */
    getFragmentTransform(fragId) {
        if (!this.viewer.model) {
            throw new Error('Fragments not yet available. Wait for Autodesk.Viewing.FRAGMENTS_LOADED_EVENT event.');
        }
        const frags = this.viewer.model.getFragmentList();
        let transform = new THREE.Matrix4();
        frags.getWorldMatrix(fragId, transform);
        return transform;
    }

    /**
     * Gets world bounding box of scene fragment.
     * @param {number} fragId Fragment ID.
     * @returns {THREE.Box3} Transformation {@link https://threejs.org/docs/#api/en/math/Box3|Box3}.
     * @throws Exception when the fragments are not yet available.
     *
     * @example
     * viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, function() {
     *   try {
     *     const bounds = utils.getFragmentBounds(1);
     *     console.log('Fragment bounds', bounds);
     *   } catch(err) {
     *     console.error('Could not retrieve fragment bounds', err);
     *   }
     * });
     */
    getFragmentBounds(fragId) {
        if (!this.viewer.model) {
            throw new Error('Fragments not yet available. Wait for Autodesk.Viewing.FRAGMENTS_LOADED_EVENT event.');
        }
        const frags = this.viewer.model.getFragmentList();
        let bounds = new THREE.Box3();
        frags.getWorldBounds(fragId, bounds);
        return bounds;
    }
}

Autodesk = Autodesk || {};
Autodesk.Viewing = Autodesk.Viewing || {};
Autodesk.Viewing.Utilities = Utilities;