Forge Viewer as Extension for another viewer - autodesk-forge

We have a two forge viewers in one div. We has initialize those viewers same time and showing different models.
Is it possible to initialize Autodesk Forge viewer as extension for another viewer and show in that viewers different models?

There is no official extension for showing one Forge Viewer inside another, but implementing this is possible. You could reuse one of the UI components of the viewer (for example, DockingPanel, also explained in this tutorial), and host the second viewer in it.
Here's how such an extension might look like:
class MyExtension extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this._group = null;
this._button = null;
this._panel = null;
}
load() {
return true;
}
unload() {
return true;
}
onToolbarCreated() {
this._group = new Autodesk.Viewing.UI.ControlGroup('allMyAwesomeExtensionsToolbar');
this._button = new Autodesk.Viewing.UI.Button('MyExtensionButton');
this._button.setToolTip('My Extension Button');
this._group.addControl(this._button);
this._panel = new MyPanel(this.viewer, this.viewer.container, 'MyPanel', 'My Panel Title');
this._button.onClick = (ev) => {
this._panel.setVisible(true);
};
this.viewer.toolbar.addControl(this._group);
}
}
class MyPanel extends Autodesk.Viewing.UI.DockingPanel {
constructor(viewer, container, id, title, options) {
super(container, id, title, options);
this.viewer = viewer;
this.secondViewer = null;
this.container.style.width = '640px';
this.container.style.height = '480px';
this.container.style.left = '1em';
this.container.style.top = '1em';
}
setVisible(show) {
super.setVisible(show);
if (show && !this.secondViewer) {
this.secondViewer = new Autodesk.Viewing.GuiViewer3D(this.container);
this.secondViewer.start();
Autodesk.Viewing.Document.load(
'urn:<your model urn>',
this.onDocumentLoadSuccess.bind(this),
this.onDocumentLoadFailure.bind(this)
);
}
}
onDocumentLoadSuccess(doc) {
const viewables = doc.getRoot().getDefaultGeometry();
this.secondViewer.loadDocumentNode(doc, viewables);
}
onDocumentLoadFailure(viewerErrorCode) {
console.error('onDocumentLoadFailure() - errorCode:' + viewerErrorCode);
}
}
Autodesk.Viewing.theExtensionManager.registerExtension('MyExtension', MyExtension);

Related

Viewer after ver. 7.37 -- use setAggregatedProperties

I am using a custom property panel.
There is any sample of how to replace older "setProperties" with new "setAggregatedProperties" in the viewer?
Thanks in advance
Yes, this sample is using it: https://forge-extensions.autodesk.io/?extension=CustomPropertiesExtension
Source code: https://github.com/Autodesk-Forge/forge-extensions/blob/master/public/extensions/CustomPropertiesExtension/contents/main.js
class CustomPropertyPanel extends Autodesk.Viewing.Extensions.ViewerPropertyPanel {
constructor (viewer, options) {
super(viewer, options);
this.properties = options.properties || {};
}
setAggregatedProperties(propertySet) {
Autodesk.Viewing.Extensions.ViewerPropertyPanel.prototype.setAggregatedProperties.call(this, propertySet);
// add your custom properties here
const dbids = propertySet.getDbIds();
dbids.forEach(id => {
var propsForObject = this.properties[id.toString()];
if (propsForObject) {
for (const groupName in propsForObject) {
const group = propsForObject[groupName];
for (const propName in group) {
const prop = group[propName];
this.addProperty(propName, prop, groupName);
}
}
}
});
}
};

Why does the ToolController's getPriority return 0 for my tool?

