This commit is contained in:
David Friedrich 2024-11-08 07:51:28 +01:00
commit 98e799b7df
21 changed files with 1936 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules
.env
*.log
.DS_Store
Thumbs.db

21
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Server",
"runtimeExecutable": "npm",
"runtimeArgs": [
"start"
],
"skipFiles": [
"<node_internals>/**/*.js"
]
}
]
}

20
Dockerfile Normal file
View file

@ -0,0 +1,20 @@
FROM node:22.9
# We have to install nodemon globally before moving into the working directory
RUN npm install -g nodemon
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 8080
CMD ./start.sh

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2024 Autodesk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

198
README.md Normal file
View file

@ -0,0 +1,198 @@
<!--
*** DanieleViero
*** §§§§§§§§§§§§§§§§§§§§§§§§§§§§_-Main readme template-_§§§§§§§§§§§§§§§§§§§§§§§§§§
*** Make sure to use this as initiate on all projects. Must be used along with the
*** .gitignore provided as a template
*** contact ref
*** user@domain.com
*** user@domain.com
*** user@domain.com
*** user@domain.com
-->
<!--
*** using ref style capability
*** for more info: https://www.markdownguide.org/basic-syntax/#reference-style-links
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
*** bottom of document for the declaration of the reference variables
***
-->
<!-- Header quick ref -->
![Development][Development]
<!-- COMPANY LOGO -->
<br />
<div align="center">
<a href="null">
<img src="img/gruner.png" alt="Logo" width="260" height="170">
</a>
<h3 align="center">Website viewer APS</h3>
<p align="center">
------ Website viewer Autodesk Platform Services driven -----
</p>
</div>
<!-- TABLE OF CONTENTS -->
<!-- REMOVE IF NOT RELEVANT -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#project-description">Project Description</a>
</li>
<li>
<a href="#getting-started">Getting Started</a>
</li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#contact">Contact</a></li>
</ol>
</details>
<!-- ABOUT THE PROJECT -->
## Project Description
APS model viewer implementation for marketing purposes.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- GETTING STARTED -->
## Getting Started
To get the latest compiled version, go to: [Github Releases](https://github.com/grunergroup/gTT_APSwebViewer/releases)
### Docker Setup
- clone the Repo
- Create a _.env_ file in the project folder, and populae it with the snippet below,
replacing `<client-id>` and `<client-secret>` with your APS Client ID and Client Secret:
```bash
APS_CLIENT_ID=<INSERT ID>
APS_CLIENT_SECRET=<INSERT SECRET>
APS_BUCKET=
presetModel=BPZ_00_GRU_SIT_STR_MOD_EXI_Powerhouse.rvt # Insert filename of the model
```
- edit `docker-compose.yml` and change the port.
```yml
ports:
- "127.0.0.1:808X:8080"
```
- edit the
- Run the application, `docker compose up -d` in terminal
- Open http://localhost:8080
### NGINX Setup
1. Edit the `sudo nano /etc/nginx/nginx.conf`.
```js
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
location / {
proxy_pass http://127.0.0.1:8080;
}
location ~/test(.*)$ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:8081/$1;
}
...
```
- copy the `location ~/test..`
- adjust the path to your liking
- adjust `proxy_pass http://127.0.0.1:808X/$1;`port to the port configured in your docker-compose.yml
2. reload nginx to activate the new configuration `sudo systemctl reload nginx.service`.
The viewer should now be displaying the Model on the configured URL.
### ASP Documentation
- [Link1](https://aps.autodesk.com/apis-and-services/viewer)
- [Link2](https://aps.autodesk.com/developer/overview/viewer-sdk)
- [Link3](https://aps.autodesk.com/en/docs/viewer/v7/developers_guide/overview/)
<!-- CONTRIBUTING -->
## Contributing
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
Don't forget to give the project a star! Thanks again!
1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- CONTACT -->
## Authors
Daniele Viero - daniele.viero@gruner.ch
David Friedrich - david.friedrich@gruner.ch
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[DepICT]: https://img.shields.io/badge/DEP-ICT-brightgreen
[DepICT-url]: https://gruner.sharepoint.com/sites/gn-su/SitePages/BSC_ICT.aspx
[DepBIM]: https://img.shields.io/badge/DEP-BIM-orange
[DepBIIM]: https://img.shields.io/badge/DEP-BIIM-orange
[DepBIM-url]: https://gruner.sharepoint.com/sites/intranet/SitePages/en/Home.aspx
[DepINFOM]: https://img.shields.io/badge/DEP-INFOMANAGEMENT-yellowgreen
[DepINFOM-url]: https://gruner.sharepoint.com/sites/intranet/SitePages/en/Home.aspx
[DepDATAM]: https://img.shields.io/badge/DEP-DATAMANAGEMENT-blue
[DepDATAM-url]: https://gruner.sharepoint.com/sites/intranet/SitePages/en/Home.aspx
[Integration]: https://img.shields.io/badge/-Integration-blue
[Automation]: https://img.shields.io/badge/-Automation-brightgreen
[Development]: https://img.shields.io/badge/-Development-yellow
[Support]: https://img.shields.io/badge/-Support-red
[Token]: https://img.shields.io/badge/-TOKEN-orange
## Development
### Prerequisites
- [APS credentials](https://forge.autodesk.com/en/docs/oauth/v2/tutorials/create-app)
- [Node.js](https://nodejs.org) (Long Term Support version is recommended)
- Command-line terminal such as [PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/overview)
or [bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) (should already be available on your system)
> We recommend using [Visual Studio Code](https://code.visualstudio.com) which, among other benefits,
> provides an [integrated terminal](https://code.visualstudio.com/docs/terminal/basics) as well.
## Troubleshooting
## License
This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT).
Please see the [LICENSE](LICENSE) file for more details.

17
config.js Normal file
View file

@ -0,0 +1,17 @@
require('dotenv').config();
let { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_BUCKET, PORT, presetModel } = process.env;
if (!APS_CLIENT_ID || !APS_CLIENT_SECRET) {
console.warn('Missing some of the environment variables.');
process.exit(1);
}
APS_BUCKET = APS_BUCKET || `${APS_CLIENT_ID.toLowerCase()}-basic-app`;
PORT = PORT || 8080;
module.exports = {
APS_CLIENT_ID,
APS_CLIENT_SECRET,
APS_BUCKET,
PORT,
presetModel
};

11
docker-compose.yml Normal file
View file

@ -0,0 +1,11 @@
services:
app:
build: .
volumes:
- .:/usr/src/app
container_name: docker-node-tut
restart: always
ports:
- 127.0.0.1:8080:8080
command: bash -c /usr/src/app/start.sh
env_file: .env

1177
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "aps-simple-viewer-nodejs",
"version": "1.0.0",
"description": "Autodesk Platform Services application built by following the Simple Viewer tutorial from https://aps.autodesk.com/tutorials.",
"scripts": {
"start": "node server.js"
},
"keywords": [
"autodesk-platform-services"
],
"license": "MIT",
"dependencies": {
"@aps_sdk/authentication": "0.1.0-beta.1",
"@aps_sdk/autodesk-sdkmanager": "0.0.7-beta.1",
"@aps_sdk/model-derivative": "0.1.0-beta.1",
"@aps_sdk/oss": "0.1.0-beta.1",
"dotenv": "^16.4.1",
"express": "^4.18.2",
"express-formidable": "^1.2.0"
},
"devDependencies": {
"@types/forge-viewer": "^7.48.0"
}
}

14
routes/auth.js Normal file
View file

@ -0,0 +1,14 @@
const express = require('express');
const { getPublicToken } = require('../services/aps.js');
let router = express.Router();
router.get('/api/auth/token', async function (req, res, next) {
try {
res.json(await getPublicToken());
} catch (err) {
next(err);
}
});
module.exports = router;

67
routes/models.js Normal file
View file

@ -0,0 +1,67 @@
const express = require('express');
const formidable = require('express-formidable');
const { listObjects, uploadObject, translateObject, getManifest, urnify } = require('../services/aps.js');
const { presetModel } = require('../config.js');
let router = express.Router();
router.get('/api/models', async function (req, res, next) {
try {
const objects = await listObjects();
res.json(objects.map(o => ({
name: o.objectKey,
urn: urnify(o.objectId)
})));
} catch (err) {
next(err);
}
});
router.get('/api/selectedmodel', async function (req, res, next){
res.json({ name:presetModel });
});
router.get('/api/models/:urn/status', async function (req, res, next) {
try {
const manifest = await getManifest(req.params.urn);
if (manifest) {
let messages = [];
if (manifest.derivatives) {
for (const derivative of manifest.derivatives) {
messages = messages.concat(derivative.messages || []);
if (derivative.children) {
for (const child of derivative.children) {
messages.concat(child.messages || []);
}
}
}
}
res.json({ status: manifest.status, progress: manifest.progress, messages });
} else {
res.json({ status: 'n/a' });
}
} catch (err) {
next(err);
}
});
router.post('/api/models', formidable({ maxFileSize: Infinity }), async function (req, res, next) {
const file = req.files['model-file'];
if (!file) {
res.status(400).send('The required field ("model-file") is missing.');
return;
}
try {
const obj = await uploadObject(file.name, file.path);
await translateObject(urnify(obj.objectId), req.fields['model-zip-entrypoint']);
res.json({
name: obj.objectKey,
urn: urnify(obj.objectId)
});
} catch (err) {
next(err);
}
});
module.exports = router;

8
server.js Normal file
View file

@ -0,0 +1,8 @@
const express = require('express');
const { PORT } = require('./config.js');
let app = express();
app.use(express.static('wwwroot'));
app.use(require('./routes/auth.js'));
app.use(require('./routes/models.js'));
app.listen(PORT, function () { console.log(`Server listening on port ${PORT}...`); });

100
services/aps.js Normal file
View file

@ -0,0 +1,100 @@
const { SdkManagerBuilder } = require('@aps_sdk/autodesk-sdkmanager');
const { AuthenticationClient, Scopes } = require('@aps_sdk/authentication');
const { OssClient, CreateBucketsPayloadPolicyKeyEnum, CreateBucketXAdsRegionEnum } = require('@aps_sdk/oss');
const { ModelDerivativeClient, View, Type } = require('@aps_sdk/model-derivative');
const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_BUCKET } = require('../config.js');
const sdk = SdkManagerBuilder.create().build();
const authenticationClient = new AuthenticationClient(sdk);
const ossClient = new OssClient(sdk);
const modelDerivativeClient = new ModelDerivativeClient(sdk);
const service = module.exports = {};
service.getInternalToken = async () => {
const credentials = await authenticationClient.getTwoLeggedToken(APS_CLIENT_ID, APS_CLIENT_SECRET, [
Scopes.DataRead,
Scopes.DataCreate,
Scopes.DataWrite,
Scopes.BucketCreate,
Scopes.BucketRead
]);
return credentials;
};
service.getPublicToken = async () => {
const credentials = await authenticationClient.getTwoLeggedToken(APS_CLIENT_ID, APS_CLIENT_SECRET, [
Scopes.DataRead
]);
return credentials;
};
service.ensureBucketExists = async (bucketKey) => {
const { access_token } = await service.getInternalToken();
try {
await ossClient.getBucketDetails(access_token, bucketKey);
} catch (err) {
if (err.axiosError.response.status === 404) {
await ossClient.createBucket(access_token, CreateBucketXAdsRegionEnum.Us, {
bucketKey: bucketKey,
policyKey: CreateBucketsPayloadPolicyKeyEnum.Persistent
});
} else {
throw err;
}
}
};
service.listObjects = async () => {
await service.ensureBucketExists(APS_BUCKET);
const { access_token } = await service.getInternalToken();
let resp = await ossClient.getObjects(access_token, APS_BUCKET, { limit: 64 });
let objects = resp.items;
while (resp.next) {
const startAt = new URL(resp.next).searchParams.get('startAt');
resp = await ossClient.getObjects(access_token, APS_BUCKET, { limit: 64, startAt });
objects = objects.concat(resp.items);
}
return objects;
};
service.uploadObject = async (objectName, filePath) => {
await service.ensureBucketExists(APS_BUCKET);
const { access_token } = await service.getInternalToken();
const obj = await ossClient.upload(APS_BUCKET, objectName, filePath, access_token);
return obj;
};
service.translateObject = async (urn, rootFilename) => {
const { access_token } = await service.getInternalToken();
const job = await modelDerivativeClient.startJob(access_token, {
input: {
urn,
compressedUrn: !!rootFilename,
rootFilename
},
output: {
formats: [{
views: [View._2d, View._3d],
type: Type.Svf
}]
}
});
return job.result;
};
service.getManifest = async (urn) => {
const { access_token } = await service.getInternalToken();
try {
const manifest = await modelDerivativeClient.getManifest(access_token, urn);
return manifest;
} catch (err) {
if (err.axiosError.response.status === 404) {
return null;
} else {
throw err;
}
}
};
service.urnify = (id) => Buffer.from(id).toString('base64').replace(/=/g, '');

1
start.sh Normal file
View file

@ -0,0 +1 @@
npm start run

BIN
thumbnail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
wwwroot/gruner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

28
wwwroot/index.html Normal file
View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="icon" type="image/x-icon" href="https://cdn.autodesk.io/favicon.ico">
<link rel="stylesheet" href="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.css">
<link rel="stylesheet" href="./main.css">
<title>Autodesk Platform Services: Simple Viewer</title>
</head>
<body>
<div id="header">
<img class="logo" src="./gruner.png" >
<span class="title">Simple Viewer</span>
<input style="display: none" type="file" id="input">
</div>
<div id="preview"></div>
<div id="overlay"></div>
<script src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.js"></script>
<script>modelURN = '<%=modelURN%>'</script>
<script src="./main.js" type="module"></script>
</body>
</html>

55
wwwroot/main.css Normal file
View file

@ -0,0 +1,55 @@
body, html {
margin: 0;
padding: 0;
height: 100vh;
font-family: ArtifaktElement;
}
#header, #preview, #overlay {
position: absolute;
width: 100%;
}
#header {
height: 3em;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
}
#preview, #overlay {
top: 3em;
bottom: 0;
}
#overlay {
z-index: 1;
background-color: rgba(0, 0, 0, 0.5);
padding: 1em;
display: none;
}
#overlay > .notification {
margin: auto;
padding: 1em;
max-width: 50%;
background: white;
}
#header > * {
height: 2em;
margin: 0 0.5em;
font-size: 1em;
font-family: ArtifaktElement;
}
#header .title {
flex: 1 0 auto;
height: auto;
}
#models {
flex: 0 1 auto;
min-width: 2em;
}

