Picking under elements of an element in Autodesk Forge viewer

2.5k views Asked by At

I would like to accomplish a feature that I can do in Three.js but cannot in Autodesk Forge viewer. Here is the link to test: http://app.netonapp.com/JavaScript/Three.js/select_inner_objects.html

The requirement is to select objects inside an object. This job can be done with THREE.Raycaster in the above demo, to use a raycaster to detect all elements which are on the line the ray going through. Then I can get objects behind or inner another object.

I tried this concept in Autodesk Forge viewer but having no success. Here is the code:

// Change this to:
// true to use original Three.js
// false to use Autodesk Forge Viewer API
var useThreeJS = true;

var container = $('div.canvas-wrap')[0];

container.addEventListener('mousedown', function (event) {
    if (useThreeJS) {
        var canvas = _viewer.impl.canvas;
        var containerWidth = canvas.clientWidth;
        var containerHeight = canvas.clientHeight;

        var camera = _viewer.getCamera();

        var mouse = mouse || new THREE.Vector3();
        var raycaster = raycaster || new THREE.Raycaster();

        mouse.x = 2 * (event.clientX / containerWidth) - 1;
        mouse.y = 1 - 2 * (event.clientY / containerHeight);
        mouse.unproject(camera);

        raycaster.set(camera.position, mouse.sub(camera.position).normalize());
        var intersects = raycaster.intersectObjects(objects);

        if (intersects.length == 1) {
            var obj = intersects[0].object;
            obj.material.color.setRGB(1.0 - i / intersects.length, 0, 0);
        } else if (intersects.length > 1) {
            // Exclude the first which is the outer object (i == 0)
            for (var i = 1; i < intersects.length; i++) {
                var obj = intersects[i].object;
                obj.material.color.setRGB(1.0 - i / intersects.length, 0, 0);
            }
        }
    } else {
        var vp = _viewer.impl.clientToViewport(event.canvasX, event.canvasY);
        var renderer = _viewer.impl.renderer();

        var dbId = renderer.idAtPixel(vp.x, vp.y);
        if (dbId) {
            console.debug("Selected Id: " + dbId);
            _viewer.select(dbId);
            _viewer.impl.invalidate(true);
        }
    }
}, false);

I found the Forge viewer has viewer.impl.renderer().idAtPixel method which is great to get an element at the picking pixel. However, I want it to do more, to select all elements (which are under or nested) at the picking pixel. How I can do it with the Forge Viewer API?

3

There are 3 answers

0
Khoa Ho On BEST ANSWER

Based on the suggestion of Zhong Wu in another post, here is the final solution to select element which is under or inside another element. I created an Autodesk Forge viewer extension to use it easily.

///////////////////////////////////////////////////////////////////////////////
// InnerSelection viewer extension
// by Khoa Ho, December 2016
//
///////////////////////////////////////////////////////////////////////////////
AutodeskNamespace("Autodesk.ADN.Viewing.Extension");

Autodesk.ADN.Viewing.Extension.InnerSelection = function (viewer, options) {

    Autodesk.Viewing.Extension.call(this, viewer, options);

    var _self = this;

    var _container = viewer.canvas.parentElement;
    var _renderer = viewer.impl.renderer();
    var _instanceTree = viewer.model.getData().instanceTree;
    var _fragmentList = viewer.model.getFragmentList();
    var _eventSelectionChanged = false;
    var _viewport;
    var _outerDbId;

    _self.load = function () {

        _container.addEventListener('mousedown',
            onMouseDown);

        viewer.addEventListener(
            Autodesk.Viewing.SELECTION_CHANGED_EVENT,
            onItemSelected);

        console.log('Autodesk.ADN.Viewing.Extension.InnerSelection loaded');

        return true;
    };

    _self.unload = function () {

        _container.removeEventListener('mousedown',
            onMouseDown);

        viewer.removeEventListener(
            Autodesk.Viewing.SELECTION_CHANGED_EVENT,
            onItemSelected);

        console.log('Autodesk.ADN.Viewing.Extension.InnerSelection unloaded');

        return true;
    };

    function onMouseDown(e) {

        var viewport = viewer.impl.clientToViewport(e.canvasX, e.canvasY);
        _viewport = viewport; // Keep this viewport to use in onItemSelected()

        var dbId = _renderer.idAtPixel(viewport.x, viewport.y);

        if (_outerDbId == dbId) {
            _outerDbId = -1;

            // Deselect everything
            viewer.select();
        } else {
            _outerDbId = dbId;

            // Hide outer element temporarily to allow picking its behind element
            viewer.hideById(dbId);

            _eventSelectionChanged = true;
        }

        viewer.impl.sceneUpdated(true);
    }

    function onItemSelected(e) {

        if (_eventSelectionChanged) {

            // Prevent self looping on selection
            _eventSelectionChanged = false;

            // Show outer element back
            viewer.show(_outerDbId);

            // Get inner element Id after the outer element
            // was just hidden on mouse down event
            var innerDbId = _renderer.idAtPixel(_viewport.x, _viewport.y);

            if (innerDbId > -1) {
                // Select the inner element when it is found
                viewer.select(innerDbId);

                console.debug("Selected inner Id: " + innerDbId);

            } else if (_outerDbId > -1) {
                // Select the outer element if the inner element is not found
                viewer.select(_outerDbId);

                console.debug("Selected outer Id: " + _outerDbId);

            }
        }
    }

};

