Set instanceTree to a custom node in Forge 3D viewer - autodesk-forge

Lets say I'm working with a 3D file which is the combination of one Architectural model and one Structural model.
The instance tree or Model Browser looks like this
root/
Arch/
Level 01/
Level 02/
...
Str/
Level 01/
Level 02/
...
I want to display only the Level 01.
So I:
Followed the steps in the Viewer tutorial
Add an event listener to both Autodesk.Viewing.GEOMETRY_LOADED_EVENT & Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT
When the 2 are fired, I use the code in this article to display only the Level 01 without ghosting.
I have 2 problem with this approach
I have to wait until the entire model is loaded before I can filter the level
After filtering the level, if I click on Model Browser, I can still see the entire model structure (but with everything as hidden except Level 01). How can I set the instance tree to only have what's below?
root/
Arch/
Level 01/
Str/
Level 01/
EDIT
At what point am I supposed to override the shouldInclude() function?
I've tried this and put a breakpoint but it seems it never gets called... I also tried to move it around but in vain.
const start = Date.now();
Autodesk.Viewing.UI.ModelStructurePanel.shouldInclude = (node) => {
Logger.log(node);
return true;
};
Autodesk.Viewing.Initializer(options, () => {
Logger.log(`Viewer initialized in ${Date.now() - start}ms`);
const config = {};
// prettier-ignore
Autodesk.Viewing.theExtensionManager.registerExtension('MyAwesomeExtension', MyAwesomeExtension);
viewerApp = new Autodesk.Viewing.ViewingApplication('MyViewerDiv');
viewerApp.registerViewer(viewerApp.k3D, Autodesk.Viewing.Private.GuiViewer3D, config);
loadDocumentStart = Date.now();
// prettier-ignore
viewerApp.loadDocument(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
});

Regarding #1: the object tree is stored in the file's internal database which - for performance reasons - is only loaded after the actual geometry.
Regarding #2: you can subclass the ModelStructurePanel class and add your own behavior, for example, by overriding the ModelStructurePanel#shouldInclude method.

Since I wasn't able to understand how to use ModelStructurePanel, I overrode Autodesk.Viewing.ViewingApplication.selectItem to only modify the options which are either passed to loadDocumentNode or startWithDocumentNode as below:
const options = {
ids: leafIDs.length > 0 ? leafIDs : null, // changed this line
acmSessionId: this.myDocument.acmSessionId,
loadOptions,
useConsolidation: this.options.useConsolidation,
consolidationMemoryLimit: this.options.consolidationMemoryLimit || 100 * 1024 * 1024, // 100 MB
};
With leafIDs being an array of objectIDs to display. I was able to build it by:
querying the ModelDerivativeAPI using GET :urn/metadata/:guid
going through the tree to find the ids which I am interested in.
There's probably a more elegant way to do this but that's the best I could do so far.

Related

How can I filter only on elements in the Autodesk Forge Viewer API that are relevant for quantity take off?

For BIM model quantity take off I need to filter on all elements that are relevant for quantity take off. Below are some visual examples of the element I need (green circled) and which elements I don't need (red cross)
Example 1
Example 2
How can I filter only on elements in the Autodesk Forge API that are relevant for quantity take off?
When I use the API I get all levels and I didn't manage to get the level I need.
First of all, you'll need to define the logic to filter these elements.
From the images you've shared, the hierarchy tree of your model seems to be organized in a way that you're interested in the nodes in the 5th level from the root node.
So, if that's the case, you could use something like the snippet below to find the nodes' paths and then filter it to retrieve the 5th-level nodes' dbids:
var findfifthNodes = function (model) {
return new Promise(function (resolve, reject) {
model.getObjectTree(function (tree) {
let nodes = {};
tree.enumNodeChildren(tree.getRootId(), function (dbid) {
let nodepath = [dbid];
if(!!nodes[tree.getNodeParentId(dbid)]){
nodepath = nodepath.concat(nodes[tree.getNodeParentId(dbid)]);
}
nodes[dbid] = nodepath;
}, true /* recursively enumerate children's children as well */);
let fifthNodes = Object.values(nodes).filter(p => p.length == 5).map(a => a[0]);
resolve(fifthNodes);
}, reject);
});
}
You can also add your own custom tree view that might be more suitable to your workflow.
To achieve that, please refer to https://aps.autodesk.com/blog/custom-tree-views

Set transparent for specific element

I want set transparent for specific element, i follow this code:
var instanceTree = this.viewer.model.getInstanceTree();
var fragList = this.viewer.model.getFragmentList();
this.listElement.forEach(element => {
instanceTree.enumNodeFragments(element, (fragId) => {
console.log(element.material)
var material = fragList.getMaterial(fragId)
if (material) {
material.opacity = value;
material.transparent = true;
material.needsUpdate = true;
}
});
});
this.viewer.impl.invalidate(true, true, true);
but it overide for all elements have that material. How can i set for selected element?
Appreciate any comments.
UPDATE 1:
i found go around way is clone main material than register it with different name:
var newMaterial = material.clone();
const materials = this.viewer.impl.matman();
materials.addMaterial('mymaterial',newMaterial,true);
fragList.setMaterial(fragId,newMaterial);
newMaterial.opacity = value;
newMaterial.transparent = true;
newMaterial.needsUpdate = true;
but effect is not what i want, it has different color and when set transparent i can only see a couple object behind it
You can create your own, custom THREE.js material and assign it to the fragment using fragList.setMaterial(fragId, material).
For more information on using custom materials or shaders, see https://forge.autodesk.com/blog/custom-shader-materials-forge-viewer.
EDIT:
Regarding the visual anomalies (for example, when you only see some of the objects behind something semi-transparent), this is a known problem, unfortunately with no clear solution. When the Forge Model Derivative service creates an SVF file to be viewed in Forge Viewer, the individual scene elements are stored in a data structure optimized for fast traversal, depending on whether they are semi-transparent or fully opaque. This data structure is fixed, and so unfortunately, when you take an object that was originally fully opaque, and you make it semi-transparent, it will most likely be rendered in a wrong order...

Instance of ModelStructurePanel in div outside the viewer GUI

I am building an html page with model query functionality starting from the viewer.
I have implemented some methods including viewer.getProperties () and viewer.getBulkProperties (). Working on these themes I realized that it would be very useful to create instances of the panels (modelstructure, properties etc ...) in elements external to the viewer (not in the docking panels), maintaining the functionality and if possible customizing them (for example showing the elements not -collapsed).
First question: is it possible to do this? Second question: suggestions on a general method or tutorial for this theme?
For the Q1, it's possible to do that, appending the container of the ModelStructurePanel to a div outside the viewer container, but it's not recommended. It would take extra effort to fix CSS of the ModelStructurePanel after moving it out from viewer container.
For the Q2, you could take advantage of the jstree.js to make a three like UI, and for rebuilding model hierarchy data as the native ModelStructurePanel does, here is a code snippet for you. You would have to modify it to match jstree's data requirement.
function buildModelTree( model ) {
//builds model tree recursively
function _buildModelTreeRec( node ) {
it.enumNodeChildren( node.dbId, function(childId) {
node.children = node.children || [];
var childNode = {
dbId: childId,
name: it.getNodeName( childId )
};
node.children.push( childNode );
_buildModelTreeRec( childNode );
});
}
//get model instance tree and root component
var it = model.getData().instanceTree;
var rootId = it.getRootId();
var rootNode = {
dbId: rootId,
name: it.getNodeName( rootId )
};
_buildModelTreeRec( rootNode );
return rootNode;
}
var root = buildModelTree( viewer.model );
Then you will need to bind those events to change tree UI's look in some particular situations:
Autodesk.Viewing.SELECTION_CHANGED_EVENT: To make tree nodes be selected.
Autodesk.Viewing.ISOLATE_EVENT:
Autodesk.Viewing.HIDE_EVENT: To change tree node look, make tree nodes' text color gray if this node is invisible.
Autodesk.Viewing.SHOW_EVENT: To change selected three node's text color from gray to black
Autodesk.Viewing.ISOLATE_EVENT: a combination of the HIDE_EVENT and SHOW_EVENT.
And bind select_node.jstree event to isolate, fit to view the viewer objects according to the selected three nodes.
Isolate: Viewer3D#isolate( dbIds )
Fit to view: Viewer3D#fitToView( dbIds )

How can I remove or hide an object on the model tree panel in Forge Viewer?

I need to hide (make it go away completely) from the model tree panel in Viewer.
I already tried overriding methods from the Viewer (some other stuff is done that way), but the Tree-related methods and objects are not accessible for extending. It also seems too dangerous to mess with instanceTree data, like removing the dbId from the nodes list.
I'm running on the latest Viewer code (6.5.3), and writing pure javascript extensions.
For example, I tried overriding this function, which is used internally to determine if a node should or not be displayed. It doesn't work, neither does overriding the same function on the ModelStructureTreeDelegate:
Autodesk.Viewing.UI.TreeDelegate.prototype.shouldCreateTreeNode = function (dbId)
{
// original code on the viewer.js is:
// return true;
let itGo = true;
// _objectsHiddenInTree is populated with dbIds of objects to be hidden right after initializing the viewer
_objectsHiddenInTree.forEach(x => {
if (x == dbId){
itGo = false;
}
});
// return false; doesn't work either
return itGo;
};
Is there a way to do this from the Viewer side? I mean, to remove an item from the model tree?
If it's more viable, removing the object from the scene altogether is also a valid option. But I can't remove it from the model before sending to model derivative, it has to be done when opening the Viewer, or before opening the Tree Model panel.
Personally the easiest way would be to access node element via viewer.modelstructure and use styling to hide the node:
<style>
.yourHiddenNodeClass{display:none!important}
</style>
...
<script>
let modelStructureControl = viewer.modelstructure;
modelStructureControl.createUI(); //initialize the panel if it hasn't
let treeViewControl = modelStructureControl.tree;
let modelDelegate = treeViewControl.getDelegate(model.id);
treeViewControl.addClass(modelDelegate, dbid, "yourHiddenNodeClass", false) //hide a node - last boolean to toggle recursiveness
...
treeViewControl.removeClass(modelDeleagate, dbid, "yourHiddenNodeClass", false) //remove your custom class
</script>
And to hide a node completely:
model.visibilityManager.setNodeOff(dbid, true) // true=hide, false=show
Bryan's answer gave me an idea that seems to work for now:
Every element on the tree panel has an atribute 'lmv-nodeid', with the dbId of the object. So I looked for it, and added the 'hidden' attribute to the div:
document.querySelectorAll('[lmv-nodeid="' + objectDbId + '"]')[0].hidden = true;
His answer is still better, though, because there is no guarantee that the attribute will remain on newer versions of the Viewer, whereas the Viewer classes and methods are more stable and future-proof.

Change default ViewCube Orientation

I'm using the Forge Viewer to display simple geometry extractions from buildings.
However, when loading them the orientation of the model/view cube is not matching the expected use case (see image).
Basically I would need to swap the "Front View" with the "Top View".
Is this possible to achieve such a thing through e.g. default settings on the viewer object?
My set up is basically identical to the one in this 3rd-party react wrapper of the Forge Viewer: https://github.com/outer-labs/react-forge-viewer
Thank you already very much.
Daniel
EDIT: The model is in STP format
Basically, you can archive this with following steps via Viewer APIs after the model is loaded completely and can be separated into two parts.
(Preproccess) Get the Front view state of the Viewer that your want to make it as the Top:
a. Orient the current view to Front view: viewer.setViewCube( 'front' ).
b. Obtain current view statue of the viewport: var viewState = .getState( { viewport: true } ).
c. Save this viewState to somewhere, your js file or database.
Restore view state and set it as the Top view:
a. Obtain the viewState from somewhere (e.g. js file or database) that you got from the step1.
b. Restore view state via viewer.restoreState( viewState ).
c. Set the current view as Top view: viewer.autocam.setCurrentViewAsTop().
d. Set the current view as Home to avoid the state of the viewcube to be reset: viewer.autocam.setCurrentViewAsHome().
The code snippet for step2:
viewer.addEventListener(
Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
function( event ) {
console.log( '%cGEOMETRY_LOADED_EVENT: !!!Geometries loaded!!!', 'color: green;' );
setTimeout(() => {
const onOrientTopViewCompleted = function() {
viewer.removeEventListener(
Autodesk.Viewing.CAMERA_TRANSITION_COMPLETED,
onOrientTopViewCompleted
);
viewer.autocam.setCurrentViewAsTop();
viewer.autocam.setCurrentViewAsHome();
console.log( 'CAMERA_TRANSITION_COMPLETED' );
};
viewer.addEventListener(
Autodesk.Viewing.CAMERA_TRANSITION_COMPLETED,
onOrientTopViewCompleted
);
var viewState = '....'; //!<< the view state of the original `Front` view.
viewer.restoreState( viewState )
}, 1000);
});
Hope it helps!