110
wwwroot/main.js Normal file
View file

@ -0,0 +1,110 @@
import { initViewer, loadModel } from "./viewer.js";
// Declare the index number of the model in the array
const urnModelID = window.modelURN;
console.log(urnModelID)
// Function to change the title text
function changeTitle(newTitle) {
const titleElement = document.querySelector(".title");
if (titleElement) {
titleElement.textContent = newTitle;
} else {
console.error("Title element not found");
}
}
async function getModelsUrn() {
try {
// const resp = await fetch("/api/theurn");
// if (!resp.ok) {
// throw new Error(await resp.text());
// }
// const data = await resp.json();
// const urn = data.urn;
const resp1 = await fetch("/api/models");
const allmodels = await resp1.json();
const resp2 = await fetch("/api/selectedmodel")
const selectModelName = await resp2.json();
// const data2 = await resp2.json();
for (let i = 0; i < allmodels.length; i++){
if (allmodels[i].name == selectModelName.name){
console.log(allmodels[i].urn);
changeTitle(allmodels[i].name)
return allmodels[i].urn;
}
}
// const urns = models.map((model) => model.urn);
// const titles = models.map((model) => model.name);
// const randomIndex = Math.floor(Math.random() * urns.length);
// console.log(urns)
// changeTitle(titles[urnModelID]);
// return urn;
} catch (err) {
alert("Could not list models. See the console for more details.");
console.error(err);
}
}
initViewer(document.getElementById("preview")).then((viewer) => {
loadModelwithViewer(viewer);
});
async function loadModelwithViewer(viewer) {
if (window.onModelSelectedTimeout) {
clearTimeout(window.onModelSelectedTimeout);
delete window.onModelSelectedTimeout;
}
const urn = await getModelsUrn();
window.location.hash = urn;
try {
const resp = await fetch(`/api/models/${urn}/status`);
if (!resp.ok) {
throw new Error(await resp.text());
}
const status = await resp.json();
switch (status.status) {
case "n/a":
showNotification(`Model has not been translated.`);
break;
case "inprogress":
showNotification(`Model is being translated (${status.progress})...`);
window.onModelSelectedTimeout = setTimeout(
onModelSelected,
5000,
viewer,
urn
);
break;
case "failed":
showNotification(
`Translation failed. <ul>${status.messages
.map((msg) => `<li>${JSON.stringify(msg)}</li>`)
.join("")}</ul>`
);
break;
default:
clearNotification();
loadModel(viewer, urn);
break;
}
} catch (err) {
alert("Could not load model. See the console for more details.");
console.error(err);
}
}
function showNotification(message) {
const overlay = document.getElementById("overlay");
overlay.innerHTML = `<div class="notification">${message}</div>`;
overlay.style.display = "flex";
}
function clearNotification() {
const overlay = document.getElementById("overlay");
overlay.innerHTML = "";
overlay.style.display = "none";
}