According to a prior SO answer, you can implement getPriority for a forge viewer Tool. And according to another SO answer extending the ToolInterface does not work. Hence, me not extending the ToolInterface implementing my Tool like so:
class MyCustomExtension extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this.theiaUtil = new TheiaUtil(this);
}
getPriority() {
console.log("Theia#getPriority called! ", (this.getPriority && this.getPriority() || 0));
return 100000;
}
...
}
My tool's priority is returned as 0 in the ToolController, although it shouldn't:
function getPriority(tool)
{
return tool.getPriority instanceof Function && tool.getPriority() || 0;
}
I don't know why this function returns 0 as tool.getPriority instanceof Function returns true if I call MyCustomExtension.getPriority myself.
Note that ToolInterface is implemented like so:
function ToolInterface()
{
this.names = [ "unnamed" ];
this.getNames = function() { return this.names; };
this.getName = function() { return this.names[0]; };
this.getPriority = function() { return 0; };
this.register = function() {};
this.deregister = function() {};
this.activate = function(name, viewerApi) {};
this.deactivate = function(name) {};
this.update = function(highResTimestamp) { return false; };
this.handleSingleClick = function( event, button ) { return false; };
this.handleDoubleClick = function( event, button ) { return false; };
this.handleSingleTap = function( event ) { return false; };
this.handleDoubleTap = function( event ) { return false; };
// ...
}
Because of that, simply extending the ToolInterface class won't work because all these properties and functions added to the instance in the constructor will take precedence over your actual class methods. This is also likely the reason why you're seeing the priority value returned as zero - when you call myTool.getPriority(), you are not actually calling your getPriority method, but rather the default function which was assigned to this.getPriority in ToolInterface's constructor.
To work around this issue I would recommend explicitly deleting the corresponding fields in your class' constructor (something I explain in my blog post on implementing custom Forge Viewer tools):
class DrawTool extends Autodesk.Viewing.ToolInterface {
constructor() {
super();
this.names = ['box-drawing-tool', 'sphere-drawing-tool'];
// Hack: delete functions defined *on the instance* of the tool.
// We want the tool controller to call our class methods instead.
delete this.register;
delete this.deregister;
delete this.activate;
delete this.deactivate;
delete this.getPriority;
delete this.handleMouseMove;
delete this.handleButtonDown;
delete this.handleButtonUp;
delete this.handleSingleClick;
}
register() {
console.log('DrawTool registered.');
}
deregister() {
console.log('DrawTool unregistered.');
}
activate(name, viewer) {
console.log('DrawTool activated.');
}
deactivate(name) {
console.log('DrawTool deactivated.');
}
getPriority() {
return 42; // Or feel free to use any number higher than 0 (which is the priority of all the default viewer tools)
}
// ...
}
TL;DR: Activate the tool in button click event from a toolbar button instead of the extension's load method.
class MyExtension extends Autodesk.Viewing.Extension {
...
onToolbarCreated(toolbar) {
const MyToolName = 'My.Tool.Name'
let button = new Autodesk.Viewing.UI.Button('my-tool-button');
button.onClick = (e) => {
const controller = this.viewer.toolController;
if (controller.isToolActivated(MyToolName)) {
controller.deactivateTool(MyToolName);
button.setState(Autodesk.Viewing.UI.Button.State.INACTIVE);
} else {
controller.activateTool(MyToolName);
button.setState(Autodesk.Viewing.UI.Button.State.ACTIVE);
}
};
}
...
}
I activated the tool instantly after registering it in the Extension's load method. Petr Broz's github repo from his blog post loads the tool from a button in the toolbar. So I moved the activation of the tool to a button click in the toolbar which worked for me.

How to fix DOMException error in google chrome?

