Autodesk Forge Viewer Api Cannot load markups inside screenshot - autodesk-forge

Good day,
I am using the latest Autodesk forge viewer and I am trying to take a screenshot that also renders my markups. Right now my code takes a screenshot without any markups. Below is my viewer code. I am loading markups Core and markups Gui extensions. Notice the "takeSnapshot(viewer)" function inside onDocumentLoadSuccess(viewerDocument). The function is defined right before the initializer function.
function takeSnapshot(target){
$('#snipViewer').click( () => {
target.getScreenShot(1600, 920, (blobURL) => {
let snip = blobURL;
$('#sniplink').attr("href", snip);
$('#sniplink').html('Not Empty');
$('#sniplink').css({"background-image": `url(${blobURL})`});
});
});
}
//Autodesk Viewer Code
instance.data.showViewer = function showViewer(viewerAccessToken, viewerUrn){
localStorage.setItem("viewerAccessTokentoken", viewerAccessToken);
localStorage.setItem("viewerUrn", viewerUrn);
var viewer;
var options = {
env: 'AutodeskProduction',
api: 'derivativeV2',
getAccessToken: function(onTokenReady) {
var token = viewerAccessToken;
var timeInSeconds = 3600;
onTokenReady(token, timeInSeconds);
}
};
Autodesk.Viewing.Initializer(options, function() {
let htmlDiv = document.getElementById('forgeViewer');
viewer = new Autodesk.Viewing.GuiViewer3D(htmlDiv);
let startedCode = viewer.start();
viewer.setTheme("light-theme");
viewer.loadExtension("Autodesk.CustomDocumentBrowser").then(() => {
viewer.loadExtension("Autodesk.Viewing.MarkupsCore");
viewer.loadExtension("Autodesk.Viewing.MarkupsGui");
});
if (startedCode > 0) {
console.error('Failed to create a Viewer: WebGL not supported.');
$("#loadingStatus").html("Failed to create a Viewer: WebGL not supported.");
return;
}
console.log('Initialization complete, loading a model next...');
});
var documentId = `urn:` + viewerUrn;
var derivativeId = `urn:` + instance.derivativeUrn;
Autodesk.Viewing.Document.load(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
function onDocumentLoadSuccess(viewerDocument) {
var defaultModel = viewerDocument.getRoot().getDefaultGeometry();
viewer.loadDocumentNode(viewerDocument, defaultModel);
takeSnapshot(viewer);
}
function onDocumentLoadFailure() {
console.error('Failed fetching Forge manifest');
$("#loadingStatus").html("Failed fetching Forge manifest.");
}
}
I have already read this article: https://forge.autodesk.com/blog/screenshot-markups
I have tried doing this method but the instructions are very unclear for me. <div style="width:49vw; height:100vh;display:inline-block;"><canvas id="snapshot" style="position:absolute;"></canvas><button onclick="snaphot();" style="position:absolute;">Snapshot!</button></div>
What is the canvas element here for? Am I supposed to renderToCanvas() when I load the markups extension inside the initialize function or in my screenshot function? Is there some way I can implement the renderToCanvas() without changing too much of what I already am using here? I am not an expert with the viewer API so please if you could help me it would be very much appreciated, I am a beginner please don't skip many steps.
Thank you very much!

Here's a bit more simplified logic for generating screenshots with markups in Forge Viewer, with a bit more explanation on why it needs to be done this way below:
function getViewerScreenshot(viewer) {
return new Promise(function (resolve, reject) {
const screenshot = new Image();
screenshot.onload = () => resolve(screenshot);
screenshot.onerror = err => reject(err);
viewer.getScreenShot(viewer.container.clientWidth, viewer.container.clientHeight, function (blobURL) {
screenshot.src = blobURL;
});
});
}
function addMarkupsToScreenshot(viewer, screenshot) {
return new Promise(function (resolve, reject) {
const markupCoreExt = viewer.getExtension('Autodesk.Viewing.MarkupsCore');
const canvas = document.createElement('canvas');
canvas.width = viewer.container.clientWidth;
canvas.height = viewer.container.clientHeight;
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(screenshot, 0, 0, canvas.width, canvas.height);
markupCoreExt.renderToCanvas(context, function () {
resolve(canvas);
});
});
}
const screenshot = await getViewerScreenshot(viewer);
const canvas = await addMarkupsToScreenshot(viewer, screenshot);
const link = document.createElement('a');
link.href = canvas.toDataURL();
link.download = 'screenshot.png';
link.click();
Basically, the markups extension can only render its markups (and not the underlying 2D/3D scene) into an existing <canvas> element. That's why this is a multi-step process:
You render the underlying 2D/3D scene using viewer.getScreenShot, getting a blob URL that contains the screenshot image data
You create a new <canvas> element
You insert the screenshot into the canvas (in this case we create a new Image instance and render it into the canvas using context.drawImage)
You call the extension's renderToCanvas that will render the markups in the canvas on top of the screenshot image

Related

how to use the .impl methods in forge viewer V7*

I have been using following .impl methods in V6*
viewerApp.getCurrentViewer().impl.invalidate
viewerApp.getCurrentViewer().impl.matman()._materials
How to proceed with V7*
Thank you
EDIT :
var options = {
env: 'AutodeskProduction',
api: 'derivativeV2',
getAccessToken: function(onTokenReady) {
var token = accessToken;
var timeInSeconds = 3600;
onTokenReady(token, timeInSeconds);
}
};
var documentId = "urn:" + urn;
Autodesk.Viewing.Initializer(options, function() {
var htmlDiv = document.getElementById('forgeViewer');
viewer = new Autodesk.Viewing.GuiViewer3D(htmlDiv);
var startedCode = viewer.start();
if (startedCode > 0) {
console.error('Failed to create a Viewer: WebGL not supported.');
return;
}
console.log('Initialization complete, loading a model next...');
Autodesk.Viewing.Document.load(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
});
...
Then I'm using impl methods as below ..
var mats = viewer.impl.matman()._materials;
But I still get the error
Cannot read property 'impl' of null
not sure why but may be my global viewer variable is null , any idea why this is happening or what have i done wrong.
Anything under the impl is typically considered internal implementation, and as such should not be relied on in production code. With that said, the impl property is still available, and if needed, you can use it in Forge Viewer version 7.* like so:
const viewer = new Autodesk.Viewing.GuiViewer3D(...);
viewer.impl.invalidate();
viewer.impl.matman();

Implementing Three.js SSAOPass in AFrame

I was able to successfully integrate Threejs Effect composer in aframe as a component by exporting everything as THREE.Effectcomposer, THREE.SSAOPass etc. and adding the effect inside a aframe component and i tweaked the AFrame renderer to update the effects in the scene. OutlinePass from threejs worked fine in this code but SSAO is not working and i don't get any errors. Please someone help me figure out the problem. the code for SSAOPass looks like this
AFRAME.registerComponent('ssao', {
init: function () {
this.el.addEventListener('that', evt => this.onEnter());
this.el.addEventListener('mouseleave', evt => this.onLeave());
setTimeout(() => this.el.emit("that"), 2000);
},
onEnter: function () {
const scene = this.el.sceneEl.object3D;
const camera = this.el.sceneEl.camera;
const renderer = this.el.sceneEl.renderer;
const render = renderer.render;
const composer = new THREE.EffectComposer(renderer);
//let renderPass = new THREE.RenderPass(scene, camera);
//let outlinePass = new THREE.OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
const ssaoPass = new THREE.SSAOPass( scene, camera, window.innerWidth, window.innerHeight );
//composer.addPass(renderPass);
//composer.addPass(outlinePass);
ssaoPass.kernelRadius = 16;
composer.addPass( ssaoPass );
// let objects = [];
// this.el.object3D.traverse(node => {
// if (!node.isMesh) return;
// objects.push(node);
// });
// outlinePass.selectedObjects = objects;
// outlinePass.renderToScreen = true;
// outlinePass.edgeStrength = this.data.strength;
// outlinePass.edgeGlow = this.data.glow;
// outlinePass.visibleEdgeColor.set(this.data.color);
// HACK the AFRAME render method (a bit ugly)
const clock = new THREE.Clock();
this.originalRenderMethod = render;
let calledByComposer = false;
renderer.render = function () {
if (calledByComposer) {
render.apply(renderer, arguments);
} else {
calledByComposer = true;
composer.render(clock.getDelta());
calledByComposer = false;
}
};
},
onLeave: function () {
this.el.sceneEl.renderer.render = this.originalRenderMethod;
},
remove: function () {
this.onLeave();
}
});
I have also created a glitch project which i am sharing here. Please feel free to join and collaborate in my project
Edit link: https://glitch.com/edit/#!/accessible-torpid-partridge
Site link:https://accessible-torpid-partridge.glitch.me
Thanks in advance
The code is correct, all you need is to tweak the exposed SSAOShader uniforms: SSAOPass.kernelRadius, SSAOPass.minDistance, SSAOPass.maxDistance - like in the Three.js example.
Keep in mind - the scale in the example is huge, so the values will need to be different in a default aframe scene.
It's a good idea to be able to dynamically update a component (via setAttribute() if you properly handle updates), so you can see what's going on in realtime. Something like I did here - SSAO in a-frame (also based on Don McCurdys gist.
I've used some basic HTML elements, most threejs examples use dat.GUI - it is made for demo / debug tweaks.

Forge Viewer: Properties Window

The Properties window does not populate any properties even though the 2D view has properties info for the selected room
Here is the function that loads the model. what am I missing?
function loadModel() {
var initialViewable = viewables[indexViewable];
var svfUrl = lmvDoc.getViewablePath(initialViewable);
var modelOptions = {
sharedPropertyDbPath: lmvDoc.getFullPath(lmvDoc.getRoot().findPropertyDbPath())
};
viewer.loadModel(svfUrl, modelOptions, onLoadModelSuccess, onLoadModelError);
}
One line missing in your code, please try the following instead:
var sharedDbPath = initialViewable.findPropertyDbPath();
sharedDbPath = lmvDoc.getFullPath( sharedDbPath );
var modelOptions = {
sharedPropertyDbPath: sharedDbPath
};
However, you should not need to specify the sharedPropertyDbPath manually now. You can take advantage of the Viewer3D#loadDocumentNode to load the model directly. It will automatically determine the path for you. (started from v7 viewer)
const initialViewable = viewables[0];
viewer.loadDocumentNode( lmvDoc, initialViewable, loadOptions )
.then( onLoadModelSuccess )
.catch( onLoadModelError );

Button for markupCore extension not showing in dockingpanel

I have followed Philippe Leefsma's tutorial on how to implement the markup tool, but without any luck. Link here: http://adndevblog.typepad.com/cloud_and_mobile/2016/02/playing-with-the-new-view-data-markup-api.html
and here: https://developer.api.autodesk.com/viewingservice/v1/viewers/docs/tutorial-feature_markup.html
I get errors that I need to include requireJS, but I don't want to use it. So instead I used this script in my html file:
<script src="https://autodeskviewer.com/viewers/2.2/extensions/MarkupsCore.js">
I don't know if this is the right way to go? I get no errors in the console, but the markup button doesn't show up in the dockingpanel.
This is my code for loading the extension in the viewer:
viewerApp = null;
function initializeViewer(containerId, urn, params) {
function getToken(url) {
return new Promise(function (resolve, reject) {
$.get(url, function (response) {
resolve(response.access_token);
});
});
}
var initOptions = {
documentId: 'urn:' + urn,
env: 'AutodeskProduction',
getAccessToken: function (onGetAccessToken) {
getToken(params.gettokenurl).then(function (val) {
var accessToken = val;
var expireTimeSeconds = 60 * 30;
onGetAccessToken(accessToken, expireTimeSeconds);
});
}
}
function onDocumentLoaded(doc) {
var rootItem = doc.getRootItem();
// Grab all 3D items
var geometryItems3d =
Autodesk.Viewing.Document.getSubItemsWithProperties(
rootItem, { 'type': 'geometry', 'role': '3d' }, true);
// Grab all 2D items
var geometryItems2d =
Autodesk.Viewing.Document.getSubItemsWithProperties(
rootItem, { 'type': 'geometry', 'role': '2d' }, true);
// Pick the first 3D item otherwise first 2D item
var selectedItem = (geometryItems3d.length ?
geometryItems3d[0] :
geometryItems2d[0]);
var domContainer = document.getElementById('viewerContainer');
var config = { extensions: ["Autodesk.Viewing.MarkupsCore"] };
// GUI Version: viewer with controls
var viewer = new Autodesk.Viewing.Private.GuiViewer3D(domContainer, config);
viewer.loadExtension("Autodesk.Viewing.MarkupsCore");
viewer.initialize();
viewer.loadModel(doc.getViewablePath(selectedItem));
var extension = viewer.getExtension("Autodesk.Viewing.MarkupsCore");
viewerApp = viewer;
}
function onEnvInitialized() {
Autodesk.Viewing.Document.load(
initOptions.documentId,
function (doc) {
onDocumentLoaded(doc);
},
function (errCode) {
onLoadError(errCode);
})
}
function onLoadError(errCode) {
console.log('Error loading document: ' + errCode);
}
Autodesk.Viewing.Initializer(
initOptions,
function () {
onEnvInitialized()
})
}
Any help is highly appreciated!
Unfortunately there has been a few changes to the API since I wrote that blog post. The MarkupCore.js is now included in the viewer3D.js source, so you don't need to reference any extra file or use requireJS if you use the latest version of the viewer API.
Keep in mind that this is an API-only feature, so even after loading the markup extension, you won't get any UI out of the box. You have to implemented it yourself, for example create a dialog with buttons that may eventually create markups by calling the API.
Some of the code from my blog post may still be valid and give you an idea about what you need to do.
Hope that helps.

Adding static image to Lightswitch HTML 2013 Browse Screen

In my case, I have color coded some tiles in the HTML client and I want to add a simple color code key. I have the PNG file I want to use.
I do not require the ability to upload or change the image.
This link seems to achieve what I am looking for, but I am not sure where to implement. Does all of this code go into the PostRender of the Image Control I created?
Add image to lightswitch using local property and file location
Here is what the PostRender of the simple Image data item I created as an Image Local Property, and then dragged into the Solution Designer. It was basically copied from the link above, but I did change the name of the image file to match mine, and I have already added the item to the Content\Images folder structure and it shows in the file view:
myapp.BrowseOrderLines.ColorKey_postRender = function (element, contentItem) {
// Write code here.
function GetImageProperty(operation) {
var image = new Image();
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
// XMLHttpRequest used to allow external cross-domain resources to be processed using the canvas.
// unlike crossOrigin = "Anonymous", XMLHttpRequest works in IE10 (important for LightSwitch)
// still requires the appropriate server-side ACAO header (see https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image)
var xhr = new XMLHttpRequest();
xhr.onload = function () {
var url = URL.createObjectURL(this.response);
image.onload = function () {
URL.revokeObjectURL(url);
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
var dataURL = canvas.toDataURL("image/png");
operation.complete(dataURL.substring(dataURL.indexOf(",") + 1));
};
image.src = url;
};
xhr.open('GET', this.imageSource, true);
xhr.responseType = 'blob';
xhr.send();
};
myapp.BrowseOrderLines.ColorKey_postRender = function (element, contentItem) {
// Write code here.
msls.promiseOperation
(
$.proxy(
GetImageProperty,
{ imageSource: "content/images/Key.png" }
)
).then(
function (result) {
contentItem.screen.ImageProperty = result;
}
);
};
}
Currently, the Image control does show on the screen in the browser, and is the custom size I choose, but it is just a light blue area that does not display my image file.
I am not sure if I have embedded the image? I am not sure if that is a missing step?
Thank you!!
The easiest method of testing this approach would be to change your postRender to the following (which embeds the helper function within the postRender function):
myapp.BrowseOrderLines.ColorKey_postRender = function (element, contentItem) {
function GetImageProperty(imageSource) {
return msls.promiseOperation(
function (operation) {
var image = new Image();
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
// XMLHttpRequest used to allow external cross-domain resources to be processed using the canvas.
// unlike crossOrigin = "Anonymous", XMLHttpRequest works in IE10 (important for LightSwitch)
// still requires the appropriate server-side ACAO header (see https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image)
var xhr = new XMLHttpRequest();
xhr.onload = function () {
var url = URL.createObjectURL(this.response);
image.onload = function () {
URL.revokeObjectURL(url);
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
var dataURL = canvas.toDataURL("image/png");
operation.complete(dataURL.substring(dataURL.indexOf(",") + 1));
};
image.onerror = function () {
operation.error("Image load error");
};
image.src = url;
};
xhr.open('GET', imageSource, true);
xhr.responseType = 'blob';
xhr.send();
}
);
};
GetImageProperty("content/images/Key.png").then(function onComplete(result) {
contentItem.screen.ImageProperty = result;
}, function onError(error) {
msls.showMessageBox(error);
});
};
This assumes that you named the local property ImageProperty as per my original post and the Image control on your screen is named ColorKey.
In the above example, I've also taken the opportunity to slightly simplify and improve the code from my original post. It also includes a simple error handler which may flag up if there is a problem with loading the image.
If this still doesn't work, you could test the process by changing the image source file-name to content/images/user-splash-screen.png (this png file should have been added as a matter of course when you created your LightSwitch HTML project).
As the GetImageProperty function is a helper routine, rather than embedding it within the postRender you'd normally place it within a JavaScript helper module. This will allow it to be easily reused without repeating the function's code. If you don't already have such a library and you're interested in implementing one, the following post covers some of details involved in doing this:
Lightswitch HTML global JS file to pass variable