I have two separate extensions visualizing different "datapoints" from different domains (measurements like temperature, humidity, ... and image locations). These extensions should remain separated (and should not know each other).
There is no problem when using the extensions independent of each other. But problems occur, if both extensions are used at the same time (for the same model) in the Forge Viewer.
Both extensions try to get an existing instance of the DataVis-extension and load the extension if it is not available (call to viewer.loadExtension in each custom extension no difference).
The extensions create ViewableData and add viewables to it (sharing a single instance of ViewableData makes no difference).
After adding viewables await viewableData.finish() is called and the addViewables-method of the DataVis-extension is called.
One of the main problems is, that method changeOcclusion changes the occlusion only of the viewables which were added last. The other viewables remain visible at any time.
Probably because the pointMaterial in the DataVis-extension is overwritten any time the addViewables extension is called.
Is there a way to instantiate the extension multiple times, so that it is guaranteed that there are no side-effects when using it from within different custom extensions? Or maybe other mechanisms?
After consulting our engineering team, we submitted an issue report, LMV-6574, for tracking this issue. Please take note of the LMV prefixed id for tracking. We're welcome to ask for updates in the future by sending the issue id to forge (DOT) help (AT) autodesk (DOT) com.
However, we don't want to stop your development, so here is a workaround.
As I mentioned in the above comments area, SpriteViewable's constructor accepts an argument, ViewableStyles, that is used to set up the sprite icon. So, you don't need to call DataVisualization.addViewables(data) twice. Before finishing the ViewableData, you can add viewables with different ViewableStyles without a doubt.
Back to your use case, you want to reuse the DataVisualization extension for different data sources. To do so, I would advise you to store your device (sensor) data separately (e.g. device container). When you need to add/remove devices, just modify the device container, clear viewables, and then add new viewalbe data among your device container.
Here are some code snippets demonstrating this idea:
Example code for initializing:
let sensorStyleDefinitions = {
co2: {
url: "http://localhost:3000/assets-1/images/co2.svg",
color: 0xffffff,
},
temperature: {
url: "http://localhost:3000/assets-1/images/thermometer.svg",
color: 0xffffff,
},
default: {
url: "http://localhost:3000/assets-1/images/circle.svg",
color: 0xffffff,
},
};
// Create model-to-style map from style definitions.
let styleMap = {};
Object.entries(sensorStyleDefinitions).forEach(([type, styleDef]) => {
styleMap[type] = new Autodesk.DataVisualization.Core.ViewableStyle(
Autodesk.DataVisualization.Core.ViewableType.SPRITE,
new THREE.Color(styleDef.color),
styleDef.url
);
});
let devices = [
{
id: "Hall I",
position: {
x: -14.297511041164398,
y: -77.6432056427002,
z: 11.31889820098877,
},
type: "temperature",
sensorTypes: ["temperature"],
},
{
id: "Hall IV",
position: {
x: 60.53697395324707,
y: -74.6432056427002,
z: 11.31889820098877,
},
type: "co2",
sensorTypes: ["co2"],
},
];
const viewableData = new Autodesk.DataVisualization.Core.ViewableData();
viewableData.spriteSize = 16;
// Add viewables
devices.forEach((device, index) => {
const style = styleMap[device.type] || styleMap["default"];
const viewable = new Autodesk.DataVisualization.Core.SpriteViewable(
device.position,
style,
index + 1
);
viewableData.addViewable(viewable);
});
await viewableData.finish();
dataVizExt.addViewables(viewableData);
Example code for adding a device(sensor)
devices.push({
id: "Hall XII",
position: {
x: -15,
y: -70,
z: 50,
},
type: "temperature",
sensorTypes: ["temperature"],
});
// Remove existing sprites
dataVizExt.removeAllViewables();
const viewableData = new Autodesk.DataVisualization.Core.ViewableData();
viewableData.spriteSize = 16;
// re-add viewables
devices.forEach((device, index) => {
const style = styleMap[device.type] || styleMap["default"];
const viewable = new Autodesk.DataVisualization.Core.SpriteViewable(
device.position,
style,
index + 1
);
viewableData.addViewable(viewable);
});
await viewableData.finish();
dataVizExt.addViewables(viewableData);
Example code for removing a device(sensor)
devices = devices.splice(1, 1);
// Remove existing sprites
dataVizExt.removeAllViewables();
const viewableData = new Autodesk.DataVisualization.Core.ViewableData();
viewableData.spriteSize = 16;
// re-add viewables
devices.forEach((device, index) => {
const style = styleMap[device.type] || styleMap["default"];
const viewable = new Autodesk.DataVisualization.Core.SpriteViewable(
device.position,
style,
index + 1
);
viewableData.addViewable(viewable);
});
await viewableData.finish();
dataVizExt.addViewables(viewableData);
Related
I have a simple forge app to view 3d models. At first, I initiated the forge viewer with GuiViewer3D class but then wanted to implement AggregatedView instead.
My problem is that AggregatedView shows the model correctly but it shows it as being "stitched" together. Whereas, if I use GuiViewer3D or Viewer3D, the model looks smooth and clean.
I have looked into the globalOffset but in any solution, the globalOffset is the same, and hence should not be the cause here.
This is how the model should look like (GuiViewer3D)
But this is how it looks like instea using AggregatedView
I am not quite sure what the issue here. I am using an .fbx file as the source of 3d model.
This the code of AggregatedView()
var view = new Autodesk.Viewing.AggregatedView();
function launchViewer(urn) {
var options = {
env: 'AutodeskProduction',
getAccessToken: getForgeToken
};
Autodesk.Viewing.Initializer(options, () => {
var htmlDiv = document.getElementById('forgeViewer');
view.init(htmlDiv, options);
var documentId = 'urn:' + urn;
view.unloadAll();
Autodesk.Viewing.Document.load(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
});
}
function onDocumentLoadSuccess(doc) {
var nodes = doc.getRoot().search({ role:'3d', type: 'geometry' });
console.log(nodes);
view.setNodes(nodes[0]);
}
function onDocumentLoadFailure(viewErrorCode, viewErrorMsg) {
console.error('onDocumentLoadFailure() - errorCode:' + viewErrorCode + '\n- errorMessage:' + viewErrorMsg);
}
function getForgeToken(callback) {
fetch('/api/forge/oauth/token').then(res => {
res.json().then(data => {
callback(data.access_token, data.expires_in);
});
});
}
Many thanks in advance!
UPDATE:
After setting the global offset to (0,0,0), the geometry still looks "Stitched" together rather than smooth.
The pivot point is not the global offset. Please use viewer.getAllModels().map( model => model.getGlobalOffset() ) to check that instead. For AggreagateView, you can get the viewer instance via view.viewer;
In addition, AggreagateView loads models in the shared coordinate (applyRefPoint: true), so your model might be far away from the viewer's origin. Could you try this to see if it helps?
const options3d = {
getCustomLoadOptions: (bubble, data) => {
console.log(bubble, data);
return {
applyRefPoint: false //!<<< Disable Share Coordinate
// globalOffset: new THREE.Vector3( 543.0811920166016, 9.154923574611564, -1442.591747316564 ) //!<<< uncomment this to specify your globalOffset, but you need to include `applyRefPoint: false` above together.
// createWireframe: false
};
}
};
view.init(viewerDiv, options3d);
I am trying to ensure that when a 3d model is loaded into the viewer it should always orient the model in isometric view and then fit to view.
I have tried the viewer.fitToView(null, null, true) method as well as viewer.fitToView(model) but no success.
This is what I currently have:
var options = {
env : 'Local',
};
var viewer = new Autodesk.Viewing.GuiViewer3D(document.getElementById('ADViewer'));
Autodesk.Viewing.Initializer(options,function() {
if (showDocumentBrowser) {
//file is 2D so load document browser extension
viewer.loadExtension('Autodesk.DocumentBrowser');
// for sheet metal pdf drawings display page 2 first
if(sDisplayFlag == "sm") {
viewer.loadExtension('Autodesk.PDF').then(function() {
// URL parameter `page` will override value passed to loadModel
viewer.loadModel(sFileName, { page: 2 });
});
}
else {
viewer.loadModel(sFileName);
}
}else
{
//file is 3D model. Need to add code here to orient model in isometric view and then fit to view
viewer.loadModel(sFileName);
}
viewer.setTheme('light-theme');
viewer.start(options);
});
There is no difference between local or official mode while using fitToview.
The function declaration of the Viewer3D#fitToview is fitToView(objectIds, model, immediate), so the ways you're using are incorrect.
// Make the camera focus on one object
const selSet = viewer.getSelection();
viewer.fitToView(selSet[0]);
// Make the camera zoom out
viewer.fitToView();
What is the correct process for getting a transform between Forge coordinates and Revit's shared coordinates? I know there is globalOffset, but does it reference the Revit project internal coordinate system or shared coordinates?
Update Jun 11th, 2021
Now my MultipleModelUtil.js supports the alignments I shared below. Also, we can easily tell Forge Viewer to use By shared coordinates to aggregate models. Here is the code snippet, and you can check out here to know supported alignments
const util = new MultipleModelUtil( viewer );
util.options = {
alignment: MultipleModelAlignmentType.ShareCoordinates
};
const models = [
{ name: '1.rvt', urn: 'urn:dXJuOmFkc2sud2lwcHJvZDpmcy5maWxlOnZmLlNpaHgxOTVuUVJDMHIyWXZUSVRuZFE_dmVyc2lvbj0x' },
{ name: '2.rvt', urn: 'urn:dXJuOmFkc2sud2lwcHJvZDpmcy5maWxlOnZmLldVRHJ4ajZ6UTBPLTRrbWZrZ3ZoLUE_dmVyc2lvbj0x' },
{ name: '3.rvt', urn: 'urn:dXJuOmFkc2sud2lwcHJvZDpmcy5maWxlOnZmLjRyZW5HRTNUU25xNHhYaW5xdWtyaWc_dmVyc2lvbj0x' }
];
util.processModels( models );
==================
First, Forge Viewer supports 3 kinds of Revit link methods as the below, and you can take a look at the 3rd one (By shared coordinates).
Origin to origin: Apply the globalOffset of the 1st model to others. Check MultipleModelUtil/MultipleModelUtil.js for the demo
Center to center: the default way of the viewer.
By shared coordinates: set up applyRefpoint: true and make the globalOffset to the refPoint. This method is the one you are looking for.
The refPoint is the Revit survey point location inside Revit internal coordinate system. It's accessible with the AecModelData. Meanwhile, you can take advantage of the AggregatedView to use this aligning option. Here is an example of telling how to use AggregatedView:
https://gist.github.com/yiskang/c404af571ba4d631b5929c777503891e
If you want to use this logic with the Viewer class directly, here is a code snippet for you:
let globalOffset = null;
const aecModelData = bubbleNode.getAecModelData();
const tf = aecModelData && aecModelData.refPointTransformation; // Matrix4x3 as array[12]
const refPoint = tf ? { x: tf[9], y: tf[10], z: 0.0 } : { x: 0, y: 0, z: 0 };
// Check if the current globalOffset is sufficiently close to the refPoint to avoid inaccuracies.
const MaxDistSqr = 4.0e6;
const distSqr = globalOffset && THREE.Vector3.prototype.distanceToSquared.call(refPoint, globalOffset);
if (!globalOffset || distSqr > MaxDistSqr) {
globalOffset = new THREE.Vector3().copy(refPoint);
}
viewer.loadDocumentNode(doc, bubbleNode, { applyRefpoint: true, globalOffset: globalOffset, keepCurrentModels: true });
The bubbleNode can be either of the following:
bubbleNode = doc.getRoot().getDefaultGeometry()
//Or
const viewables = viewerDocument.getRoot().search({'type':'geometry'});
bubbleNode = viewables[0];
To get AecModelData, please refer to my gist: https://gist.github.com/yiskang/c404af571ba4d631b5929c777503891e#file-index-html-L67
// Call this line before using AecModelData
await doc.downloadAecModelData();
// doc.downloadAecModelData(() => resolve(doc));
See here for details of the AecModelData: https://forge.autodesk.com/blog/consume-aec-data-which-are-model-derivative-api
I've also found success feeding the refPointTransformation into a matrix4.
This way, the orientation of the model is also taken into account. (This is based off Eason's Answer).
const bubbleNode = doc.getRoot().getDefaultGeometry();
await doc.downloadAecModelData();
const aecModelData = bubbleNode.getAecModelData();
const tf = aecModelData && aecModelData.refPointTransformation;
const matrix4 = new THREE.Matrix4()
.makeBasis(
new THREE.Vector3(tf[0], tf[1], tf[2]),
new THREE.Vector3(tf[3], tf[4], tf[5]),
new THREE.Vector3(tf[6], tf[7], tf[8])
)
.setPosition(new THREE.Vector3(tf[9], tf[10], tf[11]))
viewer.loadDocumentNode(doc, viewables, {
placementTransform: matrix4,
keepCurrentModels: true,
globalOffset: {
"x": 0,
"y": 0,
"z": 0
},
applyRefpoint: true,
applyScaling: 'ft',
})
We use the viewer (version 7) to show the issues on 3d model colouring the affected objects using a user defined palette.
The remainig objects of the model are colored with a gray tone.
To achieve this I'm using the setThemingColor technique: I set the theming color grey for the rootid recursive and then I set the correct theme color to the specific issued object.
All the colors used are THREE.Vector4 with the opacity set to 1. In this way the themingColor is non blended with the "natural" color of the object but it "covers" object.
To improve the user experience we'd like to allow the user to dimm the objects not affected by issue instead of set the gray color using setThemingColor.
And now the question: is it possible to change the color (material?) of a group of objects by specifying a fade level up to the ghost of the viewer hide method while preserving the selection functionality?
I have tried the following approach with no success:
const mat = new THREE.MeshBasicMaterial({ color: 0x00ff00, opacity: 0.925, transparent: false });
(this.viewer as any).impl.getMaterials().addMaterial('ghost-material', mat, true);
const model = (this.viewer as any).model;
const tree = model.getInstanceTree();
const frags = model.getFragmentList();
[19240, 20373, 34364, 58813].map(dbid => {
tree.enumNodeFragments(dbid, (fragid) => {
console.log(fragid);
frags.setMaterial(fragid, mat);
(this.viewer as any).impl.invalidate(true);
}, true);
});
[19240, 20373, 34364, 58813] are valid dbids.
The opacity is almost 1 and the material is not transparent hoping to see something.
I tried to invalidate the viewer for each fragment.
After running this script, the 3d model remains the same.
The 'ghost-material' is regularly registered on matman but does not 'replace' the native one.
Where I am doing wrong?
The most important thing to do is to call the method:
model.unconsolidate(); // If the model is consolidated, material changes won't have any effect
before apply the new material to the fragments as explained in the following post
https://forge.autodesk.com/blog/custom-shader-materials-forge-viewer
The right answer is
const mat = new THREE.MeshBasicMaterial({ color: 0x00ff00, opacity: 0.925, transparent: false });
(this.viewer as any).impl.getMaterials().addMaterial('ghost-material', mat, true);
const model = (this.viewer as any).model;
const tree = model.getInstanceTree();
const frags = model.getFragmentList();
// without this it is all useless
model.unconsolidate();
[19240, 20373, 34364, 58813].map(dbid => {
tree.enumNodeFragments(dbid, (fragid) => {
console.log(fragid);
frags.setMaterial(fragid, mat);
(this.viewer as any).impl.invalidate(true);
}, true);
});
Yes, it's a bit of an advanced topic but you can customize materials assigned to individual objects (fragments). The process would be as follows:
Create a custom, semi-transparent material, e.g., a simple THREE.MeshBasicMaterial, and add it to the viewer's material manager
Use the instance tree to enumerate fragments of all objects in your scene
Use the fragment list to retrieve the original material of each fragment, store its reference somewhere, and set its material to your custom one
The code could look roughly like so:
const mat = new THREE.MeshBasicMaterial({ color: 0x00ff00, opacity: 0.25, transparent: true });
viewer.impl.getMaterials().addMaterial('my-material', mat, true);
const tree = viewer.model.getInstanceTree();
const frags = viewer.model.getFragmentList();
tree.enumNodeFragments(tree.getRootId(), (fragid) => { frags.setMaterial(fragid, mat) }, true);
When needed, repeat the process, and reset each fragment to its original material.
I'm trying to switch from paperspace view to model space view in a dwg loaded in the Forge platform on v7. I think it's supposed to use the BubbleNode, but I can't find any code samples showing. Any ideas how to get the BubbleNode from a loaded document?
I've reviewed: https://forge.autodesk.com/en/docs/viewer/v7/change_history/changelog_v7/migration_guide_v6_to_v7/
and
https://forge.autodesk.com/en/docs/viewer/v7/developers_guide/viewer_basics/load-a-model/
Trying to piece together some sample code that will do the same thing as step 3 here from v6:https://forge.autodesk.com/en/docs/viewer/v6/tutorials/basic-viewer/
You can get geometry BubbleNode Array by search method of root BubbleNode with specifying { 'type': 'geometry' } as parameter.
Below is code example for switching viewable.
var viewer;
var viewables;
var indexGeom;
var viewdoc;
//Call back for viewer DocumentLoadSuccess
function onDocumentLoadSuccess(doc) {
viewdoc = doc;
indexGeom = 0;
viewables = doc.getRoot().search({ 'type': 'geometry' });
viewer.loadDocumentNode(doc, viewables[indexGeom]).then(i => {
activateUI();
});
}
//Call back for switch to next view button
function loadNextModel() {
// Next geometry index. Loop back to 0 when overflown.
indexGeom = (indexGeom + 1) % viewables.length;
viewer.tearDown();
viewer.loadDocumentNode(viewdoc, viewables[indexGeom]).then(i => {
activateUI();
});
}
pls. see developer guide '3.Load a Model' chapter.
https://forge.autodesk.com/en/docs/viewer/v7/developers_guide/viewer_basics/load-a-model/