I work with sounds in a browser game. I wrote sound manager. everything works fine, but not in Google chrome. I handled the error "uncaught (in promise) domexception", after the sounds were played in 50 percent of cases, in other cases it returns the error DOMException. What could be the problem?
export class AudioFile{
private audio: HTMLAudioElement;
private fileMP3: string;
private fileOGG: string;
private volume = 1;
private loop = false;
constructor(MP3:string, OGG:string) {
this.audio = new Audio();
this.fileMP3 = MP3;
this.fileOGG = OGG;
this.audio.canPlayType('audio/mpeg') ? this.audio.src = this.fileMP3 : this.audio.src = this.fileOGG;
this.audio.load();
this.audio.volume = this.volume;
this.audio.loop = this.loop;
}
public play() {
this.audio.currentTime = 0;
const playPromise = this.audio.play();
if (playPromise !== undefined) {
playPromise.then(_ => {
})
.catch(error => {
console.log(error);
});
}
}
public stop() {
this.audio.pause();
}
}
``````````````sound manager`````````````
export class SoundManager {
private sounds = new Map();
private static _soundManager: SoundManager;
constructor(){
if (SoundManager._soundManager) {
throw new Error("Instantiation failed: "+
"use Singleton.getInstance() instead of new.");
}
SoundManager._soundManager = this;
}
public static get instance(): SoundManager {
if (this._soundManager)
return this._soundManager;
else
return this._soundManager = new SoundManager();
}
preload() {
const pathMP3 = SoundConfig.PATHMP3;
const pathOGG = SoundConfig.PATHOGG;
for (const item in SoundConfig.SOUNDS) {
const name = SoundConfig.SOUNDS[item].NAME;
this.sounds.set(name, new AudioFile(pathMP3 + name + '.mp3', pathOGG + name + '.ogg'));
}
}
getSound(id: string): AudioFile {
return this.sounds.get(id);
}
}
Thank you spendr.
error: DOMException
code: 0
message: "play() failed because the user didn't interact with the document first.
Game runs through the iframe and I was needed to add a feature policy for autoplay.
<iframe src="..." allow="autoplay">
The article that helped me in solving the problem

Is the Property Database being Updated when Multiple Models are Loaded in The Same Viewer?

I managed to load multiple models into the same viewer and now I am trying to extract properties and values of the elements of each model; however, when I use getPropertyDb() and executeUserFunction(), I get back only the properties of the initial model.
I started with the code from this repo and used this article to understand how to load multiple models.
First model is loaded after a redirect from the server.
function onDocumentLoadSuccess(doc) {
const geometries = doc.getRoot().search({ type: 'geometry' });
if (geometries.length === 0) {
console.error('Document contains no viewables.');
return;
}
const initViewable = geometries[0];
const svfUrl = doc.getViewablePath(initViewable);
const mat = new THREE.Matrix4();
const modelOptions = {
placementTransform: mat,
globalOffset: { x: 0, y: 0, z: 0 },
sharedPropertyDbPath: doc.getPropertyDbPath()
};
const viewerDiv = document.getElementById('MyViewerDiv');
const config = {
extensions: myExtensions
};
viewer = new Autodesk.Viewing.Private.GuiViewer3D(viewerDiv, config);
viewer.start(svfUrl, modelOptions, onLoadModelSuccess, onLoadModelError);
}
After the geometry of each model is loaded an extension does some stuff.
function MyExtension(viewer, options) {
Autodesk.Viewing.Extension.call(this, viewer, options);
}
MyExtension.prototype = Object.create(Autodesk.Viewing.Extension.prototype);
MyExtension.prototype.constructor = MyExtension;
MyExtension.prototype.onGeometryLoadEvent = function(event) {
const myPromise = this.viewer.model
.getPropertyDb()
.executeUserFunction(userFunction);
myPromise
.then(function(retValue) {
if (!retValue) {
console.log('Model doesn\'t contain valid elemens.');
}
// do stuff...
})
.catch(err => console.log(err));
};
MyExtension.prototype.load = function() {
this.onGeometryLoadBinded = this.onGeometryLoadEvent.bind(this);
this.viewer.addEventListener(
Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
this.onGeometryLoadBinded
);
return true;
};
MyExtension.prototype.unload = function() {
this.viewer.removeEventListener(
Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
this.onGeometryLoadBinded
);
this.onGeometryLoadBinded = null;
return true;
};
Autodesk.Viewing.theExtensionManager.registerExtension(
'MyExtension',
MyExtension
);
function userFunction(pdb) {
// get properties of the elements
}
New models are loaded in the same viewer using an extension as well.
MyOtherExtension.prototype.onDocumentLoadSuccess = function(doc) {
// get the svfUrl of the initial geometry and set the loading options
this.viewer.loadModel(
svfUrl,
loaderOptions,
this.onLoadModelSuccessBinded,
this.onLoadModelErrorBinded
);
};
How do I update the Property Database in order to get the properties and values for all the models that are currently loaded into the viewer?
Try access a specific database through the model object:
viewer.impl.modelQueue().getModels()[index].getPropertyDb()

Setting data in viewModel knockoutjs from html5 websocket

I am trying to create knockout.js component that is getting data from HTML5 Websocket. Websocket code is in separate script e.g. util.js. I am able to connect and get data from socket, but dont know how correctly to set corresponding property in component`s ViewModel.
Websocket - util.js:
var options = {
server: '127.0.0.1',
port: '12345'
};
var socket, loadedFlag;
var timeout = 2000;
var clearTimer = -1;
var data = {};
function handleErrors(sError, sURL, iLine)
{
return true;
};
function getSocketState()
{
return (socket != null) ? socket.readyState : 0;
}
function onMessage(e)
{
data=$.parseJSON(e.data);
// ???? Is it possible to have here something like
// ???? viewModel.getDataWS1(data);
}
function onError()
{
clearInterval(clearTimer);
socket.onclose = function () {
loadedFlag = false;
};
clearTimer = setInterval("connectWebSocket()", timeout);
}
function onClose()
{
loadedFlag = false;
clearInterval(clearTimer);
clearTimer = setInterval("connectWebSocket()", timeout);
}
function onOpen()
{
clearInterval(clearTimer);
console.log("open" + getSocketState());
}
function connectWebSocket()
{
if ("WebSocket" in window)
{
if (getSocketState() === 1)
{
socket.onopen = onOpen;
clearInterval(clearTimer);
console.log(getSocketState());
}
else
{
try
{
host = "ws://" + options.server + ":" + options.port;
socket = new WebSocket(host);
socket.onopen = onOpen;
socket.onmessage = function (e) {
onMessage(e);
};
socket.onerror = onError;
socket.onclose = onClose;
}
catch (exeption)
{
console.log(exeption);
}
}
}
}
Component (productDisplay.js) - creating so that is can be used on multiple pages:
define([
'jquery',
'app/models/productDisplayModel',
'knockout',
'mapping',
'socket'
],
function ($, model, ko, mapping) {
ko.components.register('product', {
viewModel: {require: 'app/models/productModel'},
template: {require: 'text!app/views/product.html'}
});
});
Product ViewModel (productModel.js) - where I struggle to set viewModel property to data from websocket:
var viewModel = {};
define(['knockout', 'mapping', 'jquery'], function (ko, mapping, $) {
function Product(name, rating) {
this.name = name;
this.userRating = ko.observable(rating || null);
}
function MyViewModel() {
this.products = ko.observableArray(); // Start empty
}
MyViewModel.prototype.getDataWS1 = function () {
//Websocket has not connected and returned data yet, so data object is empty
// ???? Is there anyway I can add something like promise so that the value is set once socket is connected?
this.products(data);
};
// apply binding on page load
$(document).ready(function () {
connectToServer1();
viewModel = new MyViewModel();
ko.applyBindings(viewModel);
viewModel.getDataWS1();
});
});
Thank you for any ideas.
You can update an observable when you get a message in the following manner:
util.js
function onMessage(e) {
var productData = $.parseJSON(e.data);
viewModel.addNewProduct(productData);
}
productModel.js
function Product(name, rating) {
this.name = name;
this.userRating = ko.observable(rating || null);
}
function MyViewModel() {
this.products = ko.observableArray(); // Start empty
}
MyViewModel.prototype.addNewProduct(product) {
var newProduct = new Product(product.name, product.rating);
this.products.push(newProduct);
}
Basically the idea is that when you get a message (in onMessage function), you will parse the data and call a function in your viewmodel to add the message data to the viewmodel properties (observables, observableArrays, etc.)