├── Procfile ├── .gitignore ├── html ├── favicon.ico ├── img │ ├── team.jpg │ ├── devcon.png │ └── forge.png ├── js │ ├── google-analytics-tracker.js │ ├── stubs.js │ ├── vr-party-common.js │ ├── Autodesk.ADN.Viewing.Extension.VR.js │ ├── jquery.qrcode.min.js │ ├── Autodesk.ADN.Toolkit.ViewData.js │ ├── vr-party-participant.js │ ├── vr-party-participant-stereo.js │ ├── vr-party-presenter-stereo.js │ └── vr-party-presenter.js ├── css │ ├── about.css │ ├── style.css │ └── participant.css ├── landing.html ├── participant.html ├── participant2.html ├── index2.html ├── about.html ├── index.html └── res │ └── locales │ └── en │ └── allstrings.json ├── package.json ├── api.js ├── README.md ├── server.js └── sockets.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | credentials.js 2 | node_modules/* 3 | -------------------------------------------------------------------------------- /html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeanW/Vrok-It/HEAD/html/favicon.ico -------------------------------------------------------------------------------- /html/img/team.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeanW/Vrok-It/HEAD/html/img/team.jpg -------------------------------------------------------------------------------- /html/img/devcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeanW/Vrok-It/HEAD/html/img/devcon.png -------------------------------------------------------------------------------- /html/img/forge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeanW/Vrok-It/HEAD/html/img/forge.png -------------------------------------------------------------------------------- /html/js/google-analytics-tracker.js: -------------------------------------------------------------------------------- 1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 3 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 4 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 5 | 6 | ga('create', 'UA-63934239-1', 'auto'); 7 | ga('send', 'pageview'); -------------------------------------------------------------------------------- /html/js/stubs.js: -------------------------------------------------------------------------------- 1 | 2 | (function(global) { 3 | function NoSleep() {}; 4 | NoSleep.prototype.enable = function() {}; 5 | NoSleep.prototype.disable = function() {}; 6 | global.NoSleep = NoSleep; 7 | })(this); 8 | 9 | THREE.DeviceOrientationControls = function () { 10 | this.connect = function() {}; 11 | this.update = function() { return null }; 12 | this.disconnect = function() {}; 13 | }; 14 | 15 | VideoHelper = function () { 16 | this.start = function() {}; 17 | this.isStarted = function() { return false }; 18 | this.stop = function() {}; 19 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VRPartyServer", 3 | "version": "0.1.0dev", 4 | "private": true, 5 | "author": "Kean Walmsley - Autodesk", 6 | "description": "Node.js Server app for the VR Party app", 7 | "contributors": [ 8 | {"name": "Kean Walmsley"}, 9 | {"name": "Lars Schneider"}, 10 | {"name": "Oleg Dedkow"} 11 | ], 12 | "scripts": { 13 | "predeploy": "echo Deploying VRPartyServer", 14 | "postdepoly": "echo VRPartyServer Deployed" 15 | }, 16 | "dependencies": { 17 | "socket.io": "*", 18 | "express": "4.x", 19 | "morgan": "1.x", 20 | "request": "*" 21 | }, 22 | "engines": { 23 | "node": "0.10.x" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /html/css/about.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Merriweather', Times, serif; 3 | padding-top: 0px; 4 | margin: 0px 100px; 5 | text-align: justify; 6 | } 7 | 8 | p { 9 | line-height: 170%; 10 | } 11 | 12 | h1, h2, h3, h4, h5, h6 { 13 | font-family: 'Lato', Arial, sans-serif; 14 | } 15 | 16 | h1, p.desc { 17 | text-align: center; 18 | } 19 | 20 | p.desc { 21 | font-size: 22px; 22 | line-height: 150%; 23 | } 24 | 25 | a, a:link, a:visited, a:visited:hover, a:hover { 26 | text-decoration: none; 27 | } 28 | 29 | a { 30 | color: #006699; 31 | } 32 | 33 | a:hover { 34 | color: #000; 35 | } 36 | 37 | a:active, a:hover { 38 | outline: 0; 39 | } 40 | 41 | img { 42 | margin: 20px auto; 43 | display:block; 44 | width: 60%; 45 | border-color: #555; 46 | border-style: solid; 47 | border-width: 10px; 48 | } 49 | 50 | figure { 51 | margin: 30px; 52 | } 53 | 54 | figcaption { 55 | font-style: italic; 56 | text-align: center; 57 | } 58 | 59 | .container { 60 | position: relative; 61 | left: 10%; 62 | width: 80%; 63 | height: 0; 64 | padding-bottom: 45%; 65 | margin-top: 30px; 66 | margin-bottom: 30px; 67 | } 68 | 69 | .video { 70 | position: absolute; 71 | top: 0%; 72 | left: 0%; 73 | width: 100%; 74 | height: 100%; 75 | box-shadow: 8px 8px 4px #888888; 76 | } -------------------------------------------------------------------------------- /html/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Arial, sans-serif; 3 | -webkit-text-size-adjust: none; 4 | color: #333; 5 | margin: 0 auto; 6 | padding: 10px; 7 | display: block; 8 | position: relative; 9 | } 10 | 11 | p.info { 12 | font-family: 'Helvetica Neue', Arial, sans-serif; 13 | font-size: 10px; 14 | text-transform: uppercase; 15 | text-align: center; 16 | } 17 | 18 | .cmd-btn-small, .cmd-btn-small-x { 19 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 20 | font-size: 12px; 21 | border: 1px solid #555; 22 | border-radius: 2px; 23 | color: #555; 24 | text-transform: uppercase; 25 | text-align: center; 26 | width: 120px; 27 | padding-top: 5px; 28 | padding-bottom: 5px; 29 | } 30 | 31 | .cmd-btn-small { 32 | background: transparent; 33 | } 34 | 35 | .cmd-btn-small-x { 36 | width: 120px; 37 | background: #ccc; 38 | } 39 | 40 | .cmd-btn-small:hover, .cmd-btn-small-x:hover { 41 | cursor: pointer; 42 | color: #fff; 43 | } 44 | 45 | .cmd-btn-small:hover { 46 | background-color: #555; 47 | } 48 | .cmd-btn-small-x:hover { 49 | background-color: #222; 50 | } 51 | 52 | a { 53 | color: blue; 54 | color: hsl( 220, 90%, 40% ); 55 | text-decoration: none; 56 | } 57 | 58 | a:hover { 59 | background-color: blue; 60 | background-color: hsl( 220, 90%, 50% ); 61 | color: white; 62 | } 63 | 64 | .aboutLayer { 65 | text-indent: -2000px; 66 | } 67 | 68 | .full { 69 | width: 100%; 70 | height: 1800px; 71 | } 72 | 73 | .highlight { 74 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 75 | font-size: 18px; 76 | font-weight: bold; 77 | } -------------------------------------------------------------------------------- /html/landing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VR at Autodesk 6 | 7 | 8 | 9 | 10 | 35 | 36 | 37 |
38 |

Mobile VR demos

39 |
40 |
3D Viewer
41 | 42 |
Play
43 |
 
44 |
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /html/participant.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vrok-it 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | 2 | /////////////////////////////////////////////////////////////////// 3 | // Copyright (c) 2014 Autodesk, Inc. All rights reserved 4 | // 5 | // Permission to use, copy, modify, and distribute this software in 6 | // object code form for any purpose and without fee is hereby granted, 7 | // provided that the above copyright notice appears in all copies and 8 | // that both that copyright notice and the limited warranty and 9 | // restricted rights notice below appear in all supporting 10 | // documentation. 11 | // 12 | // AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS. 13 | // AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF 14 | // MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. AUTODESK, INC. 15 | // DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE 16 | // UNINTERRUPTED OR ERROR FREE. 17 | /////////////////////////////////////////////////////////////////// 18 | 19 | var BASE_URL = 'https://developer.api.autodesk.com'; 20 | 21 | var request = require('request'); 22 | 23 | function getScopedToken(res, params) { 24 | request.post(BASE_URL + '/authentication/v1/authenticate', 25 | { form: params }, 26 | function (error, response, body) { 27 | if (!error && response.statusCode == 200) { 28 | var authResponse = JSON.parse(body); 29 | res.send(authResponse); 30 | } 31 | else { 32 | console.log("Token error: "); 33 | if (response && response.statusCode) { 34 | console.log (response.statusCode); 35 | } 36 | } 37 | } 38 | ); 39 | } 40 | 41 | exports.getToken = function (req, res) { 42 | 43 | var params = { 44 | client_id: process.env.CONSUMER_KEY, 45 | client_secret: process.env.CONSUMER_SECRET, 46 | grant_type: 'client_credentials', 47 | scope: 'data:read' 48 | } 49 | getScopedToken(res, params); 50 | }; 51 | 52 | exports.getUploadToken = function (req, res) { 53 | 54 | var params = { 55 | client_id: process.env.CONSUMER_KEY, 56 | client_secret: process.env.CONSUMER_SECRET, 57 | grant_type: 'client_credentials', 58 | scope: 'data:read data:write bucket:read bucket:create' 59 | } 60 | getScopedToken(res, params); 61 | }; 62 | 63 | -------------------------------------------------------------------------------- /html/participant2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vrok-it 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /html/css/participant.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | width: 100%; 4 | height: 100%; 5 | padding: 0; 6 | margin: -1px; 7 | } 8 | 9 | .cmd-btn { 10 | font-size: 48px; 11 | border: 3px solid #555; 12 | border-radius: 5px; 13 | color: #555; 14 | background: transparent; 15 | text-transform: uppercase; 16 | text-align: center; 17 | padding: 15px; 18 | width: 350px; 19 | left: 50%; 20 | top: 50%; 21 | transform:translate(-50%,-60%); 22 | -webkit-transform:translate(-50%,-60%); 23 | position:absolute; 24 | } 25 | 26 | .message1, .message2 { 27 | font-size: 26px; 28 | color: #888; 29 | background: transparent; 30 | text-transform: uppercase; 31 | text-align: center; 32 | top: 50%; 33 | width: 50%; 34 | position: absolute; 35 | } 36 | 37 | .message1 { 38 | text-shadow: -0.035em 0 #333 39 | } 40 | 41 | .message2 { 42 | text-shadow: 0.035em 0 #333 43 | } 44 | 45 | .blink { 46 | -webkit-animation: blink 2s step-end infinite; 47 | -moz-animation: blink 2s step-end infinite; 48 | -o-animation: blink 2s step-end infinite; 49 | animation: blink 2s step-end infinite; 50 | } 51 | 52 | @-webkit-keyframes blink { 53 | 67% { opacity: 0 } 54 | } 55 | 56 | @-moz-keyframes blink { 57 | 67% { opacity: 0 } 58 | } 59 | 60 | @-o-keyframes blink { 61 | 67% { opacity: 0 } 62 | } 63 | 64 | @keyframes blink { 65 | 67% { opacity: 0 } 66 | } 67 | 68 | .control { 69 | width: 100%; 70 | height: 100%; 71 | } 72 | 73 | .cmd-btn:hover { 74 | cursor: pointer; 75 | color: #fff; 76 | background-color: #555; 77 | } 78 | 79 | .layer1, .layer2, .layer3 { 80 | margin: 3px; 81 | left: 0; 82 | right: 0; 83 | width: 100%; 84 | height: 100%; 85 | } 86 | 87 | .layer1 { 88 | z-index: 2; 89 | display: inline-block; 90 | top: 0; 91 | } 92 | 93 | .layer2 { 94 | z-index: 1; 95 | overflow: hidden; 96 | top: 0; 97 | } 98 | 99 | .layer3 { 100 | z-index: 0; 101 | top: 0; 102 | } 103 | 104 | .outer { 105 | position: relative; 106 | margin: 0; 107 | width: 100%; 108 | height: 100%; 109 | display: table; 110 | } 111 | 112 | .viewer { 113 | width: 50%; 114 | height: 100%; 115 | } 116 | -------------------------------------------------------------------------------- /html/js/vr-party-common.js: -------------------------------------------------------------------------------- 1 | // Define a String.startsWith() function 2 | 3 | if (typeof String.prototype.startsWith != 'function') { 4 | String.prototype.startsWith = function (str) { 5 | return this.indexOf(str) === 0; 6 | }; 7 | } 8 | 9 | 10 | if (typeof String.prototype.ensurePrefix != 'function') { 11 | String.prototype.ensurePrefix = function (str) { 12 | return (this.startsWith(str) ? this : str + this); 13 | }; 14 | } 15 | 16 | 17 | function getURLParameter(param) { 18 | var pageURL = window.location.search.substring(1); 19 | var urlVariables = pageURL.split('&'); 20 | for (var i = 0; i < urlVariables.length; i++) { 21 | var paramName = urlVariables[i].split('='); 22 | if (paramName[0] === param) { 23 | return paramName[1]; 24 | } 25 | } 26 | return null; 27 | } 28 | 29 | 30 | function getViewingOptions() { 31 | var options = {}; 32 | options.env = 'AutodeskProduction'; 33 | options.useConsolidation = true; 34 | options.getAccessToken = function(onSuccess) { 35 | $.get( 36 | window.location.origin + '/api/token', 37 | function (accessTokenResponse) { 38 | onSuccess( 39 | accessTokenResponse.access_token, 40 | accessTokenResponse.expires_in 41 | ); 42 | } 43 | ); 44 | }; 45 | return options; 46 | } 47 | 48 | 49 | function getScopedViewingOptions(urn) { 50 | var getToken = function() { 51 | var token = null; 52 | jQuery.ajax({ 53 | url: window.location.origin + '/api/token', 54 | success: function (result) { 55 | token = result.access_token; 56 | }, 57 | async: false 58 | }); 59 | return token; 60 | }; 61 | var options = { 62 | 'document' : urn.ensurePrefix('urn:'), 63 | 'env' : 'AutodeskProduction', 64 | 'getAccessToken' : getToken, 65 | 'refreshToken' : getToken 66 | }; 67 | return options; 68 | } 69 | 70 | 71 | function loadModel(viewer, documentData) { 72 | viewer.resize(); 73 | viewer.load( 74 | documentData, 75 | null, 76 | function() { 77 | viewer.navigation.setZoomTowardsPivot(true); 78 | viewer.navigation.setReverseZoomDirection(true); 79 | viewer.setLightPreset(0); 80 | } 81 | ); 82 | } 83 | 84 | 85 | function getModel(documentData) { 86 | var geometryItems = []; 87 | if (geometryItems.length == 0) { 88 | geometryItems = Autodesk.Viewing.Document.getSubItemsWithProperties( 89 | documentData.getRootItem(), 90 | { 'type': 'geometry', 'role': '3d' }, 91 | true 92 | ); 93 | } 94 | if (geometryItems.length === 0) { 95 | return null; 96 | } 97 | return documentData.getViewablePath(geometryItems[0]); 98 | } 99 | 100 | // From http://davidwalsh.name/essential-javascript-functions 101 | // 102 | // Returns a function, that, as long as it continues to be invoked, will not 103 | // be triggered. The function will be called after it stops being called for 104 | // N milliseconds. If `immediate` is passed, trigger the function on the 105 | // leading edge, instead of the trailing. 106 | function debounce(func, wait, immediate) { 107 | var timeout; 108 | return function() { 109 | var context = this, args = arguments; 110 | var later = function() { 111 | timeout = null; 112 | if (!immediate) func.apply(context, args); 113 | }; 114 | var callNow = immediate && !timeout; 115 | clearTimeout(timeout); 116 | timeout = setTimeout(later, wait); 117 | if (callNow) func.apply(context, args); 118 | }; 119 | }; -------------------------------------------------------------------------------- /html/index2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vrok-it 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 54 | 65 | 66 |
23 |
24 |

Scan to connect with Google Cardboard
then load a model from the list below

25 |
26 | 27 |
28 |
Upload file 33 |
34 |
35 |
36 |
37 |
New session 42 |
43 |
44 | 51 | 52 |
53 |
55 |
56 |
57 |
58 |
59 |
60 | 61 | 62 |
63 |
64 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /html/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to Vrok-it! 6 | 7 | 8 | 9 | 10 |

Welcome to Vrok-it!

11 |

12 | "Vrok" is a combination of Virtual Reality Online Collaboration and the verb to grok. Vrok-it allows multiple people to grok a 3D model in a way that simply wasn’t possible before. 13 |

14 |

Getting started

15 |

16 | Click the QR code – or scan it from your phone – to launch a stereoscopic 3D viewer for Google Cardboard* connected to this session. The 3D models you load via this page – and operations you perform on them – will be visible to all connected viewers. You can upload your own models, but for best results keep these under 2MB in size. 17 |

18 |

Here's a video of Vrok-it in action showing both this master page and a connected client device:

19 |
20 | 21 |
22 |

Origins

23 |

24 | Vrok-it was born in May 2015, when three software engineers from Autodesk entered a team in the VR Hackathon in San Francisco. The goal of the "VR Party" team was to enable a guide to assist someone immersed in virtual reality, essentially curating the VR experience. The guide would perform actions that the VR participant might otherwise find cumbersome, such as exploding a 3D model, zooming in on it, or cutting it with a section plane. Making VR more personal and social makes it an ideal tool for communicating design information, for architectural fly-throughs, site walk-throughs and model review. 25 |

26 |

27 | The team soon realized it would be easy to allow the guide to curate the VR experience for multiple participants, much in the same way as proposed by Google Expeditions, announced just the week after at Google I/O 2015. 28 |

29 |
30 | 31 |
The team behind Vrok-it is (left to right) Lars Schneider, Kean Walmsley and Oleg Dedkow.
32 |
33 |

34 | The project was well-received and was awarded the Hackathon prize for the "Best Web-based VR Project". 35 |

36 |

37 | Over the following weeks the team implemented additional features, such as making it possible for people to create and manage simultaneous sessions. When launched "VR Party" became Vrok-it. 38 |

39 |

Technology

40 |

41 | At its core, the project makes use of the Autodesk View & Data API, a web-based infrastructure for viewing 3D models, and leverages work previously published on Kean's blog. It has a Node.js back-end, with Socket.io handling the communication between presenter and participants. The site is hosted on Heroku and the complete source code for Vrok-it is available on GitHub. 42 |

43 |

44 | * We recommend these excellent googles from GoggleTech. 45 |

46 | 47 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vrok-it 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 56 | 67 | 68 |
23 |
24 |
25 |
26 |

Scan to connect with Google Cardboard
then load a model from the list below

27 |
28 | 29 |
30 |
Upload file 35 |
36 |
37 |
38 |
39 |
New session 44 |
45 |
46 | 53 | 54 |
55 |
57 |
58 |
59 |
60 |
61 |
62 | 63 | 64 |
65 |
66 |
69 | 70 | 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vrok-It 2 | _View and collaborate on 3D models inside Virtual Reality using Autodesk's Forge platform._ 3 | 4 | ## What does Vrok mean? 5 | "Vrok" is a combination of Virtual Reality Online Collaboration and the verb to grok. Vrok-it allows multiple people to grok a 3D model in a way that simply wasn’t possible before. 6 | 7 | ## Origins 8 | Vrok-it was born in May 2015, when three software engineers from Autodesk entered a team in the VR Hackathon in San Francisco. The goal of the "VR Party" team was to enable a guide to assist someone immersed in virtual reality, essentially curating the VR experience. The guide would perform actions that the VR participant might otherwise find cumbersome, such as exploding a 3D model, zooming in on it, or cutting it with a section plane. Making VR more personal and social makes it an ideal tool for communicating design information, for architectural fly-throughs, site walk-throughs and model review. 9 | 10 | The team soon realized it would be easy to allow the guide to curate the VR experience for multiple participants, much in the same way as proposed by Google Expeditions, announced just the week after at Google I/O 2015. 11 | 12 | The project was well-received and was awarded the Hackathon prize for the "Best Web-based VR Project". 13 | 14 | Over the following weeks the team implemented additional features, such as making it possible for people to create and manage simultaneous sessions. When launched "VR Party" became Vrok-it. 15 | 16 | ## How to use Vrok-It? 17 | On your "master" system - typically a PC or tablet - head on over to http://vrok.it (or http://www.vrok.it/v2 for the experimental, single-viewer version). 18 | Click the QR code – or scan it from your phone – to launch a stereoscopic 3D viewer (typically on a secondary device, such as a Google Cardboard-ready smartphone) connected to this session. The 3D models you load via the master page – and operations you perform on them – will be visible to all connected viewers. You can upload your own models, but for best results keep these under 2MB in size. 19 | 20 | ## Technology 21 | At its core, the project makes use of Autodesk's Forge platform, a web-based infrastructure that can be used for viewing 3D models, and leverages work previously published on Kean's blog. It has a Node.js back-end, with Socket.io handling the communication between presenter and participants. The site is hosted on Heroku and the complete source code for Vrok-it is available in this repository on GitHub. 22 | 23 | ## Supporting links 24 | 25 | Here are some blog posts tracking the evolution of Vrok-It: 26 | 27 | * [Cooling down after the SF VR Hackathon] (http://through-the-interface.typepad.com/through_the_interface/2015/05/cooling-down-after-the-sf-vr-hackathon.html) 28 | * [Our collaborative VR demo from the recent Hackathon] (http://through-the-interface.typepad.com/through_the_interface/2015/06/our-collaborative-vr-demo-from-the-recent-hackathon.html) 29 | * [A fun, collaborative VR tool: can you Vrok-it?] (http://through-the-interface.typepad.com/through_the_interface/2015/06/a-fun-collaborative-vr-tool-can-you-vrok-it.html) 30 | * [WebVR in Chrome and Forge] (http://through-the-interface.typepad.com/through_the_interface/2017/02/webvr-in-chrome-and-forge.html) 31 | 32 | The project built on work done previously related to using the Forge viewer for VR: 33 | 34 | * [Gearing up for the VR Hackathon] (http://through-the-interface.typepad.com/through_the_interface/2014/10/gearing-up-for-the-vr-hackathon.html) 35 | * [Creating a stereoscopic viewer for Google Cardboard using the Autodesk 360 viewer - Part 1] (http://through-the-interface.typepad.com/through_the_interface/2014/10/creating-a-stereoscopic-viewer-for-google-cardboard-using-the-autodesk-360-viewer-part-1.html) 36 | * [Creating a stereoscopic viewer for Google Cardboard using the Autodesk 360 viewer - Part 2] (http://through-the-interface.typepad.com/through_the_interface/2014/10/creating-a-stereoscopic-viewer-for-google-cardboard-using-the-autodesk-360-viewer-part-2.html) 37 | * [Creating a stereoscopic viewer for Google Cardboard using the Autodesk 360 viewer - Part 3] (http://through-the-interface.typepad.com/through_the_interface/2014/10/creating-a-stereoscopic-viewer-for-google-cardboard-using-the-autodesk-360-viewer-part-3.html) 38 | 39 | -------------------------------------------------------------------------------- /html/js/Autodesk.ADN.Viewing.Extension.VR.js: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////// 2 | // Autodesk.ADN.Viewing.Extension.VR 3 | // by Philippe Leefsma, May 2015 4 | // 5 | ///////////////////////////////////////////////////////////////////// 6 | AutodeskNamespace("Autodesk.ADN.Viewing.Extension"); 7 | 8 | Autodesk.ADN.Viewing.Extension.VR = function (viewer, options) { 9 | 10 | Autodesk.Viewing.Extension.call(this, viewer, options); 11 | 12 | var _panel = null; 13 | 14 | ///////////////////////////////////////////////////////////////// 15 | // Extension load callback 16 | // 17 | ///////////////////////////////////////////////////////////////// 18 | this.load = function () { 19 | 20 | _panel = new Panel( 21 | viewer.container, 22 | guid()); 23 | 24 | if(options.requestUserGesture) { 25 | 26 | _panel.setVisible(true); 27 | } 28 | else { 29 | 30 | activateVR(); 31 | } 32 | 33 | console.log('Autodesk.ADN.Viewing.Extension.VR loaded'); 34 | 35 | return true; 36 | } 37 | 38 | ///////////////////////////////////////////////////////////////// 39 | // Activate VR 40 | // 41 | ///////////////////////////////////////////////////////////////// 42 | function activateVR() { 43 | 44 | // viewer.setProgressiveRendering(false); 45 | 46 | //hide controls 47 | $('.adsk-control-group').each(function(){ 48 | 49 | $(this).find('>.adsk-button').each(function(){ 50 | 51 | $(this).css({ 52 | 'display':'none' 53 | }); 54 | }); 55 | }); 56 | 57 | $('.homeViewWrapper').css({ 'display':'none' }); 58 | 59 | viewer.loadExtension('Autodesk.Viewing.WebVR', { experimental: [ 'webVR_orbitModel' ] }); 60 | 61 | viewer.displayViewCube(false); 62 | 63 | setTimeout(function() { 64 | 65 | //viewer.setActiveNavigationTool('vr'); 66 | 67 | }, 3000); 68 | } 69 | 70 | ///////////////////////////////////////////////////////////////// 71 | // Extension unload callback 72 | // 73 | ///////////////////////////////////////////////////////////////// 74 | this.unload = function () { 75 | 76 | _panel.setVisible(false); 77 | 78 | console.log('Autodesk.ADN.Viewing.Extension.VR unloaded'); 79 | 80 | return true; 81 | } 82 | 83 | ///////////////////////////////////////////////////////////////// 84 | // Generates random guid to use as DOM id 85 | // 86 | ///////////////////////////////////////////////////////////////// 87 | function guid() { 88 | 89 | var d = new Date().getTime(); 90 | 91 | var guid = 'xxxx-xxxx-xxxx-xxxx'.replace( 92 | /[xy]/g, 93 | function (c) { 94 | var r = (d + Math.random() * 16) % 16 | 0; 95 | d = Math.floor(d / 16); 96 | return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16); 97 | }); 98 | 99 | return guid; 100 | } 101 | 102 | ///////////////////////////////////////////////////////////////// 103 | // The demo Panel 104 | // 105 | ///////////////////////////////////////////////////////////////// 106 | var Panel = function( 107 | parentContainer, id) { 108 | 109 | var _thisPanel = this; 110 | 111 | _thisPanel.content = document.createElement('div'); 112 | 113 | Autodesk.Viewing.UI.DockingPanel.call( 114 | this, 115 | parentContainer, 116 | id, 117 | 'Switch to VR Mode?', 118 | {shadow:true}); 119 | 120 | $(_thisPanel.container).addClass('vr-selector'); 121 | 122 | var w = 270; 123 | var h = 115; 124 | 125 | $(_thisPanel.container).css({ 126 | 127 | 'left' : 'calc(50vw - ' + w * 0.5 + 'px)', 128 | 'top' : 'calc(50vh - ' + h * 0.5 + 'px)', 129 | 'width' : w + 'px', 130 | 'height': h + 'px', 131 | 'resize': 'none' 132 | }); 133 | 134 | ///////////////////////////////////////////////////////////// 135 | // Custom html 136 | // 137 | ///////////////////////////////////////////////////////////// 138 | var html = [ 139 | 140 | '
', 141 | 142 | '', 147 | 148 | ' ', 149 | 150 | '', 155 | 156 | '
' 157 | ]; 158 | 159 | $(_thisPanel.container).append(html.join('\n')); 160 | 161 | $('#' + id + '-ok-btn').click(onOkButtonClicked); 162 | 163 | $('#' + id + '-cancel-btn').click(onCancelButtonClicked); 164 | 165 | ///////////////////////////////////////////////////////////// 166 | // button clicked handler 167 | // 168 | ///////////////////////////////////////////////////////////// 169 | function onOkButtonClicked(event) { 170 | 171 | event.preventDefault(); 172 | 173 | _thisPanel.setVisible(false); 174 | 175 | activateVR(); 176 | } 177 | 178 | function onCancelButtonClicked(event) { 179 | 180 | event.preventDefault(); 181 | 182 | _thisPanel.setVisible(false); 183 | } 184 | 185 | ///////////////////////////////////////////////////////////// 186 | // setVisible override (not used in that sample) 187 | // 188 | ///////////////////////////////////////////////////////////// 189 | _thisPanel.setVisible = function(show) { 190 | 191 | Autodesk.Viewing.UI.DockingPanel.prototype. 192 | setVisible.call(this, show); 193 | } 194 | 195 | ///////////////////////////////////////////////////////////// 196 | // initialize override 197 | // 198 | ///////////////////////////////////////////////////////////// 199 | _thisPanel.initialize = function() { 200 | 201 | this.title = this.createTitleBar( 202 | this.titleLabel || 203 | this.container.id); 204 | 205 | } 206 | } 207 | 208 | ///////////////////////////////////////////////////////////// 209 | // Set up JS inheritance 210 | // 211 | ///////////////////////////////////////////////////////////// 212 | Panel.prototype = Object.create( 213 | Autodesk.Viewing.UI.DockingPanel.prototype); 214 | 215 | Panel.prototype.constructor = Panel; 216 | 217 | ///////////////////////////////////////////////////////////// 218 | // Add needed CSS 219 | // 220 | ///////////////////////////////////////////////////////////// 221 | var css = [ 222 | 223 | 'form.vr-selector-controls{', 224 | 'margin: 20px;', 225 | '}', 226 | 227 | 'input.vr-selector-name {', 228 | 'height: 30px;', 229 | 'margin-left: 5px;', 230 | 'margin-bottom: 5px;', 231 | 'margin-top: 5px;', 232 | 'border-radius:5px;', 233 | '}', 234 | 235 | 'div.vr-selector {', 236 | 'top: 0px;', 237 | 'left: 0px;', 238 | 'width: 270px;', 239 | 'height: 115px;', 240 | 'resize: none;', 241 | '}', 242 | 243 | 'div.vr-selector-minimized {', 244 | 'height: 34px;', 245 | 'min-height: 34px', 246 | '}', 247 | 248 | 'button.btn-vr-selector {', 249 | 'width: 80px', 250 | '}' 251 | 252 | ].join('\n'); 253 | 254 | $('').appendTo('head'); 255 | }; 256 | 257 | Autodesk.ADN.Viewing.Extension.VR.prototype = 258 | Object.create(Autodesk.Viewing.Extension.prototype); 259 | 260 | Autodesk.ADN.Viewing.Extension.VR.prototype.constructor = 261 | Autodesk.ADN.Viewing.Extension.VR; 262 | 263 | Autodesk.Viewing.theExtensionManager.registerExtension( 264 | 'Autodesk.ADN.Viewing.Extension.VR', 265 | Autodesk.ADN.Viewing.Extension.VR); -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var api = require('./api'); 3 | var http = require('http'); 4 | var crypto = require('crypto'); 5 | 6 | // CONFIG 7 | var port = process.env.PORT || 5000; 8 | 9 | // WEB SERVER 10 | var app = express(); 11 | 12 | app.use(function (req, res, next) { 13 | res.header('Access-Control-Allow-Origin', '*'); 14 | res.header('Access-Control-Allow-Headers', 'X-Requested-With'); 15 | next(); 16 | }); 17 | app.use('/', express.static(__dirname + '/html')); 18 | app.use('/v2', express.static(__dirname + '/html/index2.html')); 19 | 20 | app.get('/api/token', api.getToken); 21 | 22 | app.get('/api/uploadtoken', api.getUploadToken); 23 | 24 | app.get('/api/sessionId', function(req, res) { 25 | var sessionId; 26 | do { 27 | sessionId = randomValueBase64(6); 28 | console.log("generated session id: " + sessionId); 29 | } 30 | while (sessionIds.indexOf(sessionId) > -1); 31 | res.json(sessionId); 32 | }); 33 | 34 | app.get('/join', function(req, res) { 35 | var id = req.query.id; 36 | res.redirect('/participant.html?session=' + id); 37 | }); 38 | 39 | // Currently only return the URN - could also return 40 | // the various explode, zoom factors, etc. 41 | app.get('/api/getSession/:id', function(req, res) { 42 | var sessionId = req.params.id; 43 | var idx = sessionIds.indexOf(sessionId); 44 | res.json(idx < 0 ? "" : models[idx]); 45 | }); 46 | 47 | var server = http.createServer(app); 48 | server.listen(port); 49 | console.log('Listening on port ' + port + '...'); 50 | 51 | var sessionIds = []; 52 | var models = []; 53 | var zoomFactors = []; 54 | var explodeFactors = []; 55 | var isolateIds = []; 56 | var hideIds = []; 57 | var showIds = []; 58 | var sectionPlanes = []; 59 | 60 | var defZoom = null; 61 | var defExplode = 0; 62 | var defIsolate = []; 63 | var defHide = []; 64 | var defShow = []; 65 | var defSection = []; 66 | 67 | // WEB SOCKETS 68 | var io = require('socket.io')(server); 69 | io.on('connection', function(socket) { 70 | console.log('a user connected (id=' + socket.id +')'); 71 | 72 | socket.on('create-session', function(session) { 73 | console.log('session created (id=' + session.id +')'); 74 | // Add our session info to the beginning of our various arrays 75 | sessionIds.unshift(session.id); 76 | models.unshift(null); 77 | zoomFactors.unshift(defZoom); 78 | explodeFactors.unshift(defExplode); 79 | isolateIds.unshift(defIsolate); 80 | hideIds.unshift(defHide); 81 | showIds.unshift(defShow); 82 | sectionPlanes.unshift(defSection); 83 | }); 84 | 85 | socket.on('join-session', function(session) { 86 | console.log('user joined session (id=' + session.id +')'); 87 | var idx = sessionIds.indexOf(session.id); 88 | if (idx > -1) { 89 | 90 | // Add our user to the room for this session 91 | socket.join(session.id); 92 | 93 | if (models[idx]) { 94 | // Bring this user up to speed with the state of the session 95 | emitDirectAndLog(socket, { name: 'load', value: models[idx] }); 96 | if (zoomFactors[idx] !== defZoom) { 97 | emitDirectAndLog(socket, { name: 'zoom', value: zoomFactors[idx] }); 98 | } 99 | if (explodeFactors[idx] > defExplode) { 100 | emitDirectAndLog(socket, { name: 'explode', value: explodeFactors[idx] }); 101 | } 102 | if (isolateIds[idx] !== defIsolate) { 103 | emitDirectAndLog(socket, { name: 'isolate', value: isolateIds[idx] }); 104 | } 105 | if (hideIds[idx] !== defHide) { 106 | emitDirectAndLog(socket, { name: 'hide', value: hideIds[idx] }); 107 | } 108 | if (showIds[idx] !== defShow) { 109 | emitDirectAndLog(socket, { name: 'show', value: showIds[idx] }); 110 | } 111 | if (sectionPlanes[idx] !== defSection) { 112 | emitDirectAndLog(socket, { name: 'section', value: sectionPlanes[idx] }); 113 | } 114 | } 115 | } 116 | else { 117 | console.log('could not find session (id=' + session.id +')'); 118 | emitDirectAndLog(socket, { name: 'load' }); 119 | } 120 | }); 121 | 122 | socket.on('close-session', function(session) { 123 | var idx = sessionIds.indexOf(session.id); 124 | if (idx > -1) { 125 | // Clear the model for participants 126 | emitToGroupAndLog({ session: session.id, name: 'load', value: '' }); 127 | 128 | // Clean up state 129 | sessionIds.splice(idx, 1); 130 | models.splice(idx, 1); 131 | zoomFactors.splice(idx, 1); 132 | explodeFactors.splice(idx, 1); 133 | isolateIds.splice(idx, 1); 134 | hideIds.splice(idx, 1); 135 | showIds.splice(idx, 1); 136 | sectionPlanes.splice(idx, 1); 137 | 138 | console.log('session closed (id=' + session.id +')'); 139 | } 140 | }); 141 | 142 | socket.on('disconnect', function() { 143 | console.log('a user disconnected (id=' + socket.id +')'); 144 | }); 145 | 146 | socket.on('lmv-command', function(command) { 147 | var idx = sessionIds.indexOf(command.session); 148 | if (idx > -1) { 149 | if (command.name === 'load') { 150 | // Create our default settings for the model 151 | models[idx] = command.value; 152 | zoomFactors[idx] = defZoom; 153 | explodeFactors[idx] = defExplode; 154 | isolateIds[idx] = defIsolate; 155 | hideIds[idx] = defHide; 156 | showIds[idx] = defShow; 157 | sectionPlanes[idx] = defSection; 158 | 159 | // Emit the load command 160 | emitToGroupAndLog(command); 161 | 162 | // Emit the defaults to the group participants (no need for hide/show) 163 | emitToGroupAndLog({ session: command.session, name: 'zoom', value: defZoom }); 164 | emitToGroupAndLog({ session: command.session, name: 'explode', value: defExplode }); 165 | emitToGroupAndLog({ session: command.session, name: 'isolate', value: defIsolate }); 166 | emitToGroupAndLog({ session: command.session, name: 'section', value: defSection }); 167 | } 168 | else { 169 | if (command.name === 'zoom') { 170 | zoomFactors[idx] = command.value; 171 | } 172 | else if (command.name === 'explode') { 173 | explodeFactors[idx] = command.value; 174 | } 175 | else if (command.name === 'isolate') { 176 | isolateIds[idx] = command.value; 177 | if (command.value == defIsolate) { 178 | hideIds[idx] = defHide; 179 | showIds[idx] = defShow; 180 | } 181 | } 182 | else if (command.name === 'hide') { 183 | hideIds[idx] = hideIds[idx].concat(command.value); 184 | showIds[idx] = stripIds(showIds[idx], command.value); 185 | } 186 | else if (command.name === 'show') { 187 | showIds[idx] = showIds[idx].concat(command.value); 188 | hideIds[idx] = stripIds(hideIds[idx], command.value); 189 | } 190 | else if (command.name === 'section') { 191 | sectionPlanes[idx] = command.value; 192 | } 193 | emitToGroupAndLog(command); 194 | } 195 | } 196 | else { 197 | console.log('could not find session (id=' + command.session +')'); 198 | } 199 | }); 200 | }); 201 | 202 | function stripIds(existing, ids) { 203 | for (var i = 0; i < ids.length; i++) { 204 | var idx = existing.indexOf(ids[i]); 205 | if (idx > -1) { 206 | existing.splice(idx, 1); 207 | } 208 | } 209 | return existing; 210 | } 211 | 212 | function emitDirectAndLog(socket, command) { 213 | socket.emit('lmv-command', command); 214 | console.log(command); 215 | } 216 | 217 | function emitToGroupAndLog(command) { 218 | io.to(command.session).emit('lmv-command', command); 219 | console.log(command); 220 | } 221 | 222 | function randomValueBase64 (len) { 223 | return crypto.randomBytes(Math.ceil(len * 3 / 4)) 224 | .toString('base64') // convert to base64 format 225 | .slice(0, len) // return required number of characters 226 | .replace(/\+/g, '0') // replace '+' with '0' 227 | .replace(/\//g, '0'); // replace '/' with '0' 228 | } -------------------------------------------------------------------------------- /sockets.js: -------------------------------------------------------------------------------- 1 | var socketio = require('socket.io').listen(5001); 2 | var express = require('express'); 3 | var request = require('request'); 4 | var path = require('path'); 5 | 6 | var router = express.Router(); 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // 10 | // 11 | /////////////////////////////////////////////////////////////////////////////// 12 | 13 | router.get('/', function(req, res) { 14 | 15 | var rootPath = path.join( 16 | __dirname, 17 | '.'); 18 | 19 | res.sendFile('index.html', { root: rootPath }); 20 | }); 21 | 22 | /////////////////////////////////////////////////////////////////////////////// 23 | // 24 | // 25 | /////////////////////////////////////////////////////////////////////////////// 26 | router.initializeSocket = function(serverApp) { 27 | 28 | var tracker = {}; 29 | 30 | var showcaseData = null; 31 | 32 | /////////////////////////////////////////////////////////////////////// 33 | // 34 | // 35 | /////////////////////////////////////////////////////////////////////// 36 | function initShowcaseData() { 37 | 38 | showcaseData = { 39 | controllingUser: null, 40 | urn: '', 41 | view: null, 42 | isolateIds: null, 43 | explodeScale: 0.0 44 | }; 45 | } 46 | 47 | /////////////////////////////////////////////////////////////////////// 48 | // 49 | // 50 | /////////////////////////////////////////////////////////////////////// 51 | function removeUser(socketId, user) { 52 | 53 | tracker[socketId].user = null; 54 | 55 | emitAll('removeUser', user); 56 | 57 | var msg = { 58 | user: user, 59 | text: '> ' + '' + user.name + '' + 60 | ' left the showcase

' 61 | } 62 | 63 | var activeUsers = 0; 64 | 65 | for (var key in tracker) { 66 | 67 | if(tracker[key].user) 68 | ++activeUsers; 69 | 70 | tracker[key].socket.emit('chatMessage', msg); 71 | } 72 | 73 | // clears showcase data if no more active users 74 | if(activeUsers == 0) { 75 | 76 | initShowcaseData(); 77 | } 78 | } 79 | 80 | /////////////////////////////////////////////////////////////////////// 81 | // 82 | // 83 | /////////////////////////////////////////////////////////////////////// 84 | function emitAll(signalId, data) { 85 | 86 | for (var key in tracker) { 87 | 88 | tracker[key].socket.emit(signalId, data); 89 | } 90 | } 91 | 92 | /////////////////////////////////////////////////////////////////////// 93 | // 94 | // 95 | /////////////////////////////////////////////////////////////////////// 96 | function emitExclude(socketId, signalId, data) { 97 | 98 | for (var key in tracker) { 99 | 100 | if(key !== socketId) { 101 | 102 | tracker[key].socket.emit(signalId, data); 103 | } 104 | } 105 | } 106 | 107 | /////////////////////////////////////////////////////////////////////////// 108 | // 109 | // 110 | /////////////////////////////////////////////////////////////////////////// 111 | 112 | initShowcaseData(); 113 | 114 | var io = socketio.listen(serverApp, { log: false }); 115 | 116 | io.sockets.on('connection', function (socket) { 117 | 118 | console.log('Incoming socket connection: ' + socket.id); 119 | 120 | tracker[socket.id] = { 121 | socket: socket, 122 | user: null 123 | }; 124 | 125 | /////////////////////////////////////////////////////////////////////// 126 | // 127 | // 128 | /////////////////////////////////////////////////////////////////////// 129 | function buildInitData() { 130 | 131 | var users = []; 132 | 133 | for (var key in tracker) { 134 | 135 | if(tracker[key].user) 136 | users.push(tracker[key].user); 137 | } 138 | 139 | var initData = { 140 | users: users, 141 | socketId: socket.id, 142 | showcaseData: showcaseData 143 | }; 144 | 145 | return initData; 146 | } 147 | 148 | /////////////////////////////////////////////////////////////////////// 149 | // 150 | // 151 | /////////////////////////////////////////////////////////////////////// 152 | socket.on('requestData', function () { 153 | 154 | socket.emit('showcaseData', buildInitData()); 155 | }); 156 | 157 | /////////////////////////////////////////////////////////////////////// 158 | // 159 | // 160 | /////////////////////////////////////////////////////////////////////// 161 | socket.on('requestControl', function (user) { 162 | 163 | // grants control with no further check 164 | 165 | if(user.hasControl) { 166 | 167 | if(showcaseData.controllingUser) { 168 | 169 | //current controlling user looses control 170 | showcaseData.controllingUser.hasControl = false; 171 | 172 | emitAll('controlEvent', showcaseData.controllingUser); 173 | } 174 | 175 | showcaseData.controllingUser = user; 176 | 177 | var msg = { 178 | text: '> ' + '' + user.name + '' + 179 | ' has taken control' + '

' 180 | }; 181 | 182 | emitAll('chatMessage', msg); 183 | } 184 | else { 185 | if(showcaseData.controllingUser) { 186 | if(showcaseData.controllingUser.socketId === socket.id) { 187 | showcaseData.controllingUser = null; 188 | } 189 | } 190 | } 191 | 192 | tracker[socket.id].user = user; 193 | 194 | emitAll('controlEvent', user); 195 | }); 196 | 197 | /////////////////////////////////////////////////////////////////////// 198 | // 199 | // 200 | /////////////////////////////////////////////////////////////////////// 201 | socket.on('cameraChanged', function (data) { 202 | 203 | showcaseData.view = data.view; 204 | 205 | emitExclude(socket.id, 'cameraChanged', data); 206 | }); 207 | 208 | /////////////////////////////////////////////////////////////////////// 209 | // 210 | // 211 | /////////////////////////////////////////////////////////////////////// 212 | socket.on('explode', function (data) { 213 | 214 | showcaseData.explodeScale = data.explodeScale; 215 | 216 | emitExclude(socket.id, 'explode', data); 217 | }); 218 | 219 | /////////////////////////////////////////////////////////////////////// 220 | // 221 | // 222 | /////////////////////////////////////////////////////////////////////// 223 | socket.on('isolate', function (data) { 224 | 225 | showcaseData.isolateIds = data.isolateIds; 226 | 227 | emitExclude(socket.id, 'isolate', data); 228 | }); 229 | 230 | /////////////////////////////////////////////////////////////////////// 231 | // 232 | // 233 | /////////////////////////////////////////////////////////////////////// 234 | socket.on('sendMessage', function (msg) { 235 | 236 | msg.text = '> ' + '' + msg.user.name + '' + ' says:
' + 237 | msg.text + '

'; 238 | 239 | emitAll('chatMessage', msg); 240 | }); 241 | 242 | /////////////////////////////////////////////////////////////////////// 243 | // 244 | // 245 | /////////////////////////////////////////////////////////////////////// 246 | socket.on('addUser', function (user) { 247 | 248 | user.socketId = socket.id; 249 | 250 | tracker[socket.id].user = user; 251 | 252 | emitAll('addUser', user); 253 | 254 | var msg = { 255 | user: user, 256 | text: '> ' + '' + user.name + '' + 257 | ' joined the showcase

' 258 | } 259 | 260 | emitAll('chatMessage', msg); 261 | }); 262 | 263 | /////////////////////////////////////////////////////////////////////// 264 | // 265 | // 266 | /////////////////////////////////////////////////////////////////////// 267 | socket.on('removeUser', function (user) { 268 | 269 | removeUser(socket.id, user); 270 | }); 271 | 272 | socket.on('disconnect', function () { 273 | 274 | //console.log('Socket disconnection: ' + socket.id); 275 | 276 | if(tracker[socket.id].user) { 277 | removeUser(socket.id, tracker[socket.id].user); 278 | } 279 | 280 | delete tracker[socket.id]; 281 | }); 282 | 283 | /////////////////////////////////////////////////////////////////////// 284 | // 285 | // 286 | /////////////////////////////////////////////////////////////////////// 287 | socket.on('loadDocument', function (urn) { 288 | 289 | showcaseData.urn = urn; 290 | showcaseData.view = null; 291 | showcaseData.isolateIds = null; 292 | showcaseData.explodeScale = 0.0; 293 | 294 | emitExclude(socket.id, 'loadDocument', urn); 295 | 296 | var msg = { 297 | text: '> Loading document...

' 298 | } 299 | 300 | emitAll('chatMessage', msg); 301 | }); 302 | 303 | /////////////////////////////////////////////////////////////////////// 304 | // 305 | // 306 | /////////////////////////////////////////////////////////////////////// 307 | socket.on('closeDocument', function () { 308 | 309 | showcaseData.urn = ''; 310 | showcaseData.view = null; 311 | showcaseData.isolateIds = null; 312 | showcaseData.explodeScale = 0.0; 313 | 314 | emitAll('closeDocument'); 315 | }); 316 | }); 317 | } 318 | 319 | module.exports = router; -------------------------------------------------------------------------------- /html/js/jquery.qrcode.min.js: -------------------------------------------------------------------------------- 1 | (function(r){r.fn.qrcode=function(h){var s;function u(a){this.mode=s;this.data=a}function o(a,c){this.typeNumber=a;this.errorCorrectLevel=c;this.modules=null;this.moduleCount=0;this.dataCache=null;this.dataList=[]}function q(a,c){if(void 0==a.length)throw Error(a.length+"/"+c);for(var d=0;da||this.moduleCount<=a||0>c||this.moduleCount<=c)throw Error(a+","+c);return this.modules[a][c]},getModuleCount:function(){return this.moduleCount},make:function(){if(1>this.typeNumber){for(var a=1,a=1;40>a;a++){for(var c=p.getRSBlocks(a,this.errorCorrectLevel),d=new t,b=0,e=0;e=d;d++)if(!(-1>=a+d||this.moduleCount<=a+d))for(var b=-1;7>=b;b++)-1>=c+b||this.moduleCount<=c+b||(this.modules[a+d][c+b]= 5 | 0<=d&&6>=d&&(0==b||6==b)||0<=b&&6>=b&&(0==d||6==d)||2<=d&&4>=d&&2<=b&&4>=b?!0:!1)},getBestMaskPattern:function(){for(var a=0,c=0,d=0;8>d;d++){this.makeImpl(!0,d);var b=j.getLostPoint(this);if(0==d||a>b)a=b,c=d}return c},createMovieClip:function(a,c,d){a=a.createEmptyMovieClip(c,d);this.make();for(c=0;c=f;f++)for(var i=-2;2>=i;i++)this.modules[b+f][e+i]=-2==f||2==f||-2==i||2==i||0==f&&0==i?!0:!1}},setupTypeNumber:function(a){for(var c= 7 | j.getBCHTypeNumber(this.typeNumber),d=0;18>d;d++){var b=!a&&1==(c>>d&1);this.modules[Math.floor(d/3)][d%3+this.moduleCount-8-3]=b}for(d=0;18>d;d++)b=!a&&1==(c>>d&1),this.modules[d%3+this.moduleCount-8-3][Math.floor(d/3)]=b},setupTypeInfo:function(a,c){for(var d=j.getBCHTypeInfo(this.errorCorrectLevel<<3|c),b=0;15>b;b++){var e=!a&&1==(d>>b&1);6>b?this.modules[b][8]=e:8>b?this.modules[b+1][8]=e:this.modules[this.moduleCount-15+b][8]=e}for(b=0;15>b;b++)e=!a&&1==(d>>b&1),8>b?this.modules[8][this.moduleCount- 8 | b-1]=e:9>b?this.modules[8][15-b-1+1]=e:this.modules[8][15-b-1]=e;this.modules[this.moduleCount-8][8]=!a},mapData:function(a,c){for(var d=-1,b=this.moduleCount-1,e=7,f=0,i=this.moduleCount-1;0g;g++)if(null==this.modules[b][i-g]){var n=!1;f>>e&1));j.getMask(c,b,i-g)&&(n=!n);this.modules[b][i-g]=n;e--; -1==e&&(f++,e=7)}b+=d;if(0>b||this.moduleCount<=b){b-=d;d=-d;break}}}};o.PAD0=236;o.PAD1=17;o.createData=function(a,c,d){for(var c=p.getRSBlocks(a, 9 | c),b=new t,e=0;e8*a)throw Error("code length overflow. ("+b.getLengthInBits()+">"+8*a+")");for(b.getLengthInBits()+4<=8*a&&b.put(0,4);0!=b.getLengthInBits()%8;)b.putBit(!1);for(;!(b.getLengthInBits()>=8*a);){b.put(o.PAD0,8);if(b.getLengthInBits()>=8*a)break;b.put(o.PAD1,8)}return o.createBytes(b,c)};o.createBytes=function(a,c){for(var d= 10 | 0,b=0,e=0,f=Array(c.length),i=Array(c.length),g=0;g>>=1;return c},getPatternPosition:function(a){return j.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,c,d){switch(a){case 0:return 0==(c+d)%2;case 1:return 0==c%2;case 2:return 0==d%3;case 3:return 0==(c+d)%3;case 4:return 0==(Math.floor(c/2)+Math.floor(d/3))%2;case 5:return 0==c*d%2+c*d%3;case 6:return 0==(c*d%2+c*d%3)%2;case 7:return 0==(c*d%3+(c+d)%2)%2;default:throw Error("bad maskPattern:"+ 14 | a);}},getErrorCorrectPolynomial:function(a){for(var c=new q([1],0),d=0;dc)switch(a){case 1:return 10;case 2:return 9;case s:return 8;case 8:return 8;default:throw Error("mode:"+a);}else if(27>c)switch(a){case 1:return 12;case 2:return 11;case s:return 16;case 8:return 10;default:throw Error("mode:"+a);}else if(41>c)switch(a){case 1:return 14;case 2:return 13;case s:return 16;case 8:return 12;default:throw Error("mode:"+ 15 | a);}else throw Error("type:"+c);},getLostPoint:function(a){for(var c=a.getModuleCount(),d=0,b=0;b=g;g++)if(!(0>b+g||c<=b+g))for(var h=-1;1>=h;h++)0>e+h||c<=e+h||0==g&&0==h||i==a.isDark(b+g,e+h)&&f++;5a)throw Error("glog("+a+")");return l.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;256<=a;)a-=255;return l.EXP_TABLE[a]},EXP_TABLE:Array(256), 17 | LOG_TABLE:Array(256)},m=0;8>m;m++)l.EXP_TABLE[m]=1<m;m++)l.EXP_TABLE[m]=l.EXP_TABLE[m-4]^l.EXP_TABLE[m-5]^l.EXP_TABLE[m-6]^l.EXP_TABLE[m-8];for(m=0;255>m;m++)l.LOG_TABLE[l.EXP_TABLE[m]]=m;q.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var c=Array(this.getLength()+a.getLength()-1),d=0;d 18 | this.getLength()-a.getLength())return this;for(var c=l.glog(this.get(0))-l.glog(a.get(0)),d=Array(this.getLength()),b=0;b>>7-a%8&1)},put:function(a,c){for(var d=0;d>>c-d-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var c=Math.floor(this.length/8);this.buffer.length<=c&&this.buffer.push(0);a&&(this.buffer[c]|=128>>>this.length%8);this.length++}};"string"===typeof h&&(h={text:h});h=r.extend({},{render:"canvas",width:256,height:256,typeNumber:-1, 26 | correctLevel:2,background:"#ffffff",foreground:"#000000"},h);return this.each(function(){var a;if("canvas"==h.render){a=new o(h.typeNumber,h.correctLevel);a.addData(h.text);a.make();var c=document.createElement("canvas");c.width=h.width;c.height=h.height;for(var d=c.getContext("2d"),b=h.width/a.getModuleCount(),e=h.height/a.getModuleCount(),f=0;f").css("width",h.width+"px").css("height",h.height+"px").css("border","0px").css("border-collapse","collapse").css("background-color",h.background);d=h.width/a.getModuleCount();b=h.height/a.getModuleCount();for(e=0;e").css("height",b+"px").appendTo(c);for(i=0;i").css("width", 28 | d+"px").css("background-color",a.isDark(e,i)?h.foreground:h.background).appendTo(f)}}a=c;jQuery(a).appendTo(this)})}})(jQuery); 29 | -------------------------------------------------------------------------------- /html/js/Autodesk.ADN.Toolkit.ViewData.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Copyright (c) Autodesk, Inc. All rights reserved 3 | // Written by Philippe Leefsma 2014 - ADN/Developer Technical Services 4 | // 5 | // Permission to use, copy, modify, and distribute this software in 6 | // object code form for any purpose and without fee is hereby granted, 7 | // provided that the above copyright notice appears in all copies and 8 | // that both that copyright notice and the limited warranty and 9 | // restricted rights notice below appear in all supporting 10 | // documentation. 11 | // 12 | // AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS. 13 | // AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF 14 | // MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. AUTODESK, INC. 15 | // DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE 16 | // UNINTERRUPTED OR ERROR FREE. 17 | /////////////////////////////////////////////////////////////////////////////// 18 | 19 | /////////////////////////////////////////////////////////////////////////////// 20 | // Namespace declaration 21 | // 22 | /////////////////////////////////////////////////////////////////////////////// 23 | var Autodesk = Autodesk || {}; 24 | Autodesk.ADN = Autodesk.ADN || {}; 25 | Autodesk.ADN.Toolkit = Autodesk.ADN.Toolkit || {}; 26 | Autodesk.ADN.Toolkit.ViewData = Autodesk.ADN.Toolkit.ViewData || {}; 27 | 28 | /////////////////////////////////////////////////////////////////////////////// 29 | // Autodesk.ADN.Toolkit.ViewData.AdnViewDataClient 30 | // 31 | // Parameters: 32 | // baseUrl: url of view and data API service, 33 | // for production environment, it is 'https://developer.api.autodesk.com' 34 | // accessTokenOrUrl : An url which returns the access token in JSON foramt, 35 | // for example: http://still-spire-1606.herokuapp.com/api/rawtoken, 36 | // it returns token like : 37 | // {"token_type":"Bearer", 38 | // "expires_in":1799, 39 | // "access_token":"nTeOdsiNRckNbiBF7lzdEZ3yjHRx"} 40 | /////////////////////////////////////////////////////////////////////////////// 41 | Autodesk.ADN.Toolkit.ViewData.AdnViewDataClient = function ( 42 | baseUrl, 43 | accessTokenResponse) { 44 | 45 | /////////////////////////////////////////////////////////////////////////// 46 | // Private Members 47 | // 48 | /////////////////////////////////////////////////////////////////////////// 49 | var _accessTokenResponse = accessTokenResponse; 50 | 51 | var _baseUrl = baseUrl; 52 | 53 | 54 | /////////////////////////////////////////////////////////////////////////// 55 | // Set the cookie upon server response 56 | // Subsequent http requests to this domain 57 | // will automatically send the cookie for authentication 58 | // 59 | /////////////////////////////////////////////////////////////////////////// 60 | this.setToken = function () { 61 | 62 | var xhr = new XMLHttpRequest(); 63 | 64 | xhr.open('POST', 65 | _baseUrl + '/utility/v1/settoken', 66 | false); 67 | 68 | xhr.setRequestHeader( 69 | 'Content-Type', 70 | 'application/x-www-form-urlencoded'); 71 | 72 | xhr.withCredentials = true; 73 | xhr.send("access-token=" + _accessTokenResponse.access_token); 74 | }; 75 | 76 | /////////////////////////////////////////////////////////////////////////// 77 | // Use: 78 | // Create bucket 79 | // 80 | // bucketCreationData = { 81 | // bucketKey : "bucketKey", 82 | // servicesAllowed: {}, 83 | // policy: "temporary/transient/persistent 84 | // } 85 | // 86 | // API: 87 | // POST /oss/{apiversion}/buckets 88 | // 89 | // Response: 90 | // 91 | // "{ 92 | // "key":"bucketKey", 93 | // "owner":"tAp1fqjjtcgqS4CKpCYDjAyNbKW4IVCC", 94 | // "createDate":1404984496468, 95 | // "permissions":[{ 96 | // "serviceId":"tAp1fqjjtcgqS4CKpCYDjAyNbKW4IVCC", 97 | // "access":"full"}], 98 | // "policyKey":"persistent" 99 | // }" 100 | /////////////////////////////////////////////////////////////////////////// 101 | this.createBucketAsync = function ( 102 | bucketCreationData, 103 | onSuccess, 104 | onError) { 105 | 106 | var xhr = new XMLHttpRequest(); 107 | 108 | xhr.open('POST', 109 | _baseUrl + "/oss/v1/buckets", 110 | true); 111 | 112 | xhr.setRequestHeader( 113 | 'Authorization', 114 | 'Bearer ' + _accessTokenResponse.access_token); 115 | 116 | xhr.setRequestHeader( 117 | 'Content-Type', 118 | 'application/json'); 119 | 120 | xhr.onreadystatechange = function () { 121 | if (xhr.readyState == 4) { 122 | if(xhr.status == 200) { 123 | onSuccess(JSON.parse(xhr.responseText)); 124 | } 125 | else { 126 | onError(JSON.parse(xhr.responseText)); 127 | } 128 | } 129 | } 130 | 131 | try { 132 | xhr.send(JSON.stringify(bucketCreationData)); 133 | } 134 | catch (ex) { 135 | onError(ex); 136 | } 137 | }; 138 | 139 | /////////////////////////////////////////////////////////////////////////// 140 | // Use: 141 | // Get bucket details 142 | // 143 | // API: 144 | // GET /oss/{apiversion}/buckets/{bucketkey}/details 145 | // 146 | // Response: 147 | // 148 | // "{ 149 | // "key":"bucketKey", 150 | // "owner":"tAp1fqjjtcgqS4CKpCYDjAyNbKW4IVCC", 151 | // "createDate":1404984496468, 152 | // "permissions":[{ 153 | // "serviceId":"tAp1fqjjtcgqS4CKpCYDjAyNbKW4IVCC", 154 | // "access":"full"}], 155 | // "policyKey":"persistent" 156 | // }" 157 | /////////////////////////////////////////////////////////////////////////// 158 | this.getBucketDetailsAsync = function ( 159 | bucketKey, 160 | onSuccess, 161 | onError) { 162 | 163 | var xhr = new XMLHttpRequest(); 164 | 165 | xhr.open('GET', 166 | _baseUrl + "/oss/v1/buckets/" + bucketKey + "/details", 167 | true); 168 | 169 | xhr.setRequestHeader( 170 | 'Authorization', 171 | 'Bearer ' + _accessTokenResponse.access_token); 172 | 173 | xhr.setRequestHeader( 174 | 'Content-Type', 175 | 'application/json'); 176 | 177 | xhr.onreadystatechange = function () { 178 | if (xhr.readyState == 4) { 179 | if (xhr.status == 200) { 180 | onSuccess(JSON.parse(xhr.responseText)); 181 | } 182 | else { 183 | onError(JSON.parse(xhr.responseText)); 184 | } 185 | } 186 | } 187 | 188 | try 189 | { 190 | xhr.send(); 191 | } 192 | catch (ex) 193 | { 194 | onError(ex); 195 | } 196 | }; 197 | 198 | /////////////////////////////////////////////////////////////////////////// 199 | // Use: 200 | // Upload a file to bucket 201 | // 202 | // API: 203 | // PUT /oss/{apiversion}/buckets/{bucketkey}/objects/{objectkey} 204 | // 205 | // Response: 206 | // 207 | // "{ "bucket-key" : "adn-10.07.2014-11.28.15", 208 | // "file": file, 209 | // "objects" : [ { 210 | // "location" : "baseUrl/oss/v1/buckets/bucketKey/objects/file.name", 211 | // "size" : 1493911, 212 | // "key" : "file.name", 213 | // "id" : "urn:adsk.objects:os.object:bucketKey/file.name", 214 | // "sha-1" : "ba824b22a6df9d0fc30943ffcf8129e2b9de80f6", 215 | // "content-type" : "application/stream" } ] 216 | // }" 217 | /////////////////////////////////////////////////////////////////////////// 218 | this.uploadFileAsync = function ( 219 | file, 220 | bucketKey, 221 | objectKey, 222 | onSuccess, 223 | onError) { 224 | 225 | var xhr = new XMLHttpRequest(); 226 | 227 | xhr.open('PUT', 228 | _baseUrl + '/oss/v1/buckets/' + bucketKey + '/objects/' + objectKey, 229 | true); 230 | 231 | xhr.setRequestHeader( 232 | 'Authorization', 233 | 'Bearer ' + _accessTokenResponse.access_token); 234 | 235 | xhr.setRequestHeader( 236 | 'Content-Type', 237 | 'application/stream'); 238 | 239 | xhr.onload = function () { 240 | var response = JSON.parse(xhr.responseText); 241 | response.file = file; 242 | 243 | onSuccess(response); 244 | }; 245 | 246 | var reader = new FileReader(); 247 | 248 | reader.onerror = onError; 249 | reader.onabort = onError; 250 | 251 | reader.onloadend = function (event) { 252 | if (event.target.readyState == FileReader.DONE) { 253 | try { 254 | xhr.send(event.target.result); 255 | } 256 | catch (ex) { 257 | onError(ex); 258 | } 259 | } 260 | else { 261 | onError(event); 262 | } 263 | }; 264 | 265 | var blob = file.slice(0, file.size); 266 | 267 | reader.readAsArrayBuffer(blob); 268 | }; 269 | 270 | /////////////////////////////////////////////////////////////////////////// 271 | // Use: 272 | // Register an uploaded file 273 | // 274 | // API: 275 | // POST /viewingservice/{apiversion}/register 276 | // 277 | // Response: 278 | // 279 | // "{"Result":"Success"}" 280 | /////////////////////////////////////////////////////////////////////////// 281 | this.register = function (fileId) { 282 | 283 | var xhr = new XMLHttpRequest(); 284 | 285 | xhr.open('POST', 286 | _baseUrl + '/viewingservice/v1/register', 287 | false); 288 | 289 | xhr.setRequestHeader( 290 | 'Authorization', 291 | 'Bearer ' + _accessTokenResponse.access_token); 292 | 293 | xhr.setRequestHeader( 294 | 'Content-Type', 295 | 'application/json'); 296 | 297 | //xhr.onreadystatechange = ...; 298 | 299 | var body = { 300 | urn: this.toBase64(fileId) 301 | }; 302 | 303 | try { 304 | 305 | xhr.send(JSON.stringify(body)); 306 | 307 | return JSON.parse(xhr.responseText); 308 | } 309 | catch (ex) { 310 | return ex; 311 | } 312 | }; 313 | 314 | /////////////////////////////////////////////////////////////////////////// 315 | // Use: 316 | // Get model thumbnail 317 | // 318 | // API: 319 | // GET /viewingservice/{apiversion}/thumbnails/{urn}? 320 | // guid=$GUID$ & width=$WIDTH$ & height=$HEIGHT$ (& type=$TYPE$) 321 | // 322 | // Response: 323 | // 324 | // 325 | /////////////////////////////////////////////////////////////////////////// 326 | this.getThumbnailAsync = function ( 327 | fileId, 328 | onSuccess, 329 | onError, 330 | width, 331 | height, 332 | guid) { 333 | 334 | var parameters = 335 | '?width=' + (width ? width : '150') + 336 | '&height=' + (height ? height : '150') + 337 | (guid ? '&guid=' + guid : ''); 338 | 339 | var xhr = new XMLHttpRequest(); 340 | 341 | xhr.open('GET', 342 | _baseUrl + 343 | "/viewingservice/v1/thumbnails/" + 344 | this.toBase64(fileId), 345 | true); 346 | 347 | xhr.setRequestHeader( 348 | 'Authorization', 349 | 'Bearer ' + _accessTokenResponse.access_token); 350 | 351 | xhr.responseType = 'arraybuffer'; 352 | 353 | xhr.onload = function (e) { 354 | if (this.status == 200) { 355 | 356 | //converts raw data to base64 img 357 | var base64 = btoa(String.fromCharCode.apply( 358 | null, new Uint8Array(this.response))); 359 | 360 | onSuccess(base64); 361 | } 362 | }; 363 | 364 | try { 365 | xhr.send(); 366 | } 367 | catch (ex) { 368 | onError(ex); 369 | } 370 | }; 371 | 372 | /////////////////////////////////////////////////////////////////////////// 373 | // Use: 374 | // Get model viewable 375 | // 376 | // API: 377 | // GET /viewingservice/{apiversion}/{urn}?guid=$GUID$ 378 | // GET /viewingservice/{apiversion}/{urn}/status?guid=$GUID$ 379 | // GET /viewingservice/{apiversion}/{urn}/all?guid=$GUID$ 380 | // 381 | // Response: 382 | // 383 | //{"guid":"dXJuOmFkc2sub...","type":"design","hasThumbnail":"true", 384 | //"progress":"complete","startedAt":"Mon Jun 23 11:28:18 UTC 2014", 385 | //"status":"success","success":"100%","urn":"dXJuOmFkc2sub...", 386 | //"children":[{"guid":"4cf5994a25ba","type":"folder","hasThumbnail":"true", 387 | //"name":"Trailer.dwf","progress":"complete","role":"viewable", 388 | //"status":"success","success":"100%","version":"2.0", 389 | //"children":[{"guid":"12EB52A2-7281-44D8-A704-02D8D4A2C69C_Sheets", 390 | //"type":"folder","hasThumbnail":"true","name":"Sheets", 391 | //"progress":"complete","status":"success","success":"100%", 392 | //"children":[{"guid":"com.autodesk.dwf.eModel","type":"geometry", 393 | //"hasThumbnail":"true","name":"Trailer.iam","order":0,"progress":"Complete", 394 | //"role":"3d","status":"Success","viewableID":"com.autodesk.dwf.eModel", 395 | //"properties":{"":{"Description":""}, 396 | //"hidden":{"_InitialURI":"presentation=49c8fb97,d4121f60", 397 | //"_LabelIconResourceID":"12EB52A6"}}, 398 | //"children":[{"guid":"d1fe2007","type":"view", 399 | //"camera":[5,-7,5,-0.1,-2,-5,-0.1,0.2,0.1,0.3,0.4,483.5,1], 400 | //"hasThumbnail":"false","name":"Home View","role":"3d"},{"guid":"45291936", 401 | //"type":"resource","hasThumbnail":"false", 402 | //"mime":"application/autodesk-svf","role":"graphics", 403 | //"urn":"urn:adsk.viewing:fs.file:sfsfsghs==/output/com.autodesk/0.svf"}, 404 | //{"guid":"9a5c1bcb","type":"resource", 405 | //"mime":"application/autodesk-db", 406 | //"role":"Autodesk.CloudPlatform.PropertyDatabase", 407 | //"urn":"urn:...ssdd==/output/com.autodesk.dwf/section_properties.db"}]}]}]}]} 408 | // 409 | /////////////////////////////////////////////////////////////////////////// 410 | this.getViewableAsync = function ( 411 | fileId, 412 | onSuccess, 413 | onError, 414 | option, 415 | guid) { 416 | 417 | var parameters = (guid ? '?guid=' + guid : ''); 418 | 419 | var optionStr = ""; 420 | 421 | switch (option) { 422 | 423 | case 'status': 424 | optionStr = "/status"; 425 | break; 426 | 427 | case 'all': 428 | optionStr = "/all"; 429 | break; 430 | 431 | default: 432 | break; 433 | } 434 | 435 | var xhr = new XMLHttpRequest(); 436 | 437 | xhr.open('GET', 438 | _baseUrl + 439 | "/viewingservice/v1/" + 440 | this.toBase64(fileId) + 441 | optionStr + 442 | parameters, 443 | true); 444 | 445 | xhr.setRequestHeader( 446 | 'Authorization', 447 | 'Bearer ' + _accessTokenResponse.access_token); 448 | 449 | xhr.onreadystatechange = function () { 450 | if (xhr.readyState == 4) { 451 | if (xhr.status == 200) { 452 | onSuccess(JSON.parse(xhr.responseText)); 453 | } 454 | else { 455 | onError(JSON.parse(xhr.responseText)); 456 | } 457 | } 458 | } 459 | 460 | try { 461 | xhr.send(); 462 | } 463 | catch (ex) { 464 | onError(ex); 465 | } 466 | }; 467 | 468 | /////////////////////////////////////////////////////////////////////////// 469 | // Get subitems with properties 470 | // 471 | /////////////////////////////////////////////////////////////////////////// 472 | this.getSubItemsWithProperties = function ( 473 | fileId, 474 | properties, 475 | onSuccess, 476 | onError) { 477 | 478 | function hasProperties(item, properties) { 479 | 480 | var hasProperties = true; 481 | 482 | for(var propName in properties) { 483 | 484 | if(!item.hasOwnProperty(propName)) { 485 | 486 | hasProperties = false; 487 | break; 488 | } 489 | 490 | if(item[propName] !== properties[propName]) { 491 | 492 | hasProperties = false; 493 | break; 494 | } 495 | } 496 | 497 | return hasProperties; 498 | } 499 | 500 | function getSubItemsWithPropertiesRec( 501 | viewable, properties) { 502 | 503 | var items = []; 504 | 505 | if(hasProperties(viewable, properties)) { 506 | 507 | items.push(viewable); 508 | } 509 | 510 | if (typeof viewable.children !== 'undefined') { 511 | 512 | for (var i=0; i.adsk-button').each(function(){ 160 | $(this).css({ 'display':'none' }); 161 | }); 162 | }); 163 | 164 | // Needed for v2.2+ 165 | 166 | $('#guiviewer3d-toolbar').css({ 'display':'none' }); 167 | $('.homeViewWrapper').css({ 'display':'none' }); 168 | $('.homeViewMenu').css({ 'display':'none' }); 169 | } 170 | ); 171 | } 172 | ); 173 | } 174 | else { 175 | 176 | showMessage('Disconnected', true); 177 | 178 | _viewer.uninitialize(); 179 | _viewer = new Autodesk.Viewing.Private.GuiViewer3D($('#viewer')[0], { 180 | wantInfoButton : false, 181 | extensions: [ 'Autodesk.Viewing.WebVR' ], 182 | experimental: [ 'webVR_orbitModel' ] 183 | }); 184 | } 185 | } 186 | 187 | 188 | function launchViewer(urn) { 189 | _baseDir = null; 190 | _loaded = false; 191 | _updating = false; 192 | _upVector = new THREE.Vector3(0, 1, 0); 193 | _orbitInitialPosition = null; 194 | 195 | // Make sure the VR extension doesn't change the fullscreen settings 196 | // when it loads ot unloads (these can only be called from a UI callback) 197 | // Also stub out the HUD message function, to stop those from being shown 198 | 199 | window.launchFullscreen = function() {}; 200 | window.exitFullscreen = function() {}; 201 | Autodesk.Viewing.Private.HudMessage.displayMessage = function() {}; 202 | 203 | if (urn) { 204 | // Remove all event listeners 205 | unwatchTilt; 206 | unwatchProgress(); 207 | 208 | clearMessage(); 209 | 210 | urn = urn.ensurePrefix('urn:'); 211 | 212 | Autodesk.Viewing.Document.load( 213 | urn, 214 | function(documentData) { 215 | var model = getModel(documentData); 216 | if (!model) return; 217 | 218 | // Uninitializing the viewers helps with stability 219 | 220 | if (_viewer) { 221 | _viewer.finish(); 222 | _viewer = null; 223 | } 224 | 225 | if (!_viewer) { 226 | _viewer = new Autodesk.Viewing.Private.GuiViewer3D($('#viewer')[0], { wantInfoButton : false }); 227 | _viewer.start(); 228 | 229 | // Added for WebVR support 230 | 231 | _viewer.impl.setRenderCallback( 232 | function(fn) { 233 | var pose = _vrDisplay.getPose(); 234 | var quat = new THREE.Quaternion().fromArray(pose.orientation); 235 | orbitByPose(quat); 236 | _vrDisplay.requestAnimationFrame(fn); 237 | } 238 | ); 239 | 240 | //_viewer.setQualityLevel(false, false); 241 | //_viewer.setGroundShadow(true); 242 | _viewer.setGroundReflection(false); 243 | _viewer.setProgressiveRendering(false); 244 | } 245 | 246 | watchProgress(); 247 | 248 | _viewer.prefs.remove("fusionOrbit", false); 249 | _viewer.prefs.remove("fusionOrbitConstrained", false); 250 | 251 | loadModel(_viewer, model); 252 | 253 | // Hide the viewer's toolbar and the home button 254 | 255 | $('.adsk-control-group').each(function(){ 256 | $(this).find('>.adsk-button').each(function(){ 257 | $(this).css({ 'display':'none' }); 258 | }); 259 | }); 260 | 261 | // Needed for v2.2+ 262 | 263 | $('#guiviewer3d-toolbar').css({ 'display':'none' }); 264 | $('.homeViewWrapper').css({ 'display':'none' }); 265 | $('.homeViewMenu').css({ 'display':'none' }); 266 | } 267 | ); 268 | } 269 | else { 270 | 271 | showMessage('Disconnected', true); 272 | 273 | _viewer.uninitialize(); 274 | _viewer = new Autodesk.Viewing.Private.GuiViewer3D($('#viewer')[0], { wantInfoButton : false}); 275 | } 276 | } 277 | 278 | function initConnection() { 279 | _socket.on('lmv-command', function(msg) { 280 | if (msg.name === 'load') { 281 | launchScopedViewer(msg.value, msg.disconnecting); 282 | } 283 | else if (msg.name === 'zoom') { 284 | _model_state.zoom_factor = parseFloat(msg.value); 285 | } 286 | else if (msg.name === 'explode') { 287 | _model_state.explode_factor = parseFloat(msg.value); 288 | } 289 | else if (msg.name === 'isolate') { 290 | _model_state.isolate_ids = msg.value; 291 | } 292 | else if (msg.name === 'hide') { 293 | _model_state.hide_ids = msg.value; 294 | } 295 | else if (msg.name === 'show') { 296 | _model_state.show_ids = msg.value; 297 | } 298 | else if (msg.name == 'section') { 299 | _model_state.cut_planes = msg.value.map(function(vec) { 300 | return new THREE.Vector4(vec.x, vec.y, vec.z, vec.w); 301 | }); 302 | } 303 | else if (msg.name === 'render') { 304 | _model_state.lighting = msg.value; 305 | } 306 | viewerApplyState(); 307 | }); 308 | } 309 | 310 | 311 | function viewerApplyState() { 312 | var not_ready = false; 313 | 314 | if (!_loaded || !_readyToApplyEvents) { 315 | return; 316 | } 317 | 318 | if (_model_state.zoom_factor !== undefined && !isNaN(_model_state.zoom_factor)) { 319 | 320 | unwatchTilt(); 321 | 322 | var previousUpdating = _updating; 323 | 324 | var newPos = zoomInOrOut(_viewer, _orbitInitialPosition, _model_state.zoom_factor); 325 | _viewer.navigation.setPosition(newPos); 326 | 327 | console.log("Zoomed: " + _model_state.zoom_factor); 328 | 329 | _orbitInitialPosition = newPos; 330 | 331 | _updating = previousUpdating; 332 | 333 | _model_state.zoom_factor = undefined; 334 | 335 | if (_lastVert && _lastHoriz) { 336 | orbitViews(_lastVert, _lastHoriz); 337 | } 338 | 339 | console.log('Applied zoom'); 340 | 341 | watchTilt(); 342 | } 343 | 344 | if (_model_state.explode_factor !== undefined) { 345 | viewersApply('explode', _model_state.explode_factor); 346 | _model_state.explode_factor = undefined; 347 | console.log('Applied explode'); 348 | } 349 | 350 | if (_model_state.isolate_ids !== undefined) { 351 | var worked = tryToApplyIds('isolate', _model_state.isolate_ids); 352 | if (worked) { 353 | _model_state.isolate_ids = undefined; 354 | console.log('Applied isolate'); 355 | } 356 | else 357 | console.log('Not ready to isolate'); 358 | not_ready = not_ready || !worked; 359 | } 360 | 361 | if (!not_ready && _model_state.show_ids !== undefined) { 362 | var worked = tryToApplyIds('show', _model_state.show_ids); 363 | if (worked) { 364 | _model_state.show_ids = undefined; 365 | console.log('Applied show'); 366 | } 367 | else 368 | console.log('Not ready to show'); 369 | not_ready = not_ready || !worked; 370 | } 371 | 372 | if (!not_ready && _model_state.hide_ids !== undefined) { 373 | var worked = tryToApplyIds('hide', _model_state.hide_ids); 374 | if (worked) { 375 | _model_state.hide_ids = undefined; 376 | console.log('Applied hide'); 377 | } 378 | else 379 | console.log('Not ready to hide'); 380 | not_ready = not_ready || !worked; 381 | } 382 | 383 | if (_model_state.cut_planes !== undefined) { 384 | viewersApply('setCutPlanes', _model_state.cut_planes); 385 | _model_state.cut_planes = undefined; 386 | console.log('Applied section'); 387 | } 388 | 389 | if (_model_state.lighting !== undefined) { 390 | viewersApply('setLightPreset', _model_state.lighting); 391 | _model_state.lighting = undefined; 392 | console.log('Applied lighting'); 393 | } 394 | 395 | if (not_ready) { 396 | setTimeout(function() { viewerApplyState(); }, 1000); 397 | } 398 | } 399 | 400 | 401 | function tryToApplyIds(prop, ids) { 402 | var success = true; 403 | try { 404 | viewersApply(prop, ids); 405 | } 406 | catch (ex) { 407 | success = false; 408 | } 409 | return success; 410 | } 411 | 412 | 413 | function viewersApply(func){ 414 | var val = Array.prototype.slice.call(arguments, 1); 415 | _viewer[func].apply(_viewer, val); 416 | } 417 | 418 | 419 | // Progress listener to set the view once the data has started 420 | // loading properly (we get a 5% notification early on that we 421 | // need to ignore - it comes too soon) 422 | function progressListener(e) { 423 | if (e.percent >= 10) { 424 | if (e.target.clientContainer.id === 'viewer') { 425 | _viewer.getObjectTree( 426 | function() { 427 | _loaded = true; 428 | console.log('Viewer has an instance tree'); 429 | setTimeout(finishProgress, 100); 430 | }, 431 | function() { 432 | _loaded = false; 433 | console.log('Cannot get instance tree'); 434 | } 435 | ); 436 | _viewer.removeEventListener('progress', progressListener); 437 | } 438 | } 439 | } 440 | 441 | function finishProgress() { 442 | 443 | if (_loaded) { 444 | 445 | if (!_orbitInitialPosition) { 446 | _orbitInitialPosition = _viewer.navigation.getPosition(); 447 | } 448 | var vec = _viewer.model.getUpVector(); 449 | _upVector = new THREE.Vector3(vec[0], vec[1], vec[2]); 450 | 451 | _viewer.loadExtension('Autodesk.ADN.Viewing.Extension.VR', {}); 452 | 453 | watchTilt(); 454 | 455 | _readyToApplyEvents = true; 456 | viewerApplyState(); 457 | } 458 | } 459 | 460 | 461 | function watchProgress() { 462 | _viewer.addEventListener('progress', progressListener); 463 | } 464 | 465 | 466 | function unwatchProgress() { 467 | if (_viewer) { 468 | _viewer.removeEventListener('progress', progressListener); 469 | } 470 | } 471 | 472 | 473 | function watchTilt() { 474 | if (window.DeviceOrientationEvent) { 475 | //window.addEventListener('deviceorientation', orb); 476 | } 477 | } 478 | 479 | 480 | function unwatchTilt() { 481 | if (window.DeviceOrientationEvent) { 482 | //window.removeEventListener('deviceorientation', orb); 483 | } 484 | } 485 | 486 | 487 | // And for the deviceorientation event 488 | 489 | 490 | function orb(e) { 491 | if (!e.alpha || !e.beta || !e.gamma || _updating) return; 492 | 493 | // gamma is the front-to-back in degrees (with 494 | // this screen orientation) with +90/-90 being 495 | // vertical and negative numbers being 'downwards' 496 | // with positive being 'upwards' 497 | var ab = Math.abs(e.beta); 498 | var flipped = (ab < 90 && e.gamma < 0) || (ab > 90 && e.gamma > 0); 499 | var vert = ((flipped ? e.gamma : -e.gamma) + (ab < 90 ? 90 : -90)) * _deg2rad; 500 | 501 | // When the orientation changes, reset the base direction 502 | if (_wasFlipped != flipped) { 503 | // If the angle goes below/above the horizontal, we don't 504 | // flip direction (we let it go a bit further) 505 | if (Math.abs(e.gamma) < 45) { 506 | flipped = _wasFlipped; 507 | } else { 508 | // Our base direction allows us to make relative horizontal 509 | // rotations when we rotate left & right 510 | _wasFlipped = flipped; 511 | _baseDir = e.alpha; 512 | } 513 | } 514 | 515 | // alpha is the compass direction the device is 516 | // facing in degrees. This equates to the 517 | // left - right rotation in landscape 518 | // orientation (with 0-360 degrees) 519 | var horiz = (e.alpha - _baseDir) * _deg2rad; 520 | 521 | // Save the latest horiz and vert values for use in zoom 522 | _lastHoriz = horiz; 523 | _lastVert = vert; 524 | 525 | orbitViews(vert, horiz); 526 | } 527 | 528 | function orbitByPose(q) { 529 | var x = q.x; 530 | var y = q.y; 531 | var z = q.z; 532 | var w = q.w; 533 | 534 | var roll = Math.atan2(2*y*w - 2*x*z, 1 - 2*y*y - 2*z*z); 535 | var pitch = Math.atan2(2*x*w - 2*y*z, 1 - 2*x*x - 2*z*z); 536 | var yaw = Math.asin(2*x*y + 2*z*w); 537 | 538 | //console.log("Roll: " + roll + ", Pitch: " + pitch + ", Yaw: " + yaw); 539 | 540 | if (pitch < 0) { 541 | roll = roll + Math.PI; 542 | } 543 | orbitViews(-pitch, Math.PI + roll); 544 | } 545 | 546 | function orbitViews(vert, horiz) { 547 | // We'll rotate our position based on the initial position 548 | // and the target will stay the same 549 | 550 | if (_orbitInitialPosition === null) 551 | return; 552 | 553 | var pos = _orbitInitialPosition.clone(); 554 | var trg = _viewer.navigation.getTarget(); 555 | 556 | // Start by applying the left/right orbit 557 | // (we need to check the up/down value, though) 558 | if (vert < 0 && !isIOSDevice()) { 559 | horiz = horiz + Math.PI; 560 | } 561 | 562 | var zAxis = _upVector.clone(); 563 | pos.applyAxisAngle(zAxis, horiz); 564 | 565 | // Now add the up/down rotation 566 | var axis = trg.clone().sub(pos).normalize(); 567 | axis.cross(zAxis); 568 | pos.applyAxisAngle(axis, -vert); 569 | 570 | // Determine the camera up vector: this is important if 571 | // getting to the extremities, to stop direction flipping 572 | var camUp = pos.clone().sub(trg).normalize(); 573 | camUp.cross(axis).normalize(); 574 | 575 | // Zoom in to this location 576 | zoom(_viewer, pos, trg, camUp); 577 | } 578 | 579 | 580 | function zoom(viewer, pos, trg, up) { 581 | // Make sure our up vector is correct for this model 582 | viewer.navigation.setWorldUpVector(_upVector, true); 583 | viewer.navigation.setView(pos, trg); 584 | viewer.navigation.setCameraUpVector(up); 585 | } 586 | 587 | 588 | function zoomInOrOut(viewer, pos, factor) { 589 | var direction = new THREE.Vector3(); 590 | var target = new THREE.Vector3(); //_viewer.navigation.getTarget(); 591 | direction.subVectors(pos, target); 592 | direction.normalize(); 593 | direction.multiplyScalar(factor); 594 | return direction.add(target); 595 | } -------------------------------------------------------------------------------- /html/js/vr-party-presenter-stereo.js: -------------------------------------------------------------------------------- 1 | var _socket = io(); 2 | var _sessionId; 3 | var _viewer; 4 | var _last_distance_to_target; 5 | var _view_data_bucket = 'vrparty'; 6 | var _default_models = { 7 | 'robot arm' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL1JvYm90QXJtLmR3Zng=', 8 | 'welding robot' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dnJwYXJ0eS9BQkJfcm9ib3QuZHdm', 9 | 'ergon chair' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL0VyZ29uLnppcA==', 10 | 'differential' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL0RpZmYuZHdmeA==', 11 | 'suspension' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL1N1c3BlbnNpb24uZHdm', 12 | 'house' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL2hvdXNlLmR3Zng=', 13 | 'flyer one' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL0ZseWVyT25lLmR3Zng=', 14 | 'motorcycle' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL01vdG9yY3ljbGUuZHdmeA==', 15 | 'V8 engine' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL1Y4RW5naW5lLnN0cA==', 16 | //'aotea' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL2FvdGVhMy5kd2Y=', 17 | //'dinghy' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL2RpbmdoeS5mM2Q=', 18 | 'column' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL3RhYmxldDIuemlw', 19 | 'tablet' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL2VneXB0NC56aXA=', 20 | //'trophy' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dnJwYXJ0eS9Ucm9waHlfQW5nZWxIYWNrLmYzZA==', 21 | //'cake' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dnJwYXJ0eS9IQkM0LmR3Zng=' 22 | 'movement' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dnJwYXJ0eS9FVEFfNjQ5Ny0xX01vdmVtZW50X0NvcnJlY3RlZF8zLmR3Zg' 23 | }; 24 | var _hosts = [ 'vr-party.herokuapp.com', 'www.vrok.it' ]; 25 | 26 | // 27 | // Initialize 28 | // 29 | 30 | function initialize() { 31 | 32 | _sessionId = getURLParameter('session'); 33 | if (_sessionId) { 34 | // Only generate the UI if a session ID was passed in via the URL 35 | 36 | // Populate our initial UI with a set of buttons, one for each function in the Buttons object 37 | var panel = document.getElementById('control'); 38 | for (var name in _default_models) { 39 | var urn = _default_models[name]; 40 | addButton(panel, name, function(urn) { return function() { launchUrn(urn); } }(urn)); 41 | } 42 | 43 | var base_url = window.location.origin; 44 | if (_hosts.indexOf(window.location.hostname) > -1) { 45 | // Apparently some phone browsers don't like the mix of http and https 46 | // Default to https on Heroku deployment 47 | base_url = 'https://' + window.location.hostname; 48 | } 49 | 50 | var url = base_url + '/participant2.html?session=' + _sessionId; 51 | $('#url').attr('href', url); 52 | $('#qrcode').qrcode(url); 53 | 54 | // If the provided session exists then load its data (right now just its URN) 55 | $.get( 56 | window.location.origin + '/api/getSession/' + _sessionId, 57 | function(req2, res2) { 58 | if (res2 === "success") { 59 | 60 | readCookiesForCustomModel(); 61 | initializeSelectFilesDialog(); 62 | 63 | if (req2 !== "") { 64 | Autodesk.Viewing.Initializer(getViewingOptions(), function() { 65 | launchUrn(req2); 66 | }); 67 | } 68 | else { 69 | // Otherwise we'll create a session with this name 70 | // (we may want to disable this for security reasons, 71 | // but it's actually a nice way to create sessions 72 | // with custom names) 73 | _socket.emit('create-session', { id: _sessionId }); 74 | 75 | // Initialize viewing but don't start a viewer 76 | Autodesk.Viewing.Initializer(getViewingOptions(), function() { 77 | showAbout(); 78 | !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs'); 79 | }); 80 | } 81 | } 82 | } 83 | ); 84 | } 85 | else { 86 | // If no session was provided, redirect the browser to a session 87 | // generated by the server 88 | $.get( 89 | window.location.origin + '/api/sessionId', 90 | function(res) { 91 | _sessionId = res; 92 | window.location.href = window.location.href + "?session=" + _sessionId; 93 | //window.location.replace(window.location.origin + "?session=" + _sessionId); 94 | } 95 | ); 96 | } 97 | } 98 | 99 | 100 | // 101 | // Terminate 102 | // 103 | 104 | function terminate() { 105 | if (_sessionId) { 106 | _socket.emit('close-session', { id: _sessionId }); 107 | } 108 | } 109 | 110 | 111 | function addButton(panel, buttonName, loadFunction) { 112 | var button = document.createElement('div'); 113 | button.classList.add('cmd-btn-small'); 114 | 115 | button.innerHTML = buttonName; 116 | button.onclick = loadFunction; 117 | 118 | panel.appendChild(button); 119 | } 120 | 121 | 122 | function launchUrn(urn) { 123 | 124 | var viewerToClose; 125 | 126 | // Uninitializing the viewer helps with stability 127 | if (_viewer) { 128 | viewerToClose = _viewer; 129 | _viewer = null; 130 | } 131 | 132 | if (urn) { 133 | 134 | $('#aboutDiv').hide(); 135 | $('#3dViewDiv').show(); 136 | 137 | _socket.emit('lmv-command', { session: _sessionId, name: 'load', value: urn }); 138 | 139 | urn = urn.ensurePrefix('urn:'); 140 | 141 | Autodesk.Viewing.Document.load( 142 | urn, 143 | function(documentData) { 144 | var model = getModel(documentData); 145 | if (!model) return; 146 | 147 | _viewer = new Autodesk.Viewing.Private.GuiViewer3D($('#3dViewDiv')[0]); 148 | _viewer.start(); 149 | _viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, onCameraChange); 150 | _viewer.addEventListener(Autodesk.Viewing.ISOLATE_EVENT, onIsolate); 151 | _viewer.addEventListener(Autodesk.Viewing.HIDE_EVENT, onHide); 152 | _viewer.addEventListener(Autodesk.Viewing.SHOW_EVENT, onShow); 153 | _viewer.addEventListener(Autodesk.Viewing.EXPLODE_CHANGE_EVENT, onExplode); 154 | _viewer.addEventListener(Autodesk.Viewing.CUTPLANES_CHANGE_EVENT,onSection); 155 | _viewer.addEventListener(Autodesk.Viewing.RENDER_OPTION_CHANGED_EVENT, onRenderOption); 156 | 157 | resetSize(_viewer.container); 158 | 159 | if (viewerToClose) { 160 | viewerToClose.finish(); 161 | } 162 | 163 | loadModel(_viewer, model); 164 | } 165 | ); 166 | } 167 | else { 168 | // Create a blank viewer on first load 169 | _viewer = new Autodesk.Viewing.Private.GuiViewer3D($('#3dViewDiv')[0]); 170 | resetSize(_viewer.container); 171 | } 172 | } 173 | 174 | 175 | function resetSize(elem, fullHeight) { 176 | elem.style.width = window.innerWidth - 360 + 'px'; // subtract the left column 177 | if (fullHeight) { 178 | elem.style.height = ''; 179 | } 180 | else { 181 | elem.style.height = (window.innerHeight - 40) + 'px'; // subtract the table padding 182 | } 183 | } 184 | 185 | 186 | // 187 | // Viewer3D events 188 | // 189 | 190 | function onCameraChange(event) { 191 | 192 | // With OBJ models the target moves to keep equidistant from the camera 193 | // So we just check the distance from the origin rather than the target 194 | // It seems to work, anyway! 195 | var distance_to_target = _viewer.navigation.getPosition().length(); //distanceTo(_viewer.navigation.getTarget()); 196 | if (_last_distance_to_target === undefined || Math.abs(distance_to_target - _last_distance_to_target) > 0.1) { 197 | _socket.emit('lmv-command', { session: _sessionId, name: 'zoom', value: distance_to_target }); 198 | _last_distance_to_target = distance_to_target; 199 | } 200 | } 201 | 202 | 203 | // Translate a list of objects (for R13 & R14) to a list of IDs 204 | // Socket.io prefers not to have binary content to transfer, it seems 205 | function getIdList(ids) { 206 | if (ids.length > 0 && typeof ids[0] === 'object') { 207 | ids = ids.map(function(obj) { return obj.dbId;}); 208 | } 209 | return ids; 210 | } 211 | 212 | function onIsolate(event) { 213 | _socket.emit('lmv-command', { session: _sessionId, name: 'isolate', value: getIdList(event.nodeIdArray) }); 214 | } 215 | 216 | 217 | function onHide(event) { 218 | _socket.emit('lmv-command', { session: _sessionId, name: 'hide', value: getIdList(event.nodeIdArray) }); 219 | } 220 | 221 | 222 | function onShow(event) { 223 | _socket.emit('lmv-command', { session: _sessionId, name: 'show', value: getIdList(event.nodeIdArray) }); 224 | } 225 | 226 | 227 | function onExplode() { 228 | _socket.emit('lmv-command', { session: _sessionId, name: 'explode', value: _viewer.getExplodeScale() }); 229 | } 230 | 231 | 232 | function onSection(event) { 233 | _socket.emit('lmv-command', { session: _sessionId, name: 'section', value: _viewer.getCutPlanes() }); 234 | } 235 | 236 | 237 | function onRenderOption(event) { 238 | _socket.emit('lmv-command', { session: _sessionId, name: 'render', value: _viewer.impl.currentLightPreset() }); 239 | } 240 | 241 | 242 | // 243 | // Models upload 244 | // 245 | 246 | function onFileSelect() { 247 | var el = document.getElementById('fileElem'); 248 | if (el) { 249 | el.click(); 250 | } 251 | } 252 | 253 | 254 | function cancel() { 255 | $(this).dialog('close'); 256 | $('#upload-button').html('Upload file'); 257 | } 258 | 259 | 260 | function upload() { 261 | 262 | $('#upload-button').html('Uploading...'); 263 | 264 | var filteredForUpload = new Array(); 265 | 266 | $(':checkbox').each(function() { 267 | if ($(this).is(':checked')) { 268 | // 'filesToUpload' seems to be not a regular array, 'filter()'' function is undefined 269 | for (var i = 0; i < filesToUpload.length; ++i) { 270 | var file = filesToUpload[i]; 271 | if (file.name == $(this).val()) { 272 | filteredForUpload.push(file); 273 | } 274 | } 275 | } 276 | }); 277 | 278 | console.log("Filtered for upload"); 279 | for (var i = 0; i < filteredForUpload.length; ++i) { 280 | var file = filteredForUpload[i]; 281 | console.log('Selected file: ' + file.name + ' size: ' + file.size); 282 | } 283 | 284 | onUpload(filteredForUpload); 285 | 286 | $(this).dialog('close'); 287 | } 288 | 289 | 290 | function deselectAllFiles() { 291 | $(':checkbox').prop('checked', false); 292 | $(":button:contains('OK')").prop("disabled", true).addClass("ui-state-disabled"); 293 | } 294 | 295 | 296 | function selectAllFiles() { 297 | $(':checkbox').prop('checked', true); 298 | $(":button:contains('OK')").prop("disabled", false).removeClass("ui-state-disabled"); 299 | } 300 | 301 | 302 | function initializeSelectFilesDialog() { 303 | var dlg = document.getElementsByName("upload-files"); 304 | 305 | if (dlg.length == 0) { 306 | 307 | var dlgDiv = document.createElement("div"); 308 | dlgDiv.id = "upload-files"; 309 | dlgDiv.title='Uploading files'; 310 | document.getElementsByTagName("body")[0].appendChild(dlgDiv); 311 | 312 | $('#upload-files').append("

The following files are larger than 2MB. Are you sure you want to upload them?

"); 313 | 314 | var buttons = { 315 | Cancel: cancel, 316 | 'OK': upload, 317 | 'Deselect All': deselectAllFiles, 318 | 'Select All': selectAllFiles 319 | }; 320 | 321 | $('#upload-files').dialog({ 322 | autoOpen: false, 323 | modal: true, 324 | buttons: buttons, 325 | width:"auto", 326 | resizable: false, 327 | }); 328 | } 329 | } 330 | 331 | 332 | function clearCheckBoxes() { 333 | var checkboxes = document.getElementById("checkboxes"); 334 | if (checkboxes) { 335 | checkboxes.parentNode.removeChild(checkboxes); 336 | } 337 | 338 | checkboxes = document.createElement('div'); 339 | checkboxes.id = "checkboxes"; 340 | $('#upload-files').append(checkboxes); 341 | } 342 | 343 | 344 | function createCheckBox(fileName) { 345 | var id = "filename-checkbox-" + fileName; 346 | var checkbox = document.createElement('input'); 347 | checkbox.id = id; 348 | checkbox.type = "checkbox"; 349 | checkbox.name = "upload-files"; 350 | checkbox.value = fileName; 351 | 352 | $("#upload-files").change(function() { 353 | var numberChecked = $("input[name='upload-files']:checked").size(); 354 | if (numberChecked > 0) { 355 | $(":button:contains('OK')").prop("disabled", false).removeClass("ui-state-disabled"); 356 | } else { 357 | $(":button:contains('OK')").prop("disabled", true).addClass("ui-state-disabled"); 358 | } 359 | }); 360 | 361 | var label = document.createElement('label'); 362 | label.htmlFor = id; 363 | label.appendChild(document.createTextNode(fileName)); 364 | 365 | var br = document.createElement('br'); 366 | 367 | $('#checkboxes').append(checkbox); 368 | $('#checkboxes').append(label); 369 | $('#checkboxes').append(br); 370 | } 371 | 372 | 373 | function resetSelectedFiles() { 374 | var fileElem = $("#fileElem"); 375 | fileElem.wrap("
").closest("form").get(0).reset(); 376 | fileElem.unwrap(); 377 | } 378 | 379 | 380 | function onFilesDialogCalled(files) { 381 | filesToUpload = []; 382 | var sizeLimit = 2097152; // 2MB 383 | 384 | clearCheckBoxes(); 385 | 386 | var numberFilesLargerThanLimit = 0; 387 | for (var i = 0; i < files.length; ++i) { 388 | var file = files[i]; 389 | if (file.size > sizeLimit) { 390 | ++numberFilesLargerThanLimit; 391 | createCheckBox(file.name); 392 | } 393 | 394 | filesToUpload.push(file); 395 | } 396 | 397 | // select all files in the confirmation dialog 398 | selectAllFiles(); 399 | 400 | // reset FilesSet property of the input element 401 | resetSelectedFiles(); 402 | 403 | if (numberFilesLargerThanLimit > 0) { 404 | $('#upload-files').dialog('open'); 405 | } else { 406 | onUpload(filesToUpload); 407 | } 408 | } 409 | 410 | 411 | function onUpload(files) { 412 | $.get( 413 | window.location.origin + '/api/uploadtoken', 414 | function(accessTokenResponse) { 415 | var viewDataClient = new Autodesk.ADN.Toolkit.ViewData.AdnViewDataClient( 416 | 'https://developer.api.autodesk.com', 417 | accessTokenResponse 418 | ); 419 | viewDataClient.getBucketDetailsAsync( 420 | _view_data_bucket, 421 | function(bucketResponse) { 422 | //onSuccess 423 | console.log('Bucket details successful:'); 424 | console.log(bucketResponse); 425 | uploadFiles(viewDataClient, _view_data_bucket, files); 426 | }, 427 | function(error) { 428 | //onError 429 | console.log("Bucket doesn't exist"); 430 | console.log('Attempting to create...'); 431 | } 432 | ); 433 | } 434 | ); 435 | } 436 | 437 | 438 | function uploadFiles(viewDataClient, bucket, files) { 439 | for (var i = 0; i < files.length; ++i) { 440 | var file = files[i]; 441 | console.log('Uploading file: ' + file.name + ' ...'); 442 | viewDataClient.uploadFileAsync( 443 | file, 444 | bucket, 445 | file.name.replace(/ /g,'_'), // Translation API cannot handle spaces... 446 | function(response) { 447 | //onSuccess 448 | console.log('File upload successful:'); 449 | console.log(response); 450 | var fileId = response.objects[0].id; 451 | var registerResponse = viewDataClient.register(fileId); 452 | 453 | if (registerResponse.Result === 'Success' || 454 | registerResponse.Result === 'Created') { 455 | console.log('Registration result: ' + registerResponse.Result); 456 | console.log('Starting translation: ' + fileId); 457 | 458 | checkTranslationStatus( 459 | viewDataClient, 460 | fileId, 461 | 1000 * 60 * 5, //5 mins timeout 462 | function(viewable) { 463 | //onSuccess 464 | console.log('Translation successful: ' + response.file.name); 465 | console.log('Viewable: '); 466 | console.log(viewable); 467 | 468 | var urn = viewable.urn; 469 | 470 | // add new button 471 | var panel = document.getElementById('control'); 472 | var name = truncateName(response.file.name); 473 | addButton(panel, name, function(urn) { return function() { launchUrn(urn); } }(urn)); 474 | 475 | // open it in a viewer 476 | launchUrn(urn); 477 | 478 | // and store as a cookie 479 | createCookieForCustomModel('custom_model_' + response.file.name, urn); 480 | }); 481 | } 482 | }, 483 | 484 | //onError 485 | function (error) { 486 | console.log('File upload failed:'); 487 | console.log(error); 488 | }); 489 | } 490 | } 491 | 492 | 493 | function checkTranslationStatus(viewDataClient, fileId, timeout, onSuccess) { 494 | var startTime = new Date().getTime(); 495 | var timer = setInterval(function() { 496 | var dt = (new Date().getTime() - startTime) / timeout; 497 | if (dt >= 1.0) { 498 | clearInterval(timer); 499 | } else { 500 | viewDataClient.getViewableAsync( 501 | fileId, 502 | function(response) { 503 | console.log(response); 504 | console.log('Translation Progress ' + fileId + ': ' + response.progress); 505 | $('#upload-button').html(response.progress); 506 | 507 | if (response.progress === 'complete') { 508 | clearInterval(timer); 509 | onSuccess(response); 510 | $('#upload-button').html('Upload file'); 511 | } 512 | }, 513 | function(error) {} 514 | ); 515 | } 516 | }, 2000); 517 | }; 518 | 519 | 520 | // 521 | // Models stored in cookies 522 | // 523 | 524 | function truncateName(name) { 525 | var dotIdx = name.lastIndexOf("."); 526 | if (dotIdx != -1) { 527 | var name = name.substring(0, dotIdx); 528 | 529 | if (name.length > 8) { 530 | name = name.substring(0, 8) + "..."; 531 | } 532 | } 533 | 534 | return name; 535 | } 536 | 537 | 538 | function createCookieForCustomModel(name, value, days) { 539 | if (days) { 540 | var date = new Date(); 541 | date.setTime(date.getTime() + (days*24*60*60*1000)); 542 | var expires = '; expires=' + date.toGMTString(); 543 | } else { 544 | var expires = ''; 545 | } 546 | 547 | var urn = encodeURIComponent(value); 548 | document.cookie = name + '=' + urn + expires + '; path=/'; 549 | } 550 | 551 | 552 | function readCookiesForCustomModel() { 553 | var prefix = 'custom_model_'; 554 | var cookies = document.cookie.split(';'); 555 | 556 | for (var i in cookies) { 557 | var c = cookies[i]; 558 | if (c.indexOf(prefix) != -1) { 559 | c = c.replace(prefix, ''); 560 | var nameValue = c.split('='); 561 | if (nameValue) { 562 | var panel = document.getElementById('control'); 563 | addButton(panel, truncateName(nameValue[0]), function(urn) { 564 | return function() { launchUrn(urn); } 565 | }(decodeURIComponent(nameValue[1]))); 566 | } 567 | } 568 | } 569 | } 570 | 571 | 572 | function showAbout() { 573 | $('#aboutDiv').css('text-indent', 0); 574 | resetSize($('#layer2')[0], true); 575 | $('#3dViewDiv').hide(); 576 | $('#aboutDiv').show(); 577 | } 578 | 579 | 580 | // Prevent resize from being called too frequently 581 | // (might want to adjust the timeout from 50ms) 582 | var resize = debounce(function() { 583 | var div = $('#3dViewDiv'); 584 | var viewing = div.is(':visible'); 585 | resetSize(viewing ? _viewer.container : $('#layer2')[0], !viewing); 586 | }, 50); -------------------------------------------------------------------------------- /html/js/vr-party-presenter.js: -------------------------------------------------------------------------------- 1 | var _socket = io(); 2 | var _sessionId; 3 | var _viewer; 4 | var _last_distance_to_target; 5 | var _view_data_bucket = 'vrparty'; 6 | var _default_models = { 7 | 'pumpkin' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dnJwYXJ0eS9wdW1rcGluX3YxLjcyODk1MjAzLTIzZTQtNGRkZi05MThlLWZhNjliOTlhYWJiOC5mM2Q', 8 | 'robot arm' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL1JvYm90QXJtLmR3Zng=', 9 | 'welding robot' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dnJwYXJ0eS9BQkJfcm9ib3QuZHdm', 10 | 'ergon chair' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL0VyZ29uLnppcA==', 11 | 'differential' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL0RpZmYuZHdmeA==', 12 | 'suspension' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL1N1c3BlbnNpb24uZHdm', 13 | 'house' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL2hvdXNlLmR3Zng=', 14 | 'flyer one' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL0ZseWVyT25lLmR3Zng=', 15 | 'motorcycle' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL01vdG9yY3ljbGUuZHdmeA==', 16 | 'V8 engine' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL1Y4RW5naW5lLnN0cA==', 17 | //'aotea' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL2FvdGVhMy5kd2Y=', 18 | //'dinghy' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL2RpbmdoeS5mM2Q=', 19 | 'column' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL3RhYmxldDIuemlw', 20 | 'tablet' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c3RlYW1idWNrL2VneXB0NC56aXA=', 21 | //'trophy' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dnJwYXJ0eS9Ucm9waHlfQW5nZWxIYWNrLmYzZA==', 22 | //'cake' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dnJwYXJ0eS9IQkM0LmR3Zng=' 23 | 'movement' : 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dnJwYXJ0eS9FVEFfNjQ5Ny0xX01vdmVtZW50X0NvcnJlY3RlZF8zLmR3Zg' 24 | }; 25 | var _hosts = [ 'vr-party.herokuapp.com', 'www.vrok.it' ]; 26 | 27 | // 28 | // Initialize 29 | // 30 | 31 | function initialize() { 32 | 33 | _sessionId = getURLParameter('session'); 34 | if (_sessionId) { 35 | // Only generate the UI if a session ID was passed in via the URL 36 | 37 | // Populate our initial UI with a set of buttons, one for each function in the Buttons object 38 | var panel = document.getElementById('control'); 39 | for (var name in _default_models) { 40 | var urn = _default_models[name]; 41 | addButton(panel, name, function(urn) { return function() { launchUrn(urn); } }(urn)); 42 | } 43 | 44 | var base_url = window.location.origin; 45 | if (_hosts.indexOf(window.location.hostname) > -1) { 46 | // Apparently some phone browsers don't like the mix of http and https 47 | // Default to https on Heroku deployment 48 | base_url = 'https://' + window.location.hostname; 49 | } 50 | 51 | var url = base_url + '/participant.html?session=' + _sessionId; 52 | $('#url').attr('href', url); 53 | $('#qrcode').qrcode(url); 54 | $('#join').text("www.vrok.it/join?id=" + _sessionId); 55 | 56 | // If the provided session exists then load its data (right now just its URN) 57 | $.get( 58 | window.location.origin + '/api/getSession/' + _sessionId, 59 | function(req2, res2) { 60 | if (res2 === "success") { 61 | 62 | readCookiesForCustomModel(); 63 | initializeSelectFilesDialog(); 64 | 65 | if (req2 !== "") { 66 | Autodesk.Viewing.Initializer(getViewingOptions(), function() { 67 | launchUrn(req2); 68 | }); 69 | } 70 | else { 71 | // Otherwise we'll create a session with this name 72 | // (we may want to disable this for security reasons, 73 | // but it's actually a nice way to create sessions 74 | // with custom names) 75 | _socket.emit('create-session', { id: _sessionId }); 76 | 77 | // Initialize viewing but don't start a viewer 78 | Autodesk.Viewing.Initializer(getViewingOptions(), function() { 79 | showAbout(); 80 | !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs'); 81 | }); 82 | } 83 | } 84 | } 85 | ); 86 | } 87 | else { 88 | // If no session was provided, redirect the browser to a session 89 | // generated by the server 90 | $.get( 91 | window.location.origin + '/api/sessionId', 92 | function(res) { 93 | _sessionId = res; 94 | window.location.href = window.location.origin + "?session=" + _sessionId; 95 | //window.location.replace(window.location.origin + "?session=" + _sessionId); 96 | } 97 | ); 98 | } 99 | } 100 | 101 | 102 | // 103 | // Terminate 104 | // 105 | 106 | function terminate() { 107 | if (_sessionId) { 108 | _socket.emit('close-session', { id: _sessionId }); 109 | } 110 | } 111 | 112 | 113 | function addButton(panel, buttonName, loadFunction) { 114 | var button = document.createElement('div'); 115 | button.classList.add('cmd-btn-small'); 116 | 117 | button.innerHTML = buttonName; 118 | button.onclick = loadFunction; 119 | 120 | panel.appendChild(button); 121 | } 122 | 123 | 124 | function launchUrn(urn) { 125 | 126 | var viewerToClose; 127 | 128 | // Uninitializing the viewer helps with stability 129 | if (_viewer) { 130 | viewerToClose = _viewer; 131 | _viewer = null; 132 | } 133 | 134 | if (urn) { 135 | 136 | $('#aboutDiv').hide(); 137 | $('#3dViewDiv').show(); 138 | 139 | _socket.emit('lmv-command', { session: _sessionId, name: 'load', value: urn }); 140 | 141 | urn = urn.ensurePrefix('urn:'); 142 | 143 | Autodesk.Viewing.Document.load( 144 | urn, 145 | function(documentData) { 146 | var model = getModel(documentData); 147 | if (!model) return; 148 | 149 | _viewer = new Autodesk.Viewing.Private.GuiViewer3D($('#3dViewDiv')[0]); 150 | _viewer.start(); 151 | _viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, onCameraChange); 152 | _viewer.addEventListener(Autodesk.Viewing.ISOLATE_EVENT, onIsolate); 153 | _viewer.addEventListener(Autodesk.Viewing.HIDE_EVENT, onHide); 154 | _viewer.addEventListener(Autodesk.Viewing.SHOW_EVENT, onShow); 155 | _viewer.addEventListener(Autodesk.Viewing.EXPLODE_CHANGE_EVENT, onExplode); 156 | _viewer.addEventListener(Autodesk.Viewing.CUTPLANES_CHANGE_EVENT,onSection); 157 | _viewer.addEventListener(Autodesk.Viewing.RENDER_OPTION_CHANGED_EVENT, onRenderOption); 158 | 159 | resetSize(_viewer.container); 160 | 161 | if (viewerToClose) { 162 | viewerToClose.finish(); 163 | } 164 | 165 | loadModel(_viewer, model); 166 | } 167 | ); 168 | } 169 | else { 170 | // Create a blank viewer on first load 171 | _viewer = new Autodesk.Viewing.Private.GuiViewer3D($('#3dViewDiv')[0]); 172 | resetSize(_viewer.container); 173 | } 174 | } 175 | 176 | 177 | function resetSize(elem, fullHeight) { 178 | elem.style.width = window.innerWidth - 360 + 'px'; // subtract the left column 179 | if (fullHeight) { 180 | elem.style.height = ''; 181 | } 182 | else { 183 | elem.style.height = (window.innerHeight - 40) + 'px'; // subtract the table padding 184 | } 185 | } 186 | 187 | 188 | // 189 | // Viewer3D events 190 | // 191 | 192 | function onCameraChange(event) { 193 | 194 | // With OBJ models the target moves to keep equidistant from the camera 195 | // So we just check the distance from the origin rather than the target 196 | // It seems to work, anyway! 197 | var distance_to_target = _viewer.navigation.getPosition().length(); //distanceTo(_viewer.navigation.getTarget()); 198 | if (_last_distance_to_target === undefined || Math.abs(distance_to_target - _last_distance_to_target) > 0.1) { 199 | _socket.emit('lmv-command', { session: _sessionId, name: 'zoom', value: distance_to_target }); 200 | _last_distance_to_target = distance_to_target; 201 | } 202 | } 203 | 204 | 205 | // Translate a list of objects (for R13 & R14) to a list of IDs 206 | // Socket.io prefers not to have binary content to transfer, it seems 207 | function getIdList(ids) { 208 | if (ids.length > 0 && typeof ids[0] === 'object') { 209 | ids = ids.map(function(obj) { return obj.dbId;}); 210 | } 211 | return ids; 212 | } 213 | 214 | function onIsolate(event) { 215 | _socket.emit('lmv-command', { session: _sessionId, name: 'isolate', value: getIdList(event.nodeIdArray) }); 216 | } 217 | 218 | 219 | function onHide(event) { 220 | _socket.emit('lmv-command', { session: _sessionId, name: 'hide', value: getIdList(event.nodeIdArray) }); 221 | } 222 | 223 | 224 | function onShow(event) { 225 | _socket.emit('lmv-command', { session: _sessionId, name: 'show', value: getIdList(event.nodeIdArray) }); 226 | } 227 | 228 | 229 | function onExplode() { 230 | _socket.emit('lmv-command', { session: _sessionId, name: 'explode', value: _viewer.getExplodeScale() }); 231 | } 232 | 233 | 234 | function onSection(event) { 235 | _socket.emit('lmv-command', { session: _sessionId, name: 'section', value: _viewer.getCutPlanes() }); 236 | } 237 | 238 | 239 | function onRenderOption(event) { 240 | _socket.emit('lmv-command', { session: _sessionId, name: 'render', value: _viewer.impl.currentLightPreset() }); 241 | } 242 | 243 | 244 | // 245 | // Models upload 246 | // 247 | 248 | function onFileSelect() { 249 | var el = document.getElementById('fileElem'); 250 | if (el) { 251 | el.click(); 252 | } 253 | } 254 | 255 | 256 | function cancel() { 257 | $(this).dialog('close'); 258 | $('#upload-button').html('Upload file'); 259 | } 260 | 261 | 262 | function upload() { 263 | 264 | $('#upload-button').html('Uploading...'); 265 | 266 | var filteredForUpload = new Array(); 267 | 268 | $(':checkbox').each(function() { 269 | if ($(this).is(':checked')) { 270 | // 'filesToUpload' seems to be not a regular array, 'filter()'' function is undefined 271 | for (var i = 0; i < filesToUpload.length; ++i) { 272 | var file = filesToUpload[i]; 273 | if (file.name == $(this).val()) { 274 | filteredForUpload.push(file); 275 | } 276 | } 277 | } 278 | }); 279 | 280 | console.log("Filtered for upload"); 281 | for (var i = 0; i < filteredForUpload.length; ++i) { 282 | var file = filteredForUpload[i]; 283 | console.log('Selected file: ' + file.name + ' size: ' + file.size); 284 | } 285 | 286 | onUpload(filteredForUpload); 287 | 288 | $(this).dialog('close'); 289 | } 290 | 291 | 292 | function deselectAllFiles() { 293 | $(':checkbox').prop('checked', false); 294 | $(":button:contains('OK')").prop("disabled", true).addClass("ui-state-disabled"); 295 | } 296 | 297 | 298 | function selectAllFiles() { 299 | $(':checkbox').prop('checked', true); 300 | $(":button:contains('OK')").prop("disabled", false).removeClass("ui-state-disabled"); 301 | } 302 | 303 | 304 | function initializeSelectFilesDialog() { 305 | var dlg = document.getElementsByName("upload-files"); 306 | 307 | if (dlg.length == 0) { 308 | 309 | var dlgDiv = document.createElement("div"); 310 | dlgDiv.id = "upload-files"; 311 | dlgDiv.title='Uploading files'; 312 | document.getElementsByTagName("body")[0].appendChild(dlgDiv); 313 | 314 | $('#upload-files').append("

The following files are larger than 2MB. Are you sure you want to upload them?

"); 315 | 316 | var buttons = { 317 | Cancel: cancel, 318 | 'OK': upload, 319 | 'Deselect All': deselectAllFiles, 320 | 'Select All': selectAllFiles 321 | }; 322 | 323 | $('#upload-files').dialog({ 324 | autoOpen: false, 325 | modal: true, 326 | buttons: buttons, 327 | width:"auto", 328 | resizable: false, 329 | }); 330 | } 331 | } 332 | 333 | 334 | function clearCheckBoxes() { 335 | var checkboxes = document.getElementById("checkboxes"); 336 | if (checkboxes) { 337 | checkboxes.parentNode.removeChild(checkboxes); 338 | } 339 | 340 | checkboxes = document.createElement('div'); 341 | checkboxes.id = "checkboxes"; 342 | $('#upload-files').append(checkboxes); 343 | } 344 | 345 | 346 | function createCheckBox(fileName) { 347 | var id = "filename-checkbox-" + fileName; 348 | var checkbox = document.createElement('input'); 349 | checkbox.id = id; 350 | checkbox.type = "checkbox"; 351 | checkbox.name = "upload-files"; 352 | checkbox.value = fileName; 353 | 354 | $("#upload-files").change(function() { 355 | var numberChecked = $("input[name='upload-files']:checked").size(); 356 | if (numberChecked > 0) { 357 | $(":button:contains('OK')").prop("disabled", false).removeClass("ui-state-disabled"); 358 | } else { 359 | $(":button:contains('OK')").prop("disabled", true).addClass("ui-state-disabled"); 360 | } 361 | }); 362 | 363 | var label = document.createElement('label'); 364 | label.htmlFor = id; 365 | label.appendChild(document.createTextNode(fileName)); 366 | 367 | var br = document.createElement('br'); 368 | 369 | $('#checkboxes').append(checkbox); 370 | $('#checkboxes').append(label); 371 | $('#checkboxes').append(br); 372 | } 373 | 374 | 375 | function resetSelectedFiles() { 376 | var fileElem = $("#fileElem"); 377 | fileElem.wrap("").closest("form").get(0).reset(); 378 | fileElem.unwrap(); 379 | } 380 | 381 | 382 | function onFilesDialogCalled(files) { 383 | filesToUpload = []; 384 | var sizeLimit = 2097152; // 2MB 385 | 386 | clearCheckBoxes(); 387 | 388 | var numberFilesLargerThanLimit = 0; 389 | for (var i = 0; i < files.length; ++i) { 390 | var file = files[i]; 391 | if (file.size > sizeLimit) { 392 | ++numberFilesLargerThanLimit; 393 | createCheckBox(file.name); 394 | } 395 | 396 | filesToUpload.push(file); 397 | } 398 | 399 | // select all files in the confirmation dialog 400 | selectAllFiles(); 401 | 402 | // reset FilesSet property of the input element 403 | resetSelectedFiles(); 404 | 405 | if (numberFilesLargerThanLimit > 0) { 406 | $('#upload-files').dialog('open'); 407 | } else { 408 | onUpload(filesToUpload); 409 | } 410 | } 411 | 412 | 413 | function onUpload(files) { 414 | $.get( 415 | window.location.origin + '/api/uploadtoken', 416 | function(accessTokenResponse) { 417 | var viewDataClient = new Autodesk.ADN.Toolkit.ViewData.AdnViewDataClient( 418 | 'https://developer.api.autodesk.com', 419 | accessTokenResponse 420 | ); 421 | viewDataClient.getBucketDetailsAsync( 422 | _view_data_bucket, 423 | function(bucketResponse) { 424 | //onSuccess 425 | console.log('Bucket details successful:'); 426 | console.log(bucketResponse); 427 | uploadFiles(viewDataClient, _view_data_bucket, files); 428 | }, 429 | function(error) { 430 | //onError 431 | console.log("Bucket doesn't exist"); 432 | console.log('Attempting to create...'); 433 | } 434 | ); 435 | } 436 | ); 437 | } 438 | 439 | 440 | function uploadFiles(viewDataClient, bucket, files) { 441 | for (var i = 0; i < files.length; ++i) { 442 | var file = files[i]; 443 | console.log('Uploading file: ' + file.name + ' ...'); 444 | viewDataClient.uploadFileAsync( 445 | file, 446 | bucket, 447 | file.name.replace(/ /g,'_'), // Translation API cannot handle spaces... 448 | function(response) { 449 | //onSuccess 450 | console.log('File upload successful:'); 451 | console.log(response); 452 | var fileId = response.objects[0].id; 453 | var registerResponse = viewDataClient.register(fileId); 454 | 455 | if (registerResponse.Result === 'Success' || 456 | registerResponse.Result === 'Created') { 457 | console.log('Registration result: ' + registerResponse.Result); 458 | console.log('Starting translation: ' + fileId); 459 | 460 | checkTranslationStatus( 461 | viewDataClient, 462 | fileId, 463 | 1000 * 60 * 5, //5 mins timeout 464 | function(viewable) { 465 | //onSuccess 466 | console.log('Translation successful: ' + response.file.name); 467 | console.log('Viewable: '); 468 | console.log(viewable); 469 | 470 | var urn = viewable.urn; 471 | 472 | // add new button 473 | var panel = document.getElementById('control'); 474 | var name = truncateName(response.file.name); 475 | addButton(panel, name, function(urn) { return function() { launchUrn(urn); } }(urn)); 476 | 477 | // open it in a viewer 478 | launchUrn(urn); 479 | 480 | // and store as a cookie 481 | createCookieForCustomModel('custom_model_' + response.file.name, urn); 482 | }); 483 | } 484 | }, 485 | 486 | //onError 487 | function (error) { 488 | console.log('File upload failed:'); 489 | console.log(error); 490 | }); 491 | } 492 | } 493 | 494 | 495 | function checkTranslationStatus(viewDataClient, fileId, timeout, onSuccess) { 496 | var startTime = new Date().getTime(); 497 | var timer = setInterval(function() { 498 | var dt = (new Date().getTime() - startTime) / timeout; 499 | if (dt >= 1.0) { 500 | clearInterval(timer); 501 | } else { 502 | viewDataClient.getViewableAsync( 503 | fileId, 504 | function(response) { 505 | console.log(response); 506 | console.log('Translation Progress ' + fileId + ': ' + response.progress); 507 | $('#upload-button').html(response.progress); 508 | 509 | if (response.progress === 'complete') { 510 | clearInterval(timer); 511 | onSuccess(response); 512 | $('#upload-button').html('Upload file'); 513 | } 514 | }, 515 | function(error) {} 516 | ); 517 | } 518 | }, 2000); 519 | }; 520 | 521 | 522 | // 523 | // Models stored in cookies 524 | // 525 | 526 | function truncateName(name) { 527 | var dotIdx = name.lastIndexOf("."); 528 | if (dotIdx != -1) { 529 | var name = name.substring(0, dotIdx); 530 | 531 | if (name.length > 8) { 532 | name = name.substring(0, 8) + "..."; 533 | } 534 | } 535 | 536 | return name; 537 | } 538 | 539 | 540 | function createCookieForCustomModel(name, value, days) { 541 | if (days) { 542 | var date = new Date(); 543 | date.setTime(date.getTime() + (days*24*60*60*1000)); 544 | var expires = '; expires=' + date.toGMTString(); 545 | } else { 546 | var expires = ''; 547 | } 548 | 549 | var urn = encodeURIComponent(value); 550 | document.cookie = name + '=' + urn + expires + '; path=/'; 551 | } 552 | 553 | 554 | function readCookiesForCustomModel() { 555 | var prefix = 'custom_model_'; 556 | var cookies = document.cookie.split(';'); 557 | 558 | for (var i in cookies) { 559 | var c = cookies[i]; 560 | if (c.indexOf(prefix) != -1) { 561 | c = c.replace(prefix, ''); 562 | var nameValue = c.split('='); 563 | if (nameValue) { 564 | var panel = document.getElementById('control'); 565 | addButton(panel, truncateName(nameValue[0]), function(urn) { 566 | return function() { launchUrn(urn); } 567 | }(decodeURIComponent(nameValue[1]))); 568 | } 569 | } 570 | } 571 | } 572 | 573 | 574 | function showAbout() { 575 | $('#aboutDiv').css('text-indent', 0); 576 | resetSize($('#layer2')[0], true); 577 | $('#3dViewDiv').hide(); 578 | $('#aboutDiv').show(); 579 | } 580 | 581 | 582 | // Prevent resize from being called too frequently 583 | // (might want to adjust the timeout from 50ms) 584 | var resize = debounce(function() { 585 | var div = $('#3dViewDiv'); 586 | var viewing = div.is(':visible'); 587 | resetSize(viewing ? _viewer.container : $('#layer2')[0], !viewing); 588 | }, 50); -------------------------------------------------------------------------------- /html/res/locales/en/allstrings.json: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "@@context": "Viewer", 4 | "@@contact": "Federico Medina", 5 | "ATF-1008": "Error: {0}", 6 | "ATF-1023": "The referenced file {0} is missing.", 7 | "ATF-1024": "The version of the file: {0} is not supported.", 8 | "ATF-1025": "The file: {0} is corrupt or invalid.", 9 | "ATF-1026": "The file: {0} is empty.", 10 | "ATF-1028": "Fail to load component: {0}.", 11 | "ATF-1031": "Unauthorized file type.", 12 | "ATF-1032": "Assembly feature is lost.", 13 | "ATF-1033": "Entity in the file: {0} is not supported.", 14 | "ATF-1034": "The file name: {0} is invalid.", 15 | "AssImp-OpenFailure": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 16 | "AssImp-InternalError": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 17 | "AssImp-CannotCreateOutput": "Cannot write output file.", 18 | "AssImp-CannotLoadOBJ": "Cannot load OBJ file.", 19 | "AssImp-MissingTextures": "Missing texture image file:
    {0}
", 20 | "AutoCAD-MissingReferences": "Missing referencesSome references were not uploaded, so the drawing may not be fully displayed.Please upload the composite design with all the missing referenced files:
    {0}
", 21 | "AutoCAD-MissingReference": "Missing referenceOne reference was not uploaded, so the drawing may not be fully displayed.Please upload the composite design with the missing referenced file: {0}", 22 | "AutoCAD-NoDWFxFilesGenerated": "The drawing may not contain visible objects. No views have been generated.", 23 | "AutoCAD-PasswordProtectedFile": "Sorry!This item can't be viewed because it's password-protected.Please remove the password, and upload it again to view.", 24 | "AutoCAD-InvalidFile": "Sorry!The drawing file is invalid and cannot be viewed.Please try to recover the file in AutoCAD, and upload it again to view.", 25 | "AutoCAD-ProxyGraphicsFalse": "The drawing contains custom objects that are not displayed in the viewer.", 26 | "AutoCAD-ProxyGraphicsTrue": "The drawing contains custom objects. Some properties of custom objects may not be displayed or searched.", 27 | "AutoCAD-ClippedView": "The 2D model view is clipped to the last saved view and may not display the entire drawing.Resave the drawing in AutoCAD with the desired view and upload again if the drawing is clipped.", 28 | "AutoCAD-MissingShxFonts": "Missing fontsSome missing fonts were substituted for the drawing file: {0}", 29 | "AutoCAD-MissingTtfFonts": "Missing fontsSome missing fonts were substituted for the drawing file: {0}", 30 | "AutoCAD-MissingPlotStyleFiles": "Missing plot style files: {0}", 31 | "DWF2D-Corrupt_File_Error": "Unable to open corrupt DWF/DWFx file.", 32 | "DWF2D-Building_XML_Object_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 33 | "DWF2D-File_Not_In_DWF": "Unable to open file, expected resource is not present in DWF/DWFx file contents.", 34 | "DWF2D-File_Open_Error": "Unable to open DWF/DWFx file.", 35 | "DWF2D-File_Read_Error": "Unable to read DWF/DWFx file.", 36 | "DWF2D-File_Write_Error": "Unable to write DWF/DWFx file.", 37 | "DWF2D-Internal_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 38 | "DWF2D-Invalid_Argument_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 39 | "DWF2D-Invalid_XML_Content_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 40 | "DWF2D-Invalid_DWF_Version_Error": "Invalid DWF/DWFx file version.", 41 | "DWF2D-Not_A_DWF_Error": "Unable to open, not a DWF/DWFx file.", 42 | "DWF2D-Open_Section_Descriptor_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 43 | "DWF2D-Out_Of_Memory_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 44 | "DWF2D-Stream_Write_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 45 | "DWF2D-Temp_File_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 46 | "DWF2D-Toolkit_Usage_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 47 | "DWF2D-ZipLib_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 48 | "DWF2D-Bad_Password_Error": "Unable to open password protected file.", 49 | "DWF2D-Resource_Handler_Not_Specified_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 50 | "DWF2D-Not_Implemented": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 51 | "DWF2D-Stream_In_Progress": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 52 | "DWF2D-User_Requested_Abort": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 53 | "DWF2D-Unsupported_File_Type_Error": "Unable to open, not a valid DWF/DWFx file.", 54 | "DWF2D-Temp_Path_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 55 | "DWF2D-Incompatable_XML_Version_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 56 | "DWF2D-Incompatible_Section_Type": "Unknown section resource processing DWF/DWFx file.", 57 | "DWF2D-Drm_Encryption": "Unable to open DRM / rights-protected file.", 58 | "DWF2D-Load_Section_Error": "Unable to load section resource in DWF/DWFx file.", 59 | "DWF2D-Secured_Section_Type": "Unable to open DRM / rights-protected file.", 60 | "DWF2D-Unknown_Error": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 61 | "DWF3D-BadPassword": "Could not open password-protected DWF file.", 62 | "DWF3D-CorruptFile": "Could not open DWF file.The data appears corrupted.", 63 | "DWF3D-InvalidVersion": "Could not open DWF file.Unsupported version.", 64 | "DWF3D-DRMFailure": "Could not open DWF file.File contains DRM protected content.", 65 | "Navisworks-2DSheetUnsupported": "Navisworks 2D sheet is not supported.", 66 | "Navisworks-MissingXref": "Missing external reference file:
    {0}
", 67 | "Navisworks-EmptyFile": "There is no geometry that can be displayed.", 68 | "Navisworks-PointUnsupported": "Navisworks point and snappoint are not supported.", 69 | "Navisworks-Failed": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 70 | "Revit-UnsupportedFileType": "The file is not a Revit file or is not a supported version.", 71 | "Revit-MissingLink": "Missing link files:
    {0}
", 72 | "Revit-UpgradeWarning": "Removed some elements to resolve issues encountered during upgrade. Please note that Revit file may not view as expected.For best results, open the file in Revit, resolve warnings, re-save it and try again.", 73 | "Revit-UpgradeError": "Failed to upgrade the project.Please upgrade the project using the current version of Revit and try again.", 74 | "Revit-FileOpenError": "Failed to open the file.Please open the file in the current version of Revit, resolve warnings and errors, re-save it, and try again.", 75 | "Revit-Timeout": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 76 | "Revit-MassVisibilitySettingFailure": "Failed to export the mass in 3D view.", 77 | "Revit-View3DExportError": "Failed to export the 3D view - {0}.", 78 | "Revit-View2DExportError": "Failed to export the 2D view - {0}.", 79 | "Revit-ThumbnailGenerationError": "Failed to generate the preview image for the view - {0}.", 80 | "Revit-DocumentCorruption": "The file is corrupt.Please open the file in the current version of Revit to see more details about the corruption.", 81 | "Revit-InternalError": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 82 | "TranslationWorker-TextureResizingFailed": "The drawing’s textures were not correctly processed.", 83 | "TranslationWorker-ThumbnailGenerationFailed": "The drawing’s thumbnails were not properly created.", 84 | "TranslationWorker-InternalFailure": "We have encountered some issues while preparing the file for viewing. Please contact support for assistance.", 85 | "TranslationWorker-InsufficientDataFromExtractor": "Not enough drawing information was produced to make this file viewable. Contact support for further assistance.", 86 | "TranslationWorker-RecoverableInternalFailure": "An error occurred while processing this file. It may succeed if processing is attempted again later.", 87 | "Play": "Play", 88 | "Pause": "Pause", 89 | "Next keyframe": "Next keyframe", 90 | "Previous keyframe": "Previous keyframe", 91 | "Click-drag to scrub": "Click-drag to scrub", 92 | "Close animation timeline": "Close animation timeline", 93 | "Live review": "Live review", 94 | "Start a Live Review": "Start a Live Review", 95 | "Start Review": "Start Review", 96 | "Join a Live Review": "Join a Live Review", 97 | "Join Review": "Join Review", 98 | "Invite": "Invite", 99 | "Please Join My Live Review": "Please Join My Live Review", 100 | "Invite Others": "Invite Others", 101 | "Email Invite": "Email Invite", 102 | "Type a message": "Type a message...", 103 | "End video call": "End video call", 104 | "Enter your name": "Enter your name:", 105 | "Review URL": "Review URL", 106 | "Copy and send this URL to invite others": "Copy and send this URL to invite others", 107 | "you": "you", 108 | "Components": "Components", 109 | "Sheets": "Sheets", 110 | "Riverbank": "Riverbank", 111 | "Simple Grey": "Simple Grey", 112 | "Grey Room": "Grey Room", 113 | "Photo Booth": "Photo Booth", 114 | "Sharp Highlights": "Sharp Highlights", 115 | "Contrast": "Contrast", 116 | "Infinity Pool": "Infinity Pool", 117 | "Tranquility": "Tranquility", 118 | "Dark Sky": "Dark Sky", 119 | "Simple White": "Simple White", 120 | "Rim Highlights": "Rim Highlights", 121 | "Cool Light": "Cool Light", 122 | "Warm Light": "Warm Light", 123 | "Soft Light": "Soft Light", 124 | "Grid Light": "Grid Light", 125 | "Plaza": "Plaza", 126 | "Snow Field": "Snow Field", 127 | "Field": "Field", 128 | "Crossroads": "Crossroads", 129 | "Seaport": "Seaport", 130 | "Glacier": "Glacier", 131 | "Boardwalk": "Boardwalk", 132 | "RaaS Test Env": "RaaS Test Env", 133 | "header-sorry": "Sorry", 134 | "header-warning": "Warning", 135 | "Error Occurred": "Error Occurred", 136 | "OK": "OK", 137 | "Viewer-UnknownFailure": "Sorry We seem to have some technical difficulties and couldn’t complete your request. Try loading the item again. Please verify your Internet connection, and refresh the browser to see if that fixes the problem.", 138 | "Viewer-BadData": "SorryThe item you are trying to view was not processed completely. Try loading the item again. Please upload the file again to see if that fixes the issue.", 139 | "Viewer-NetworkError": "SorryWe seem to have some technical difficulties and couldn’t complete your request. Try loading the item again. Please verify your Internet connection, and refresh the browser to see if that fixes the problem.", 140 | "Viewer-AccessDenied": "No access Sorry. You don’t have the required privileges to access this item. Please contact the author.", 141 | "Viewer-FileNotFound": "SorryWe can’t display the item you are looking for. It may not have been processed yet. It may have been moved, deleted, or you may be using a corrupt file or unsupported file format. Try loading the item again. Please upload the file again to see if that fixes the issue. See a list of supported formats.", 142 | "Viewer-ServerError": "SorryWe seem to have some technical difficulties and couldn’t complete your request Try loading the item again. Please verify your Internet connection, and refresh the browser to see if that fixes the problem.", 143 | "Viewer-UnhandledResponseCode": "Network problem Sorry. We seem to have some technical difficulties and couldn’t complete your request. Try loading the item again. Please verify your Internet connection, and refresh the browser to see if that fixes the problem.", 144 | "Viewer-WebGlNotSupported": "SorryWe can't show this item because this browser doesn’t support WebGL.Try Google Chrome, Mozilla Firefox, or another browser that supports WebGL 3D graphics.For more information, see the A360 browser requirements.", 145 | "Viewer-WebGlDisabled": "SorryWe can't show this item because WebGL is disabled on this device. For more information see the A360 Help.", 146 | "Viewer-NoViewable": "No viewable contentThere’s nothing to display for this item. It may not have been processed or it may not have content we can display. Please contact the author. Please upload the file again to see if that fixes the issue.", 147 | "Viewer-RTCError": " Sorry We couldn’t connect to the Collaboration server. Please verify your Internet connection, and refresh the browser to see if that fixes the problem.", 148 | "UNKNOWN FAILURE": "Unknown Failure", 149 | "BAD DATA": "Bad Data", 150 | "NETWORK ERROR": "Network Error", 151 | "NETWORK ACCESS DENIED": "Network Access Denied", 152 | "NETWORK FILE NOT FOUND": "Network File Not Found", 153 | "NETWORK SERVER ERROR": "Network Server Error", 154 | "NETWORK UNHANDLED RESPONSE CODE": "Network Unhandled Response Code", 155 | "BROWSER WEBGL NOT SUPPORTED": "Browser WebGL Not Supported", 156 | "BAD DATA NO VIEWABLE CONTENT": "Bad Data No Viewable Content", 157 | "Network failure": "Network failure", 158 | "Access denied to remote resource": "Access denied to remote resource", 159 | "Remote resource not found": "Remote resource not found", 160 | "Server error when accessing resource": "Server error when accessing resource", 161 | "Unhandled response code from server": "Unhandled response code from server", 162 | "Malformed data received when requesting file": "Malformed data received when requesting file", 163 | "Malicious document content detected Abort loading": "Malicious document content detected. Abort loading.", 164 | "No viewable content": "No viewable content", 165 | "An exception occurred while loading the document": "An exception occurred while loading the document", 166 | "Properties failed to load since model does not exist": "Properties failed to load since model does not exist", 167 | "ObjectTree failed to load since model does not exist": "ObjectTree failed to load since model does not exist", 168 | "Search failed since model does not exist": "Search failed since model does not exist", 169 | "Function getAttributeToIdMap failed": "Function getAttributeToIdMap failed", 170 | "Viewer failed to initialize": "Viewer failed to initialize", 171 | "Unhandled exception while reading pack file": "Unhandled exception while reading pack file", 172 | "Unhandled exception while reading geometry": "Unhandled exception while reading geometry", 173 | "Unable to load geometry": "Unable to load geometry", 174 | "Failed to load properties": "Failed to load properties", 175 | "Unhandled exception while loading SVF": "Unhandled exception while loading SVF", 176 | "Failure while loading SVF": "Failure while loading SVF", 177 | "First Person Tool": "First Person Tool", 178 | "Use the WASD and QE keys to move": "Use the WASD and QE keys to move. Shift+Key to move faster. G key toggles view orientation drag mode. F1 to view this help.", 179 | "View Orientation Drag Mode Toggled": "View Orientation Drag Mode Toggled", 180 | "Press the primary mouse button and drag to change the view orientation": "Press the primary mouse button and drag to change the view orientation.", 181 | "Move the cursor to change the view orientation": "Move the cursor to change the view orientation.", 182 | "Virtual Reality Tool": "Virtual Reality Tool", 183 | "This is the Virtual Reality tool": "This is the Virtual Reality tool.", 184 | "VR Toggle Move": "VR Toggle Move", 185 | "START Auto Move": "START Auto Move", 186 | "STOP Auto Move": "STOP Auto Move", 187 | "VR Device Orientation": "VR Device Orientation", 188 | "VR Mode Enabled Device Orientation": "VR Mode Enabled (Device Orientation)", 189 | "VR Head Mounted Display": "VR Head Mounted Display", 190 | "VR Mode Enabled Head Mounted Display": "VR Mode Enabled (Head Mounted Display) - Use SPACE key to toggle move control.", 191 | "Measure": "Measure", 192 | "Select Object": "Select Object", 193 | "Vertex": "Vertex", 194 | "Edge": "Edge", 195 | "Face": "Face", 196 | "Delta": "Delta", 197 | "Unit type": "Unit type", 198 | "Decimal feet": "Decimal feet", 199 | "Feet and fractional inches": "Feet and fractional inches", 200 | "Feet and decimal inches": "Feet and decimal inches", 201 | "Decimal inches": "Decimal inches", 202 | "Fractional inches": "Fractional inches", 203 | "Meters": "Meters", 204 | "Centimeters": "Centimeters", 205 | "Millimeters": "Millimeters", 206 | "Meters and centimeters": "Meters and centimeters", 207 | "Unknown": "Unknown", 208 | "Precision": "Precision", 209 | "Isolate measurement": "Isolate measurement", 210 | "Restart": "Restart", 211 | "INITIATING DIRECT LINK": "INITIATING DIRECT LINK...", 212 | "CONTACTING MOTHERSHIP": "CONTACTING MOTHERSHIP...", 213 | "DISCONNECTED": "DISCONNECTED!", 214 | "Reload main viewer and pair again": "Reload main viewer and pair again.", 215 | "Go to this link": "Go to this link:", 216 | "Scan the QR code with your device": "Scan the QR code with your device:", 217 | "Rendering Options": "Rendering Options", 218 | "Background Color": "Background color:", 219 | "AO Enabled": "AO Enabled:", 220 | "@AO Enabled": { 221 | "description": "AO means Ambient Occlusion. Abbreviated to save space" 222 | }, 223 | "AO Radius": "AO Radius:", 224 | "@AO Radius": { 225 | "description": "AO means Ambient Occlusion. Abbreviated to save space" 226 | }, 227 | "AO Intensity": "AO Intensity:", 228 | "@AO Intensity": { 229 | "description": "AO means Ambient Occlusion. Abbreviated to save space" 230 | }, 231 | "Shadow Alpha": "Shadow Alpha:", 232 | "Shadow Color": "Shadow Color:", 233 | "Reflection Alpha": "Reflection Alpha:", 234 | "Reflection Color": "Reflection Color:", 235 | "Environment": "Environment:", 236 | "Tonemap Method": "Tonemap method:", 237 | "Exposure Bias": "Exposure bias:", 238 | "Light Intensity": "Light Intensity:", 239 | "FOV-degrees": "FOV (degrees):", 240 | "@FOV-degrees": { 241 | "description": "FOV is abbreviation for 'field of view angle'." 242 | }, 243 | "None": "None", 244 | "Canon-RGB": "Canon (RGB)", 245 | "Canon-Lum": "Canon (Lum)", 246 | "Section analysis": "Section analysis", 247 | "Add X plane": "Add X plane", 248 | "Add Y plane": "Add Y plane", 249 | "Add Z plane": "Add Z plane", 250 | "Add box": "Add box", 251 | "Settings": "Settings", 252 | "Navigation and selection": "Navigation and selection", 253 | "Performance and appearance": "Performance and appearance", 254 | "Show ViewCube": "Show ViewCube", 255 | "ViewCube acts on pivot": "ViewCube acts on pivot", 256 | "Reverse mouse zoom direction": "Reverse mouse zoom direction", 257 | "Reverse horizontal look direction": "Reverse horizontal look direction", 258 | "Reverse vertical look direction": "Reverse vertical look direction", 259 | "Zoom towards pivot": "Zoom toward pivot", 260 | "Orbit past world poles": "Orbit past world poles", 261 | "Set pivot with left mouse button": "Set pivot with left mouse button", 262 | "Fusion style orbit": "Fusion-style orbit", 263 | "First person walk": "First person walk", 264 | "Open properties on select": "Open properties on select", 265 | "Left handed mouse setup": "Left-handed mouse setup", 266 | "Ghost hidden objects": "Ghost hidden objects", 267 | "Smooth navigation": "Smooth navigation", 268 | "Ambient shadows": "Ambient shadows", 269 | "Anti-aliasing": "Anti-aliasing", 270 | "Ground shadow": "Ground shadow", 271 | "Ground reflection": "Ground reflection", 272 | "Progressive model display": "Progressive model display", 273 | "Environment map for background": "Environment map for background", 274 | "Restore default settings": "Restore default settings", 275 | "Simulation": "Simulation", 276 | "Simulation Results": "Simulation Results", 277 | "Simulation Model Structure Loading": "Simulation Model Structure Loading", 278 | "Static Stress": "Static Stress", 279 | "Modal Frequencies": "Modal Frequencies", 280 | "Analysis Type": "Analysis Type", 281 | "Nodes": "Nodes", 282 | "Elements": "Elements", 283 | "Min": "Min", 284 | "Max": "Max", 285 | "Safety Factor": "Safety Factor", 286 | "Stress": "Stress", 287 | "Strain": "Strain", 288 | "Von Mises": "Von Mises", 289 | "Per Body": "Per Body", 290 | "Total": "Total", 291 | "Equivalent": "Equivalent", 292 | "Displacement": "Displacement", 293 | "Constraints": "Constraints", 294 | "Loads": "Loads", 295 | "Total Modal Displacement": "Total Modal Displacement", 296 | "Name": "Name", 297 | "Appearance": "Appearance", 298 | "Status": "Status", 299 | "Attached": "Attached", 300 | "Type": "Type", 301 | "Magnitude": "Magnitude", 302 | "Simulation Setup": "Simulation Setup", 303 | "Simulation Setup Loading": "Simulation Setup Loading", 304 | "Orbit": "Orbit", 305 | "Free orbit": "Free orbit", 306 | "Pan": "Pan", 307 | "Zoom": "Zoom", 308 | "Walk to": "Walk to (double-click to Walk through)", 309 | "Camera interactions": "Camera interactions", 310 | "Fit to view": "Fit to view (F)", 311 | "Focal length": "Focal length (Ctrl+Shift drag)", 312 | "Focal Length": "Focal length", 313 | "Roll": "Roll (Alt+Shift drag)", 314 | "Model structure": "Model structure", 315 | "Explode model": "Explode model", 316 | "Layer Manager": "Layer manager", 317 | "Reset model": "Reset model", 318 | "Reset drawing": "Reset drawing", 319 | "Properties": "Properties", 320 | "Comments and Markers": "Comments and markers", 321 | "Rendering options": "Rendering options", 322 | "Full screen": "Full screen", 323 | "Exit full screen": "Exit full screen", 324 | "Layers": "Layers", 325 | "Enter filter term": "Enter filter term", 326 | "Show/hide all layers": "Show/hide all layers", 327 | "Show/hide this layer": "Show/hide this layer", 328 | "Background and lighting": "Background and lighting", 329 | "Display Lines": "Display lines", 330 | "Performance and appearance settings": "Performance and appearance settings", 331 | "Navigation and selection settings": "Navigation and selection settings", 332 | "RaaS": "RaaS", 333 | "@RaaS": { 334 | "description": "RaaS stands for Rendering as a Service" 335 | }, 336 | "Gallery": "Gallery", 337 | "FPS statistics": "FPS statistics", 338 | "@FPS statistics": { 339 | "description": "FPS stands for Frames Per Seconds" 340 | }, 341 | "Model statistics": "Model statistics", 342 | "Show search bar": "Show search bar", 343 | "Search": "Search", 344 | "Language": "Language", 345 | "Error": "Error", 346 | "Search the currently displayed content": "Search the currently displayed content", 347 | "Comment and Markers": "Comment and markers", 348 | "Comment": "Comment", 349 | "Post": "Post", 350 | "Hide Markers": "Hide markers", 351 | "Show Markers:": "Show markers:", 352 | "Orthographic View Set": "Orthographic View Set", 353 | "The view is set to Orthographic": "The view is set to Orthographic. Changing the focal length will switch to Perspective.", 354 | "Do not show this message again": "Do not show this message again", 355 | "The view is set to Orthographic Beeline": "The view is set to Orthographic. Using this tool will switch to Perspective.", 356 | "Long Focal Length View Set": "Long Focal Length View Set", 357 | "The view is set to a long focal length": "This view has a long focal length. Using this tool will set a short focal length.", 358 | "Go Home": "Go Home", 359 | "Orthographic": "Orthographic", 360 | "Perspective": "Perspective", 361 | "Perspective with Ortho Faces": "Perspective with Ortho Faces", 362 | "Set current view as Home": "Set current view as Home", 363 | "Focus and set as Home": "Focus and set as Home", 364 | "Reset Home": "Reset Home", 365 | "Set current view as Front": "Set current view as Front", 366 | "Set current view as Top": "Set current view as Top", 367 | "Reset orientation": "Reset orientation", 368 | "TOP": "TOP", 369 | "BOTTOM": "BOTTOM", 370 | "FRONT": "FRONT", 371 | "RIGHT": "RIGHT", 372 | "LEFT": "LEFT", 373 | "BACK": "BACK", 374 | "Isolate": "Isolate", 375 | "Hide Selected": "Hide selected", 376 | "Show Selected": "Show selected", 377 | "Show All Objects": "Show all objects", 378 | "Show All Layers": "Show all layers", 379 | "Focus": "Focus", 380 | "Clear Selection": "Clear selection", 381 | "Model browser": "Model browser", 382 | "Browser": "Browser", 383 | "Filter by name": "Filter by name", 384 | "Object Properties": "Object properties", 385 | "Object Properties Loading...": "Object properties loading...", 386 | "No properties to display": "No properties to display", 387 | "Other": "Other", 388 | "Model Properties": "Model properties", 389 | "Yes": "Yes", 390 | "No": "No", 391 | "English": "English", 392 | "Russian": "Russian", 393 | "Japanese": "Japanese", 394 | "Korean": "Korean", 395 | "Spanish": "Spanish", 396 | "Chinese Simplified": "Chinese Simplified", 397 | "Chinese Traditional": "Chinese Traditional", 398 | "Czech": "Czech", 399 | "Polish": "Polish", 400 | "French": "French", 401 | "German": "German", 402 | "Italian": "Italian", 403 | "Portuguese Brazil": "Portuguese Brazil", 404 | "Turkish": "Turkish" 405 | } --------------------------------------------------------------------------------