Autodesk.ADN.Viewing.Extension.InnerSelection.prototype =
    Object.create(Autodesk.Viewing.Extension.prototype);

Autodesk.ADN.Viewing.Extension.InnerSelection.prototype.constructor =
    Autodesk.ADN.Viewing.Extension.InnerSelection;

Autodesk.Viewing.theExtensionManager.registerExtension(
    'Autodesk.ADN.Viewing.Extension.InnerSelection',
    Autodesk.ADN.Viewing.Extension.InnerSelection);

1
Augusto Goncalves On

As of now (Dec/16), when you select using mouse click, the Viewer will not ignore transparent elements, so it will select an element even if it is transparent. Below is a code I used to track what's under the cursor, maybe can be useful.

// use jQuery to bind a mouve move event
$(_viewer.container).bind("mousemove", onMouseMove);

function onMouseMove(e) {
    var screenPoint = {
       x: event.clientX,
       y: event.clientY
    };
    var n = normalize(screenPoint);
    var dbId = /*_viewer.utilities.getHitPoint*/ getHitDbId(n.x, n.y);
    //
    // use the dbId somehow...
    //
}

// This is a built-in method getHitPoint, but the original returns
// the hit point, so this modified version returns the dbId
function getHitDbId(){
    y = 1.0 - y;
    x = x * 2.0 - 1.0;
    y = y * 2.0 - 1.0;
    var vpVec = new THREE.Vector3(x, y, 1);
    var result = _viewer.impl.hitTestViewport(vpVec, false);

    //return result ? result.intersectPoint : null; // original implementation
    return result ? result.dbId : null;
}

function normalize(screenPoint) {
    var viewport = _viewer.navigation.getScreenViewport();
    var n = {
       x: (screenPoint.x - viewport.left) / viewport.width,
       y: (screenPoint.y - viewport.top) / viewport.height
    };
    return n;
}
0
Khoa Ho On

I see method viewer.impl.renderer().idAtPixel works better than viewer.impl.hitTestViewport to select element on mouse pick. The first one can click through the hidden/ghost element to get the objectId of element behind. While the second cannot. Here is the code to test:

var container = $('div.canvas-wrap')[0];

container.addEventListener('mousedown', function (event) {

    var clickThroughHiddenElement = true;

    if (clickThroughHiddenElement) {
        var vp = _viewer.impl.clientToViewport(event.canvasX, event.canvasY);
        var renderer = _viewer.impl.renderer();

        var dbId = renderer.idAtPixel(vp.x, vp.y);
        if (!!dbId) {
            _viewer.select(dbId);
        }
        console.debug("Selected Id: " + dbId);
    } else {
        var screenPoint = {
            x: event.clientX,
            y: event.clientY
        };
        var viewport = _viewer.navigation.getScreenViewport();
        var x = (screenPoint.x - viewport.left) / viewport.width;
        var y = (screenPoint.y - viewport.top) / viewport.height;
        // Normalize point
        x = x * 2.0 - 1.0;
        y = (1.0 - y) * 2.0 - 1.0;

        var vpVec = new THREE.Vector3(x, y, 1);
        var result = _viewer.impl.hitTestViewport(vpVec, false);
        if (!!result) {
            var dbId = result.dbId;
            _viewer.select(dbId);
            console.debug("Selected Id: " + dbId);
        }
    }
}

However, they are not what I want, to click through transparent element to get elements behind. If user selects transparent element, it will be selected. If user selects inner elements, it will ignore outer transparent element to select the pick inner element.

I check Forge viewer uses THREE.Raycaster with element bounding box to detect intersection on mouse click. It seems my problem is doable with the Forge viewer like it does in my Three.js demo.