114 | For demonstration purposes, objects (files)
115 | are NOT automatically translated. After you upload, right click on
116 | the object and select "Translate". Note: Technically your bucket name is required to be globally unique across
117 | the entire platform - to keep things simple with this tutorial your client ID will be prepended by default to
118 | your bucket name and in turn masked by the UI so you only have to make sure your bucket name is unique within
119 | your current Forge app.
120 |
121 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
137 |
Edit Extension Options
138 |
139 |
140 |
141 |
142 |
143 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/public/js/ForgeTree.js:
--------------------------------------------------------------------------------
1 | /////////////////////////////////////////////////////////////////////
2 | // Copyright (c) Autodesk, Inc. All rights reserved
3 | // Written by Forge Partner Development
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 | $(document).ready(function () {
20 | prepareAppBucketTree();
21 | $('#refreshBuckets').click(function () {
22 | $('#appBuckets').jstree(true).refresh();
23 | });
24 |
25 | $('#createNewBucket').click(function () {
26 | createNewBucket();
27 | });
28 |
29 | $('#createBucketModal').on('shown.bs.modal', function () {
30 | $("#newBucketKey").focus();
31 | })
32 |
33 | $('#hiddenUploadField').change(function () {
34 | var node = $('#appBuckets').jstree(true).get_selected(true)[0];
35 | var _this = this;
36 | if (_this.files.length == 0) return;
37 | var file = _this.files[0];
38 | switch (node.type) {
39 | case 'bucket':
40 | var formData = new FormData();
41 | formData.append('fileToUpload', file);
42 | formData.append('bucketKey', node.id);
43 |
44 | $.ajax({
45 | url: '/api/forge/oss/upload',
46 | data: formData,
47 | processData: false,
48 | contentType: false,
49 | type: 'POST',
50 | success: function (data) {
51 | $('#appBuckets').jstree(true).refresh_node(node);
52 | _this.value = '';
53 | }
54 | });
55 | break;
56 | }
57 | });
58 | });
59 |
60 | function createNewBucket() {
61 | var bucketKey = $('#newBucketKey').val();
62 | var policyKey = $('#newBucketPolicyKey').val();
63 | jQuery.post({
64 | url: '/api/forge/oss/buckets',
65 | contentType: 'application/json',
66 | data: JSON.stringify({ 'bucketKey': bucketKey, 'policyKey': policyKey }),
67 | success: function (res) {
68 | $('#appBuckets').jstree(true).refresh();
69 | $('#createBucketModal').modal('toggle');
70 | },
71 | error: function (err) {
72 | if (err.status == 409)
73 | alert('Bucket already exists - 409: Duplicated')
74 | console.log(err);
75 | }
76 | });
77 | }
78 | var extensionloaded = false;
79 | function prepareAppBucketTree() {
80 | $('#appBuckets').jstree({
81 | 'core': {
82 | 'themes': { "icons": true },
83 | 'data': {
84 | "url": '/api/forge/oss/buckets',
85 | "dataType": "json",
86 | 'multiple': false,
87 | "data": function (node) {
88 | return { "id": node.id };
89 | }
90 | }
91 | },
92 | 'types': {
93 | 'default': {
94 | 'icon': 'glyphicon glyphicon-question-sign'
95 | },
96 | '#': {
97 | 'icon': 'glyphicon glyphicon-cloud'
98 | },
99 | 'bucket': {
100 | 'icon': 'glyphicon glyphicon-folder-open'
101 | },
102 | 'object': {
103 | 'icon': 'glyphicon glyphicon-file'
104 | }
105 | },
106 | "plugins": ["types", "state", "sort", "contextmenu"],
107 | contextmenu: { items: autodeskCustomMenu }
108 | }).on('loaded.jstree', function () {
109 | $('#appBuckets').jstree('open_all');
110 | }).bind("activate_node.jstree", function (evt, data) {
111 | if (data != null && data.node != null && data.node.type == 'object') {
112 | $("#forgeViewer").empty();
113 | var urn = data.node.id;
114 | var filename = data.node.text
115 | document.getElementsByClassName('tobegin')[0].style.display = 'none';
116 | getForgeToken(function (access_token) {
117 | jQuery.ajax({
118 | url: 'https://developer.api.autodesk.com/modelderivative/v2/designdata/' + urn + '/manifest',
119 | headers: { 'Authorization': 'Bearer ' + access_token },
120 | success: function (res) {
121 | if (res.progress === 'success' || res.progress === 'complete') launchViewer(urn,filename);
122 | else $("#forgeViewer").html('The translation job still running: ' + res.progress + '. Please try again in a moment.');
123 | },
124 | error: function (err) {
125 | var msgButton = 'This file is not translated yet! ' +
126 | ''
128 | $("#forgeViewer").html(msgButton);
129 | }
130 | });
131 | })
132 | }
133 | }).on('ready.jstree', function () {
134 | if (!extensionloaded) {
135 | const queryString = window.location.search;
136 | const urlParams = new URLSearchParams(queryString);
137 | let extension = urlParams.get('extension');
138 | if (extension) {
139 | document.getElementById('dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6c2FtcGxlbW9kZWxzL09mZmljZS5ydnQ=_anchor').click();
140 | window.extension = extension;
141 | }
142 | extensionloaded = true;
143 | }
144 | });
145 | }
146 |
147 | function autodeskCustomMenu(autodeskNode) {
148 | var items;
149 |
150 | switch (autodeskNode.type) {
151 | case "bucket":
152 | if(autodeskNode.id === "samplemodels"){
153 | alert('upload in the below transient bucket');
154 | break;
155 | }
156 | items = {
157 | uploadFile: {
158 | label: "Upload file",
159 | action: function () {
160 | uploadFile();
161 | },
162 | icon: 'glyphicon glyphicon-cloud-upload'
163 | }
164 | };
165 | break;
166 | case "object":
167 | items = {
168 | translateFile: {
169 | label: "Translate",
170 | action: function () {
171 | var treeNode = $('#appBuckets').jstree(true).get_selected(true)[0];
172 | translateObject(treeNode);
173 | },
174 | icon: 'glyphicon glyphicon-eye-open'
175 | }
176 | };
177 | break;
178 | }
179 |
180 | return items;
181 | }
182 |
183 | function uploadFile() {
184 | $('#hiddenUploadField').click();
185 | }
186 |
187 | function translateObject(node) {
188 | $("#forgeViewer").empty();
189 | if (node == null) node = $('#appBuckets').jstree(true).get_selected(true)[0];
190 | var bucketKey = node.parents[0];
191 | var objectKey = node.id;
192 | jQuery.post({
193 | url: '/api/forge/modelderivative/jobs',
194 | contentType: 'application/json',
195 | data: JSON.stringify({ 'bucketKey': bucketKey, 'objectName': objectKey }),
196 | success: function (res) {
197 | $("#forgeViewer").html('Translation started! Please try again in a moment.');
198 | },
199 | });
200 | }
--------------------------------------------------------------------------------
/public/js/ForgeViewer.js:
--------------------------------------------------------------------------------
1 | /////////////////////////////////////////////////////////////////////
2 | // Copyright (c) Autodesk, Inc. All rights reserved
3 | // Written by Forge Partner Development
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 viewer;
20 | var fileName;
21 |
22 | function launchViewer(urn, name) {
23 | var options = {
24 | env: 'AutodeskProduction',
25 | getAccessToken: getForgeToken
26 | };
27 |
28 | fileName = name;
29 |
30 | Autodesk.Viewing.Initializer(options, () => {
31 | viewer = new Autodesk.Viewing.GuiViewer3D(document.getElementById('forgeViewer'));
32 | viewer.start();
33 | var documentId = 'urn:' + urn;
34 | Autodesk.Viewing.Document.load(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
35 | });
36 | }
37 |
38 | function onDocumentLoadSuccess(doc) {
39 | var viewables = doc.getRoot().getDefaultGeometry();
40 | viewer.loadDocumentNode(doc, viewables).then(i => {
41 | // documented loaded, any action?
42 | var ViewerInstance = new CustomEvent("viewerinstance", {detail: {viewer: viewer}});
43 | document.dispatchEvent(ViewerInstance);
44 | // var LoadExtensionEvent = new CustomEvent("loadextension", {
45 | // detail: {
46 | // extension: "Extension1",
47 | // viewer: viewer
48 | // }
49 | // });
50 | // document.dispatchEvent(LoadExtensionEvent);
51 | });
52 | }
53 |
54 | function onDocumentLoadFailure(viewerErrorCode) {
55 | console.error('onDocumentLoadFailure() - errorCode:' + viewerErrorCode);
56 | }
57 |
58 | function getForgeToken(callback) {
59 | fetch('/api/forge/oauth/token').then(res => {
60 | res.json().then(data => {
61 | callback(data.access_token, data.expires_in);
62 | });
63 | });
64 | }
--------------------------------------------------------------------------------
/routes/common/oauth.js:
--------------------------------------------------------------------------------
1 | /////////////////////////////////////////////////////////////////////
2 | // Copyright (c) Autodesk, Inc. All rights reserved
3 | // Written by Forge Partner Development
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 | const { AuthClientTwoLegged } = require('forge-apis');
20 |
21 | const config = require('../../config');
22 |
23 | /**
24 | * Initializes a Forge client for 2-legged authentication.
25 | * @param {string[]} scopes List of resource access scopes.
26 | * @returns {AuthClientTwoLegged} 2-legged authentication client.
27 | */
28 | function getClient(scopes) {
29 | const { client_id, client_secret } = config.credentials;
30 | return new AuthClientTwoLegged(client_id, client_secret, scopes || config.scopes.internal);
31 | }
32 |
33 | let cache = {};
34 | async function getToken(scopes) {
35 | const key = scopes.join('+');
36 | if (cache[key]) {
37 | return cache[key];
38 | }
39 | const client = getClient(scopes);
40 | let credentials = await client.authenticate();
41 | cache[key] = credentials;
42 | setTimeout(() => { delete cache[key]; }, credentials.expires_in * 1000);
43 | return credentials;
44 | }
45 |
46 | /**
47 | * Retrieves a 2-legged authentication token for preconfigured public scopes.
48 | * @returns Token object: { "access_token": "...", "expires_at": "...", "expires_in": "...", "token_type": "..." }.
49 | */
50 | async function getPublicToken() {
51 | return getToken(config.scopes.public);
52 | }
53 |
54 | /**
55 | * Retrieves a 2-legged authentication token for preconfigured internal scopes.
56 | * @returns Token object: { "access_token": "...", "expires_at": "...", "expires_in": "...", "token_type": "..." }.
57 | */
58 | async function getInternalToken() {
59 | return getToken(config.scopes.internal);
60 | }
61 |
62 | module.exports = {
63 | getClient,
64 | getPublicToken,
65 | getInternalToken
66 | };
67 |
--------------------------------------------------------------------------------
/routes/modelderivative.js:
--------------------------------------------------------------------------------
1 | /////////////////////////////////////////////////////////////////////
2 | // Copyright (c) Autodesk, Inc. All rights reserved
3 | // Written by Forge Partner Development
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 | const express = require('express');
20 | const {
21 | DerivativesApi,
22 | JobPayload,
23 | JobPayloadInput,
24 | JobPayloadOutput,
25 | JobSvfOutputPayload
26 | } = require('forge-apis');
27 |
28 | const { getClient, getInternalToken } = require('./common/oauth');
29 |
30 | let router = express.Router();
31 |
32 | // Middleware for obtaining a token for each request.
33 | router.use(async (req, res, next) => {
34 | const token = await getInternalToken();
35 | req.oauth_token = token;
36 | req.oauth_client = getClient();
37 | next();
38 | });
39 |
40 | // POST /api/forge/modelderivative/jobs - submits a new translation job for given object URN.
41 | // Request body must be a valid JSON in the form of { "objectName": "" }.
42 | router.post('/jobs', async (req, res, next) => {
43 | let job = new JobPayload();
44 | job.input = new JobPayloadInput();
45 | job.input.urn = req.body.objectName;
46 | job.output = new JobPayloadOutput([
47 | new JobSvfOutputPayload()
48 | ]);
49 | job.output.formats[0].type = 'svf';
50 | job.output.formats[0].views = ['2d', '3d'];
51 | try {
52 | // Submit a translation job using [DerivativesApi](https://github.com/Autodesk-Forge/forge-api-nodejs-client/blob/master/docs/DerivativesApi.md#translate).
53 | await new DerivativesApi().translate(job, {}, req.oauth_client, req.oauth_token);
54 | res.status(200).end();
55 | } catch(err) {
56 | next(err);
57 | }
58 | });
59 |
60 | module.exports = router;
61 |
--------------------------------------------------------------------------------
/routes/oauth.js:
--------------------------------------------------------------------------------
1 | /////////////////////////////////////////////////////////////////////
2 | // Copyright (c) Autodesk, Inc. All rights reserved
3 | // Written by Forge Partner Development
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 | const express = require('express');
20 |
21 | const { getPublicToken } = require('./common/oauth');
22 |
23 | let router = express.Router();
24 |
25 | // GET /api/forge/oauth/token - generates a public access token (required by the Forge viewer).
26 | router.get('/token', async (req, res, next) => {
27 | try {
28 | const token = await getPublicToken();
29 | res.json({
30 | access_token: token.access_token,
31 | expires_in: token.expires_in
32 | });
33 | } catch(err) {
34 | next(err);
35 | }
36 | });
37 |
38 | module.exports = router;
39 |
--------------------------------------------------------------------------------
/routes/oss.js:
--------------------------------------------------------------------------------
1 | /////////////////////////////////////////////////////////////////////
2 | // Copyright (c) Autodesk, Inc. All rights reserved
3 | // Written by Forge Partner Development
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 | const fs = require('fs');
20 | const formidable = require('express-formidable');
21 | const express = require('express');
22 | const { BucketsApi, ObjectsApi, PostBucketsPayload } = require('forge-apis');
23 |
24 | const { getClient, getInternalToken } = require('./common/oauth');
25 | const config = require('../config');
26 |
27 | let router = express.Router();
28 |
29 | // Middleware for obtaining a token for each request.
30 | router.use(async (req, res, next) => {
31 | const token = await getInternalToken();
32 | req.oauth_token = token;
33 | req.oauth_client = getClient();
34 | next();
35 | });
36 |
37 | // GET /api/forge/oss/buckets - expects a query param 'id'; if the param is '#' or empty,
38 | // returns a JSON with list of buckets, otherwise returns a JSON with list of objects in bucket with given name.
39 | router.get('/buckets', async (req, res, next) => {
40 | const bucket_name = req.query.id;
41 | if (!bucket_name || bucket_name === '#') {
42 | try {
43 | // Retrieve buckets from Forge using the [BucketsApi](https://github.com/Autodesk-Forge/forge-api-nodejs-client/blob/master/docs/BucketsApi.md#getBuckets)
44 | const buckets = await new BucketsApi().getBuckets({ limit: 64 }, req.oauth_client, req.oauth_token);
45 | res.json(buckets.body.items.map((bucket) => {
46 | return {
47 | id: bucket.bucketKey,
48 | // Remove bucket key prefix that was added during bucket creation
49 | text: bucket.bucketKey.replace(config.credentials.client_id.toLowerCase() + '-', ''),
50 | type: 'bucket',
51 | children: true
52 | };
53 | }));
54 | } catch(err) {
55 | next(err);
56 | }
57 | } else {
58 | try {
59 | // Retrieve objects from Forge using the [ObjectsApi](https://github.com/Autodesk-Forge/forge-api-nodejs-client/blob/master/docs/ObjectsApi.md#getObjects)
60 | const objects = await new ObjectsApi().getObjects(bucket_name, {}, req.oauth_client, req.oauth_token);
61 | res.json(objects.body.items.map((object) => {
62 | return {
63 | id: Buffer.from(object.objectId).toString('base64'),
64 | text: object.objectKey,
65 | type: 'object',
66 | children: false
67 | };
68 | }));
69 | } catch(err) {
70 | next(err);
71 | }
72 | }
73 | });
74 |
75 | router.post('/upload', formidable(), async function (req, res, next) {
76 | const file = req.files.fileToUpload
77 | if (!file) {
78 | res.status(400).send('The required field ("model-file") is missing.');
79 | return;
80 | }
81 | try {
82 | await uploadObject(req.fields.bucketKey,file.name, file.path);
83 | res.status(200).end();
84 | } catch (err) {
85 | next(err);
86 | }
87 | });
88 |
89 | async function uploadObject(bucketKey,objectName, filePath) {
90 | const buffer = await fs.promises.readFile(filePath);
91 | const results = await new ObjectsApi().uploadResources(
92 | bucketKey,
93 | [{ objectKey: objectName, data: buffer }],
94 | { useAcceleration: false, minutesExpiration: 15 },
95 | null,
96 | await getInternalToken()
97 | );
98 | if (results[0].error) {
99 | throw results[0].completed;
100 | } else {
101 | return results[0].completed;
102 | }
103 | }
104 |
105 | module.exports = router;
106 |
--------------------------------------------------------------------------------
/start.js:
--------------------------------------------------------------------------------
1 | /////////////////////////////////////////////////////////////////////
2 | // Copyright (c) Autodesk, Inc. All rights reserved
3 | // Written by Forge Partner Development
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 | const path = require('path');
20 | const express = require('express');
21 | const fs = require('fs');
22 |
23 | let masterconfigpath = './public/extensions/config.json';
24 | let extensionsconfig = require(masterconfigpath);
25 | let source = './public/extensions';
26 | let extensions = [];
27 | fs.readdirSync(source, { withFileTypes: true })
28 | .filter(dirent => dirent.isDirectory())
29 | .forEach(folder => {
30 | let econfig = require(source+'/'+folder.name+'/config.json')
31 | extensions.push(econfig);
32 | });
33 | extensionsconfig.Extensions = extensions;
34 | fs.writeFileSync(masterconfigpath, JSON.stringify(extensionsconfig), function(err) {
35 | if (err) throw err;
36 | });
37 |
38 | const PORT = process.env.PORT || 3000;
39 | const config = require('./config');
40 | if (config.credentials.client_id == null || config.credentials.client_secret == null) {
41 | console.error('Missing FORGE_CLIENT_ID or FORGE_CLIENT_SECRET env. variables.');
42 | return;
43 | }
44 |
45 | let app = express();
46 | app.use(express.static(path.join(__dirname, 'public')));
47 | app.use(express.json({ limit: '50mb' }));
48 | app.use('/api/forge/oauth', require('./routes/oauth'));
49 | app.use('/api/forge/oss', require('./routes/oss'));
50 | app.use('/api/forge/modelderivative', require('./routes/modelderivative'));
51 | app.use((err, req, res, next) => {
52 | console.error(err);
53 | res.status(err.statusCode).json(err);
54 | });
55 | app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });
56 |
--------------------------------------------------------------------------------
/thumbnail.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Autodesk-Forge/forge-extensions/abaae38c6db4e450f0b3951b9ec1dd15bab3a1c8/thumbnail.PNG
--------------------------------------------------------------------------------