59
wwwroot/viewer.js Normal file
View file

@ -0,0 +1,59 @@
async function getAccessToken(callback) {
try {
const resp = await fetch("/api/auth/token");
if (!resp.ok) {
throw new Error(await resp.text());
}
const { access_token, expires_in } = await resp.json();
callback(access_token, expires_in);
} catch (err) {
alert("Could not obtain access token. See the console for more details.");
console.error(err);
}
}
export function initViewer(container) {
return new Promise(function (resolve, reject) {
Autodesk.Viewing.Initializer(
{ env: "AutodeskProduction", getAccessToken },
function () {
const config = {
extensions: ["Autodesk.DocumentBrowser"],
};
const viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
viewer.start();
viewer.setTheme("light-theme");
resolve(viewer);
}
);
});
}
export function loadModel(viewer, urn) {
return new Promise(function (resolve, reject) {
function onDocumentLoadSuccess(doc) {
resolve(viewer.loadDocumentNode(doc, doc.getRoot().getDefaultGeometry()));
}
function onDocumentLoadFailure(code, message, errors) {
reject({ code, message, errors });
}
viewer.setLightPreset(0);
Autodesk.Viewing.Document.load(
"urn:" + urn,
onDocumentLoadSuccess,
onDocumentLoadFailure
);
});
}
const onToolbarCreated = (e) => {
const group = viewer.toolbar.getControl("settingsTools");
group.removeControl("toolbar-modelStructureTool");
group.removeControl("toolbar-propertiesTool");
//group.removeControl('toolbar-settingsTool');
//group.removeControl('toolbar-fullscreenTool');
viewer.removeEventListener(
Autodesk.Viewing.TOOLBAR_CREATED_EVENT,
onToolbarCreated
);
};