19 Feb 2016
Playing with the new Viewer Markup API

The latest release of the viewer API is coming with a 2D markup feature that allows you to draw svg shapes on top of your 2d/3d models, potentially save the markup information you created and restore them at a later time.
There is a tutorial here that illustrates the basics of how to work with those markups. However I found it lacks a bit of information and some parts are not completely up-to-date yet, hence this post trying to bring a complement.
First of all the markups is an API-only feature, meaning there is no UI provided out of the box by the viewer to create and manipulate the markups, so you will have to create your own (alternatively get somebody to do it for you - manager saying). This might be a bit of work depending what kind of feature you want to expose to your users. I'm planning to have a custom panel that lets me test all - or most - of the features provided by the Markup API, so better spend a bit of time having a convenient UI.
What the doc doesn't say is that the markup extension isn't part of the viewer3d.js, so you have to include an extra script before attempting to use it:
https://autodeskviewer.com/viewers/2.2/extensions/MarkupsCore.js
https://autodeskviewer.com/viewers/2.2/extensions/MarkupsCore.min.js (minified version)
You then need to explicitly load the extension in the viewer as parameter when creating the viewer object with options or at a later stage:
1 viewer.loadExtension("Autodesk.Viewing.MarkupsCore"); 2 3 var markupsExtension = viewer.getExtension( 4 "Autodesk.Viewing.MarkupsCore");
You are now all set to start using the extension object by invoking its methods. See there for the complete Markups Core API reference.
How the API is working? Well you have two modes: Edit and View. In edit mode the user can draw markups onto the screen, your app can then ask the extension to save the current state of the markups as a string. In view mode your app can load previously created markups from the strings into named layers and then toggle ON or OFF those layers at will. Storage of that markup information is upon your responsibility. It is also important to mention that while in edit mode you cannot load previous markups and while in view mode you cannot create new markups, this is as-designed. Finally, while in view mode the extension maintains a stack of actions that allows to easily undo/redo the actions a user may perform: creating markup, moving, resizing, deleting, ...
For more details on how to perform those actions, you can refer to the tutorial mentioned above. One modification to take into account is that methods enterViewMode and leaveViewMode have been renamed respectively show and hide. You however still have methods enterEditMode and leaveEditMode.
At the time of this writing, I was not able to modify the svg style of the markups, so width, stroke style and colour stick to the default values. I'm working on it and will update the code when I can sort it out...
That's it, most what the API offers now it doable from my custom panel, it can save current markups to named layer and while in view mode you can toggle those layers visibility. It's a demo sample, so it does not persist the layers across sessions or if you unload the extension but from there it would be easily extensible to save that to a database.
Here is how it looks when loaded in the viewer and a link where you can test it live. The next step will be to create my own custom kind of markup (custom EditMode, as mentioned by the tutorial). Stay tuned ...
Complete code with all source files is available from Autodesk.ADN.Viewing.Extension.Markup
///////////////////////////////////////////////////////////////////// | |
// Autodesk.ADN.Viewing.Extension.Markup | |
// by Philippe Leefsma, Feb 2016 | |
// | |
///////////////////////////////////////////////////////////////////// | |
AutodeskNamespace("Autodesk.ADN.Viewing.Extension"); | |
Autodesk.ADN.Viewing.Extension.Markup = function (viewer, options) { | |
Autodesk.Viewing.Extension.call(this, viewer, options); | |
var _panel = null; | |
///////////////////////////////////////////////////////////////// | |
// Extension load callback | |
// | |
///////////////////////////////////////////////////////////////// | |
this.load = function () { | |
// Online markup API | |
//"https://autodeskviewer.com/viewers/2.1/extensions/markupsCore.min.js" | |
var extId = 'Autodesk.ADN.Viewing.Extension.Markup/'; | |
var es6 = 'api/extensions/transpile/' + extId; | |
var es5 = 'uploads/extensions/' + extId; | |
loadCss([ | |
es5 + 'style.css', | |
es5 + 'spectrum.css', | |
]); | |
require.config({ | |
//some dependencies need to go through transpiling | |
paths: { | |
'markupsCore': es5 + 'markupsCore', | |
'spectrum': es5 + 'spectrum', | |
'switch': es6 + 'switch' | |
}, | |
waitSeconds: 0 | |
}); | |
var modules = [ | |
'markupsCore', | |
'spectrum', | |
'switch' | |
]; | |
// Dynamic dependency loading with RequireJS | |
require(modules, ()=> { | |
var button = createButton(guid(), | |
'glyphicon glyphicon-edit', | |
'Markup Panel', ()=>{ | |
_panel.toggleVisibility(); | |
}); | |
var viewerToolbar = viewer.getToolbar(true); | |
var ctrlGroup = new Autodesk.Viewing.UI.ControlGroup( | |
'Autodesk.ADN.Viewing.Extension.Markup'); | |
ctrlGroup.addControl(button, {index:1}); | |
viewerToolbar.addControl(ctrlGroup); | |
_panel = new MarkupPanel( | |
viewer.container, | |
guid(), | |
button.container); | |
console.log('Autodesk.ADN.Viewing.Extension.Markup loaded'); | |
}); | |
return true; | |
} | |
///////////////////////////////////////////////////////////////// | |
// Extension unload callback | |
// | |
///////////////////////////////////////////////////////////////// | |
this.unload = function () { | |
if(_panel) { | |
_panel.unload(); | |
_panel = null; | |
var viewerToolbar = viewer.getToolbar(true); | |
viewerToolbar.removeControl( | |
'Autodesk.ADN.Viewing.Extension.Markup'); | |
} | |
console.log('Autodesk.ADN.Viewing.Extension.Markup unloaded'); | |
return true; | |
} | |
///////////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////////// | |
function loadCss(css) { | |
css.forEach((path)=>{ | |
$('<link/>', { | |
rel: 'stylesheet', | |
type: 'text/css', | |
href: path | |
}).appendTo('head'); | |
}); | |
} | |
///////////////////////////////////////////////////////////////// | |
// toolbar button | |
// | |
///////////////////////////////////////////////////////////////// | |
function createButton(id, className, tooltip, handler) { | |
var button = new Autodesk.Viewing.UI.Button(id); | |
button.icon.style.fontSize = "24px"; | |
button.icon.className = className; | |
button.setToolTip(tooltip); | |
button.onClick = handler; | |
return button; | |
} | |
///////////////////////////////////////////////////////////////// | |
// Generates random guid to use as DOM id | |
// | |
///////////////////////////////////////////////////////////////// | |
function guid() { | |
var d = new Date().getTime(); | |
var guid = 'xxxx-xxxx-xxxx-xxxx'.replace( | |
/[xy]/g, | |
function (c) { | |
var r = (d + Math.random() * 16) % 16 | 0; | |
d = Math.floor(d / 16); | |
return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16); | |
}); | |
return guid; | |
} | |
///////////////////////////////////////////////////////////////// | |
// MarkupPanel | |
// | |
///////////////////////////////////////////////////////////////// | |
var MarkupPanel = function( | |
parentContainer, | |
panelId, | |
btnElement) { | |
///////////////////////////////////////////////////////////////// | |
// Base class constructor | |
// | |
///////////////////////////////////////////////////////////////// | |
Autodesk.Viewing.UI.DockingPanel.call( | |
this, | |
parentContainer, | |
panelId, | |
'ADN Markup Panel', | |
{shadow: true}); | |
///////////////////////////////////////////////////////////////// | |
// "Private" members | |
// | |
///////////////////////////////////////////////////////////////// | |
var _thisPanel = this; | |
var _viewMode = true; | |
var _controlIds = []; | |
var _layerItems = {}; | |
var MarkupsCore = null; | |
var _isVisible = false; | |
var _isMinimized = false; | |
var _markupsExtension = null; | |
///////////////////////////////////////////////////////////// | |
// Custom html | |
// | |
///////////////////////////////////////////////////////////// | |
function generateHtml(id) { | |
var html = ` | |
<div class="container"> | |
<div class="switch-container" id="${id}-switch-container" | |
data-placement="bottom" | |
data-toggle="tooltip" | |
title="Switch between Edit/View Mode"> | |
</div> | |
<div id="${id}-dropdown-mode-container" | |
class="dropdown-mode-container"> | |
</div> | |
<hr class="v-spacer"> | |
<div style="clear: left;"> | |
<input type="text" id="${id}-spectrum"/> | |
<input id="${id}-style" type="text" | |
class="input styles" | |
value='"stroke-width":0.1, "stroke-opacity":1' | |
data-placement="bottom" | |
data-toggle="tooltip" | |
title="Set svg style properties"> | |
</div> | |
<hr class="v-spacer-large"> | |
<div style="clear: left;"> | |
<button class="btn btn-info btn-row" | |
id="${id}-btn-undo" | |
data-placement="bottom" | |
data-toggle="tooltip" | |
title="Undo last markup action" | |
disabled> | |
<span class="fa fa-undo btn-span"> | |
</span> | |
Undo | |
</button> | |
<button class="btn btn-info btn-row" | |
id="${id}-btn-redo" | |
data-placement="bottom" | |
data-toggle="tooltip" | |
title="Redo last markup action" | |
disabled> | |
<span class="fa fa-repeat btn-span"> | |
</span> | |
Redo | |
</button> | |
<button class="btn btn-info btn-row" | |
id="${id}-btn-clear-all" | |
data-placement="bottom" | |
data-toggle="tooltip" | |
title="Clear All current markups" | |
disabled> | |
<span class="glyphicon glyphicon-trash btn-span"> | |
</span> | |
Clear | |
</button> | |
</div> | |
<hr class="v-spacer-large"> | |
<div class="layer-container"> | |
<button class="btn btn-info" | |
id="${id}-btn-save" | |
data-placement="bottom" | |
data-toggle="tooltip" | |
title="Save current markups as layer" | |
disabled> | |
<span class="glyphicon glyphicon-floppy-open btn-span" | |
aria-hidden="true"> | |
</span> | |
Save | |
</button> | |
<input id="${id}-layer-name" type="text" | |
class="input" | |
placeholder=" Layer Name ..."> | |
<hr class="v-spacer-large"> | |
<div class="layer-list" id="${id}-layer-list"> | |
</div> | |
</div> | |
</div>`; | |
return html; | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function initialize() { | |
_thisPanel.content = document.createElement('div'); | |
$(_thisPanel.container).addClass('markup'); | |
$(_thisPanel.container).append(generateHtml(panelId)); | |
$('[data-toggle="tooltip"]').tooltip(); | |
var modes = [ | |
{ | |
label: 'Arrow', | |
handler: ()=>{ | |
setEditMode('arrow'); | |
} | |
}, | |
{ | |
label: 'Circle', | |
handler: ()=>{ | |
setEditMode('circle'); | |
} | |
}, | |
{ | |
label: 'Cloud', | |
handler: ()=>{ | |
setEditMode('cloud'); | |
} | |
}, | |
{ | |
label: 'Free Hand', | |
handler: ()=>{ | |
setEditMode('freehand'); | |
} | |
}, | |
{ | |
label: 'Rectangle', | |
handler: ()=>{ | |
setEditMode('rectangle'); | |
} | |
}, | |
{ | |
label: 'Text', | |
handler: ()=>{ | |
setEditMode('text'); | |
} | |
} | |
]; | |
var dropdown = createDropdownMenu( | |
`#${panelId}-dropdown-mode-container`, | |
'Markup Mode', | |
modes); | |
$(`#${panelId}-spectrum`).spectrum({ | |
color: '#FF0000', | |
change: function (color) { | |
var clr = color.toHexString(); | |
} | |
}); | |
_controlIds = [ | |
`#${dropdown.buttonId}`, | |
`#${panelId}-btn-undo`, | |
`#${panelId}-btn-redo`, | |
`#${panelId}-btn-clear-all`, | |
`#${panelId}-btn-save` | |
]; | |
createSwitchButton(`#${panelId}-switch-container`, false, | |
function(checked){ | |
if(checked){ | |
enterEditMode(); | |
} | |
else{ | |
enterViewMode(); | |
} | |
}); | |
$(`#${panelId}-btn-undo`).click(onUndo); | |
$(`#${panelId}-btn-redo`).click(onRedo); | |
$(`#${panelId}-btn-clear-all`).click(onClearMarkups); | |
$(`#${panelId}-btn-save`).click(onSaveLayer); | |
$(`#${panelId}-style`).focusout(setMarkupStyle); | |
} | |
/////////////////////////////////////////////////////////////////////////// | |
// Creates dropdown menu from input | |
// | |
/////////////////////////////////////////////////////////////////////////// | |
function createDropdownMenu(parent, title, menuItems, selectedItemIdx) { | |
var buttonId = guid(); | |
var labelId = guid(); | |
var menuId = guid(); | |
var listId = guid(); | |
var html = ` | |
<div id="${menuId}" class="dropdown chart-dropdown"> | |
<button id="${buttonId}" | |
class="btn btn-default btn-dropdown dropdown-toggle" | |
type="button" | |
data-toggle="dropdown" | |
disabled> | |
<label id="${labelId}" | |
style="font: normal 14px Times New Roman; margin-top:-4px;"> | |
${title} | |
</label> | |
<span class="caret btn-span"></span> | |
</button> | |
<ul id="${listId}" class="dropdown-menu scrollable-menu"> | |
</ul> | |
</div> | |
`; | |
$(parent).append(html); | |
$('#' + labelId).text( | |
title + ': ' + menuItems[selectedItemIdx || 0].label); | |
menuItems.forEach(function(menuItem){ | |
var itemId = guid(); | |
var itemHtml = ` | |
<li id="${itemId}"> | |
<a href="">${menuItem.label}</a> | |
</li>`; | |
$('#' + listId).append(itemHtml); | |
$('#' + itemId).click(function(event) { | |
event.preventDefault(); | |
menuItem.handler(); | |
$('#' + labelId).text( | |
title + ': ' + menuItem.label); | |
}); | |
}); | |
return { | |
menuId: menuId, | |
buttonId: buttonId | |
}; | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function enterEditMode() { | |
_viewMode = false; | |
_markupsExtension.hide(); | |
_markupsExtension.enterEditMode(); | |
_controlIds.forEach((id)=> { | |
$(id).prop('disabled', false); | |
}); | |
$(`#${panelId}-layer-list`).removeClass( | |
'view-mode'); | |
setMarkupStyle(); | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function enterViewMode() { | |
_viewMode = true; | |
_markupsExtension.leaveEditMode(); | |
_markupsExtension.show(); | |
_controlIds.forEach((id)=> { | |
$(id).prop('disabled', true); | |
}); | |
$(`#${panelId}-layer-list`).addClass( | |
'view-mode'); | |
loadMarkups(); | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function onUndo() { | |
_markupsExtension.undo(); | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function onRedo() { | |
_markupsExtension.redo(); | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function onClearMarkups() { | |
_markupsExtension.clear(); | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function onSaveLayer() { | |
var $input = $(`#${panelId}-layer-name`); | |
var name = $input.val(); | |
$input.val(''); | |
name = (name.length ? | |
name : | |
(new Date().toString('d/M/yyyy H:mm:ss')).split('GMT')[0]); | |
var item = { | |
name: name, | |
id: guid(), | |
enabled: false, | |
markupData: _markupsExtension.generateData() | |
} | |
_layerItems[item.id] = item; | |
addLayerItem(item); | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function addLayerItem(item) { | |
var itemHtml = ` | |
<div id="${item.id}" | |
class="list-group-item ${item.enabled ? 'enabled' : ''}"> | |
${item.name} | |
<button id="${item.id}-delete-btn" | |
class="btn btn-danger btn-list"> | |
<span class="glyphicon glyphicon-remove-sign btn-span-list"> | |
</span> | |
</button> | |
</div> | |
`; | |
$(`#${panelId}-layer-list`).append(itemHtml); | |
var $item = $(`#${item.id}`); | |
$item.click(()=>{ | |
if(_viewMode) { | |
$item.toggleClass('enabled'); | |
if ($item.hasClass('enabled')) { | |
_markupsExtension.loadMarkups( | |
item.markupData, | |
item.id); | |
//_markupsExtension.showMarkups(item.id); | |
} | |
else { | |
_markupsExtension.unloadMarkups(item.id); | |
//_markupsExtension.hideMarkups(item.id); | |
} | |
} | |
}); | |
$(`#${item.id}-delete-btn`).click(()=>{ | |
_markupsExtension.unloadMarkups(item.id); | |
delete _layerItems[item.id]; | |
$item.remove(); | |
}); | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function loadMarkups() { | |
$('.markup .list-group-item').each(function(){ | |
var $item = $(this); | |
if($item.hasClass('enabled')) { | |
var item = _layerItems[this.id]; | |
_markupsExtension.loadMarkups( | |
item.markupData, | |
item.id); | |
} | |
}); | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function setEditMode(mode) { | |
switch(mode){ | |
case 'arrow': | |
var mode = new MarkupsCore.EditModeArrow(_markupsExtension); | |
_markupsExtension.changeEditMode(mode); | |
break; | |
case 'circle': | |
var mode = new MarkupsCore.EditModeCircle(_markupsExtension); | |
_markupsExtension.changeEditMode(mode); | |
break; | |
case 'cloud': | |
var mode = new MarkupsCore.EditModeCloud(_markupsExtension); | |
_markupsExtension.changeEditMode(mode); | |
break; | |
case 'freehand': | |
var mode = new MarkupsCore.EditModeFreehand(_markupsExtension); | |
_markupsExtension.changeEditMode(mode); | |
break; | |
case 'rectangle': | |
var mode = new MarkupsCore.EditModeRectangle(_markupsExtension); | |
_markupsExtension.changeEditMode(mode); | |
break; | |
case 'text': | |
var mode = new MarkupsCore.EditModeText(_markupsExtension); | |
_markupsExtension.changeEditMode(mode); | |
break; | |
} | |
} | |
///////////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////////// | |
function setMarkupStyle() { | |
if(!_viewMode) { | |
try { | |
var styleStr = '{' + $(`#${panelId}-style`).val() + '}'; | |
var style = JSON.parse(styleStr); | |
var styleAttributes = ['stroke-width', 'stroke-color', 'stroke-opacity']; | |
var nsu = Autodesk.Viewing.Extensions.Markups.Core.Utils; | |
var styleObject = nsu.createStyle(styleAttributes, viewer); | |
console.log(JSON.stringify(styleObject)) | |
console.log(style) | |
_markupsExtension.setStyle(style); | |
} | |
catch(ex){ | |
console.log(ex); | |
} | |
} | |
} | |
///////////////////////////////////////////////////////////// | |
// unload extension | |
// | |
///////////////////////////////////////////////////////////// | |
_thisPanel.unload = function() { | |
_thisPanel.setVisible(false); | |
deactivateMarkups(); | |
} | |
///////////////////////////////////////////////////////////// | |
// setVisible override | |
// | |
///////////////////////////////////////////////////////////// | |
_thisPanel.setVisible = function(show) { | |
_isVisible = show; | |
btnElement.classList.toggle('active'); | |
Autodesk.Viewing.UI.DockingPanel.prototype. | |
setVisible.call(this, show); | |
if(show){ | |
viewer.loadExtension("Autodesk.Viewing.MarkupsCore"); | |
_markupsExtension = viewer.getExtension( | |
"Autodesk.Viewing.MarkupsCore"); | |
MarkupsCore = Autodesk.Viewing.Extensions.Markups.Core; | |
enterViewMode(); | |
} | |
else { | |
viewer.unloadExtension("Autodesk.Viewing.MarkupsCore"); | |
_markupsExtension = null; | |
} | |
} | |
///////////////////////////////////////////////////////////// | |
// Toggles panel visibility | |
// | |
///////////////////////////////////////////////////////////// | |
_thisPanel.toggleVisibility = function() { | |
_panel.setVisible(!_isVisible); | |
} | |
///////////////////////////////////////////////////////////// | |
// initialize override | |
// | |
///////////////////////////////////////////////////////////// | |
_thisPanel.initialize = function() { | |
this.title = this.createTitleBar( | |
this.titleLabel || | |
this.container.id); | |
this.closer = this.createCloseButton(); | |
this.container.appendChild(this.title); | |
this.title.appendChild(this.closer); | |
this.container.appendChild(this.content); | |
this.initializeMoveHandlers(this.title); | |
this.initializeCloseHandler(this.closer); | |
} | |
///////////////////////////////////////////////////////////// | |
// onTitleDoubleClick override | |
// | |
///////////////////////////////////////////////////////////// | |
_thisPanel.onTitleDoubleClick = function (event) { | |
_isMinimized = !_isMinimized; | |
if(_isMinimized) { | |
$(_thisPanel.container).addClass( | |
'minimized'); | |
} | |
else { | |
$(_thisPanel.container).removeClass( | |
'minimized'); | |
} | |
} | |
// Initializes the panel | |
initialize(); | |
} | |
///////////////////////////////////////////////////////////// | |
// Set up JS inheritance | |
// | |
///////////////////////////////////////////////////////////// | |
MarkupPanel.prototype = Object.create( | |
Autodesk.Viewing.UI.DockingPanel.prototype); | |
MarkupPanel.prototype.constructor = MarkupPanel; | |
}; | |
Autodesk.ADN.Viewing.Extension.Markup.prototype = | |
Object.create(Autodesk.Viewing.Extension.prototype); | |
Autodesk.ADN.Viewing.Extension.Markup.prototype.constructor = | |
Autodesk.ADN.Viewing.Extension.Markup; | |
Autodesk.Viewing.theExtensionManager.registerExtension( | |
'Autodesk.ADN.Viewing.Extension.Markup', | |
Autodesk.ADN.Viewing.Extension.Markup); |