How to Build Your Own 3D CAD Model Viewer (for the Web)!
A Beginner-First Introduction to Integrating Three.js with the Onshape REST API
Demo — here is what we’re building today:
Table of Contents
- Step 0: Environment Setup
- Step 1: Add Boilerplate Code
- Step 2: Adding Vaporware Functionality
- Step 3: Rendering glTFs from the Onshape REST API
3. Conclusion
How to Build for the Web?
If you’re a software engineer who’s new to working with Onshape’s REST API for your own web apps, it can be quite overwhelming to get started learning how to do so.
That’s why I wrote this blog for you — as a software engineer currently inside Onshape, my objective is to demystify our API (as well as the stages of full-stack development in general) for you. By the end of this blog, you will better understand how to:
- Setup Your Local Environment for Web Dev (using VS Code)
- Create a Boilerplate Landing Page (using HTML/CSS)
- Create a glTF Viewer on a Local Backend (using Three.js/Express.js)
- Request, Receive, and Render a glTF Model from your Onshape account (using our REST API)
With that said — let’s get started!
Tutorial: A Landing Page for a Trucking Company
For the sake of simplicity, our example will be to build a landing page that incorporates a 3D model viewer —although it will be pretty barebones, the steps you’ll learn should help in building other kinds of web apps as well.
Step 0: Environment Setup
If this your first time using git
or Express.js, there’s a few tools I need you to setup first:
1) Git: Download and install this version control tool. See this page to see how to do so for your particular operating system.
2) GitHub: login/sign up for an account now, since we’ll be using that to help you practice pushing/pulling your changes.
4) Download and install Node.js and the Node Package Manager (npm
). The preferred way to do this is by first installing something called the Node Version Manager (nvm
). Let’s do that now:
- If you’re on macOS or Linux — I’ll take you directly to the GitHub repo for this tool. Open up a Terminal on your system, and paste the following command:
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
- This in effect will download
nvm
for you. Now, use this command to activate it:
$ export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
- Once that is run, we should verify it actually installed. Run the following command — if it doesn’t break, then you’re good to go:
$ nvm --version # should output a version number, e.g. "0.38.0"
- OK! Now, we can use
nvm
to quickly install and use different versions of Node.js and npm via the command line. For this tutorial, we’ll go with version 14 of Node, and whatever the latest (compatible) version ofnpm
there is:
$ nvm install 14
$ nvm use 14 --lts
$ nvm install-latest-npm
- Lastly, before continuing you should go ahead and verify that both
npm -v
andnode -v
commands execute normally in your Terminal. The output should be a string in the formx.y.z
(just like fornvm -v
). This will let us know that everything’s working so far. - But Zain, what if I’m on Windows? — I gotchu! There is just 1 minor difference for you compared to the macOS/Linux crowd: you are going to use the nvm-windows package on GitHub, instead of plain ‘ol
nvm
. Use the installer linked from that repo to get nvm on your machine, instead of using the Terminal commands above. - But then afterwards, you should then still be able to run the same commands to get the LTS versions of Node.js and
npm
.
5) VS Code: For this tutorial, you are more than welcome to use any IDE. For those who have not used one before though, I would strongly recommend installing Visual Studio Code (aka “VS Code”), as it offers lots of extensions to make web development easier (especially for beginners).
6) Note: if you do indeed choose to use VS Code for this tutorial, my next suggestion would be to install the “Live Server Extension”. To do so, open VS Code, go to Preferences > Extensions, and then simply search for it in the search box. This video by Sana Ajani at Microsoft can provide more background if you’re interested.
Step 1: Add Boilerplate Code
Now that we’ve finished setup, it’s time to start our web app by building a what every product needs: a sleek-looking landing page!
- Head over to the GitHub repo now. Please use this link to go directly to the instructions in the README.md. Complete steps 1–4 in that file right now, so you’ll be ready for development.
- So you have the repo now? Great! We’ve divided up the different pieces of this tutorial across several
git
branches, so you won’t have to worry about keeping track of too many moving parts at once. For now, let’s go to the branch for just this part of the tutorial. Run these commands:
$ cd my-OnshapeExperiments.git
$ git checkout boilerplate-starter
This is a particularly good time to open the project in your IDE. If you’re using VS Code, this can also be done from the command line:
$ code . # the "." is a file path, it should point to wherever the project root directory is located
Good stuff! Inside of VS Code, we can verify we all the files we need for right now. If you look over at the File Explorer on the left-hand pane, please verify it looks something like the following:
A few observations to make here:
README.md
— you’ve seen this already..eslintrc.json, .gitignore, package.json, package-lock.json, .env
— you can simply ignore these for now.standalone/
— this is what we’ll focus on! As you can see, there are 3 subdirectories under here namedcss
,html
, andstatic
.- And so far, there shouldn’t be any files under
standalone
, except for the .jpg image I’ve provided for you in thestatic
folder (we’ll see how to make use of it later in this section).
3. Switched branches successfully? Nice. Now, it’s time for us to start building out our landing page!
- The first step is adding a new HTML page. Let’s do that by first opening a terminal inside of VS Code. Use Ctrl + ` (backtick) or Control + ` to show the integrated terminal (the former is for Windows, the latter for macOS/Linux).
- Then, go ahead and add the new HTML file under the
html
folder. Using thetouch
command, let’s name itindex.html.
$ touch ./standalone/html/index.html # exact file path will vary based on your local setup
- So far so good? Once you have that file, open it in the editor. You can either click on its name in the File Explorer, or invoke the same
code
command from before:code ./standalone/html/index.html
. - Next, inside of
index.html
, add the following markdown:
<h1>Hello World</h1>
4. Nice. Now before making any further changes, let’s take a look at what that web app looks like locally. It’s time to run our web server!
- For those of you who have opt-ed to use the “Live Server Extension”, you should be to do this step by doing the following:
- Hover your mouse over the index.html file, on the left hand pane of VS Code.
- Right click on the file. You should then see a menu appear, with an option that says “Open with Live Server”. For example (on macOS):
- Click on that button! It should then take you to the browser. If everything is working correctly, you should see a page like this on http://127.0.0.1:5500/standalone/html/index.html:
- You should see the text “Hello World” rendered in your browser. Nice work!
- Do you think it’s magic? Let’s prove that it’s not: go back to
index.html
, and change “Hello World” to “Launch Page”. Save the change usingcommand + s
(orCtrl + s
), then go back to your browser. What’s different about the page now?
- Alrighty — now we’ll take the next step, and build this into a real launch page. At this point, add the following comments to
index.html
— it should look like the following:
<!-- NAVBAR -->
<!-- HERO -->
<h1>Launch Page</h1>
<!-- CALL TO ACTION -->
<!-- FOOTER -->
- You may not understand what all these components are yet, and that’s ok. For now, these comments to just to at least give us an outline, or a plan to follow, for the rest of this section.
- Pro-Tip: it is almost time to start building out your HTML page. To accelerate writing this code though, I would first highly recommend installing a plug-in called Emmet, which expands your IDE’s auto-completion capabilities (so you can write code faster!). If you’re using VS Code, Emmet will work for you right out of the box. If you’re not, I suggest checking out the Download page on Emmet’s documentation to see how to get it for your particular text editor.
- Now that you have Emmet installed, you can rapidly write HTML by typing the HTML tag and hitting the
tab
key. Try out these to practice:
h1 => <h1></h1>
p => <p></p>
- You can create tags that include CSS class and id names using the
.
and the#
symbols, respectively:
p.lead => <p class="lead"></p>
.jumbotron => <div class="jumbotron"></div>
li.card => <li class="card"></li>
ul#comments => <ul id="comments"></ul>
5. Once you feel comfortable navigating the codebase, let’s come back to adding HTML boilerplate for our app!
- Boilerplate is any standard code that is always there. Emmet gives us HTML boilerplate by typing
html:5
, and hitting tab. - Go ahead and do that — then, move our comments from earlier into the
<body></body>
tag. Afterwards, yourindex.html
should look like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- NAVBAR -->
<!-- HERO -->
<h1>Launch Page</h1>
<!-- CALL TO ACTION -->
<!-- FOOTER -->
</body>
</html>
ALERT: Watch out for indentation! It is important to always keep everything properly indented. The HTML will work if the indentation is bad, but it will be much more difficult for you to spot bugs and problems. So always keep your indentation consistent.
6. OK! We have started our HTML — but what about our CSS? Although we could style this entire website ourselves using our own CSS, I’d prefer to show you how to do what almost everyone does instead, from the smallest consultant to the largest companies: use a CSS Framework!
- In this case, we’ll be using one of the most popular CSS frameworks on the web, Bootstrap 5.
- To begin, watch this 20 minute demo of Bootstrap. It will give you a visual sense of the framework, how to use it, and the code behind it. You’ll have a much easier time making progress after digesting this mental model (and if you get stuck at any point after, be sure to keep the Bootstrap documentation close by).
- Now then — there are a few ways to add Bootstrap 5 to our project, but we’re going to use the easiest. We can simply add a link to the Bootstrap 5 (both its CSS and JavaScript) in our
<head></head>,
tag like this:
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Load in Bootstrap 5 CSS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<title>Document</title>
</head>
- We can tell that Bootstrap is added if you save. The font of our
h1
should have changed to look more, well, Bootstrap-py:
Note: the rounded look of this font is more formally described as being san-serif (I just made up the word “Bootstrap-py”).
7. Next let’s set about adding a navigation bar (aka a navbar) to our site. On a lot of real launch sites, these are super common as they help users stay clued in to where they are and what actions they can take at all times.
- Bootstrap comes with a Navbar component (link to docs) to add a navbar to your page. The simplest implementation of the navbar component is this:
<!-- NAVBAR -->
<nav class="navbar navbar-light bg-light">
<a class="navbar-brand" href="#">Navbar</a>
</nav>
This has just two parts, the navbar itself, and then the navbar-brand
which is the name of your project.
While we’re on the subject, let’s now change the text to in the navbar to say something that sounds little closer to an actual company name. Make the following updates:
- Change the text inside the
<a>
tag to instead say “ACME”. - And finally — on a related note, we should change the text inside the
<title>
to be more descriptive. Change that to read “ACME Inc.: 3D Truck Viewer” (more on the 3D viewer will come in Step 2).
By the end, your index.html
should now look like the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Load in Bootstrap 5 CSS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<title>ACME Inc.: 3D Truck Viewer</title>
</head>
<body>
<!-- NAVBAR -->
<nav class="navbar navbar-light bg-light">
<a class="navbar-brand" href="#">ACME</a>
</nav>
<!-- HERO -->
<h1>Launch Page</h1>
<!-- CALL TO ACTION -->
<!-- FOOTER -->
</body>
</html>
- OK! Now back to the navbar itself. Another improvement is to make this component more accessible to users with smaller screen sizes. One of the ways we can do this with Bootstrap is by adding a “toggle” button, and use CSS so that it automatically collapses the navbar when the browser window is not wide enough. Take a look at the next code snippet, where I’ve added this
<button>
(along with a fewaria
attributes, which are also there for accessibility purposes):
<!-- NAVBAR -->
<nav class="navbar navbar-light navbar-expand-md bg-light">
<div class="row">
<a class="navbar-brand w-50 mr-auto" href="/">
<p>ACME</p>
</a>
<button class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarContent"
aria-controls="navbarContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbarContent">
<ul class="list-group list-group-horizontal ml-auto">
</ul>
</div>
</div>
</nav>
- Note: if we had other pages to this site, we could add links to those other pages by inserting
<li>
elements within the<ul>
tag above. Just so you know :) - Ok, so now your page should look like this:
- That navbar looks OK so far — but, do you notice how cramped the brandmark “ACME” looks? What can we do to improve its spacing on the left side? Answer: custom CSS!
- At this point, let’s go back to the
<a>
tag that contains the “ACME” text, inside ofindex.html
. Our goal is to move it slightly to the right. We only want to apply styling to that text itself — so, let’s go ahead and nest the text inside of a<p>
element, inside the<a
> element. Finally, give it an id attribute, and name it"custom-home-link"
. Yourindex.html
should now contain this:
<!-- NAVBAR -->
...
<a class="navbar-brand w-50 mr-auto" href="/">
<p id="custom-home-link">ACME</p>
</a>
...
- Now we have a way to apply CSS to the text, but we haven’t created the CSS styles yet. Change that by going back to the terminal, and creating a new CSS file (call it
styles.css
) inside of thecss/
folder:
$ touch standalone/css/styles.css
- Open that file in your editor. To fix the left margin of our brand mark, we can use the
margin-left
property. Add the following tostyles.css
#custom-home-link {
margin-left: 1rem;
}
- Now, to apply this to our HTML, the very last step is to nest a
link
tag in the<head
> element on our page. Go back toindex.html
, and link our CSS file from there:
<head>
<!-- Load in Bootstrap 5 CSS -->
...
<!-- Custom CSS -->
<link rel="stylesheet" href="../css/styles.css" type="text/css">
...
</head>
- Note: your custom CSS should always go below where you link Bootstrap! Otherwise, Bootstrap’s own CSS will override your classes.
- Save all your changes. Go back to the browser, see the magic:
- Cowabunga!
8. After the navbar, let’s focus on adding a “Hero” section of our website. The Hero is usually a splashy image and text of the product to give new visitors to our site a good feeling about what our company does.
- Let’s start this process by first adding a container to hold our Hero (this will automatically take care of some of the minor aspects of good design, like margins). Under the
<!-- HERO -->
comment in our code, nest the existing<h1>
in adiv
with the utility class"container"
:
<!-- HERO -->
<div class="container">
<h1>Launch Page</h1>
</div>
- Now, to actually make a nice Hero in Bootstrap, we use the
jumbotron
component. Observe the next snippet (this is more boilerplate code):
<!-- HERO -->
<div class="container">
<div class="jumbotron">
<h1>Launch Page</h1>
<p class="lead">A Fantastic Product!</p>
</div>
</div>
- Your page should now look like this:
- Good work so far! Before moving on, I think we can update that text to be a little more spirited than just “Launch Page”. Go ahead and change the
h1
text in your HTML file to instead say “Command the Road”. Underneath it, you can also ahead and change the text in the<p>
tag to say “Your new truck awaits.”, so that it matches the example site shown in the video above. Your code should look like this:
<!-- HERO -->
<div class="container">
<div class="jumbotron">
<h1>Command the Road</h1>
<p class="lead">Your new truck awaits.</p>
</div>
</div>
- If you take a look at the browser now, it should look something like this:
- To make the header stand out even more, we can use the
display
utility class. Go ahead and see the next code snippet (I’ve also incorporated morerow
classes, so we’re utilizing Bootstrap’s default grid system):
<!-- HERO -->
<div class="container">
<div class="jumbotron">
<h1 class="row display-3">Command the Road</h1>
<div class="row">
<p class="ml-3 lead">Your new truck awaits.</p>
</div>
</div>
</div>
- Finally — in preparation for the next sections we’ll add to this page, add a
hr
element to the bottom of the jumbotrondiv
. Your code should look like this:
<!-- HERO -->
<div class="container">
<div class="jumbotron">
<h1 class="row display-3">Command the Road</h1>
<div class="row">
<p class="ml-3 lead">Your new truck awaits.</p>
</div>
<hr class="my-2">
</div>
</div>
- Now double check your browser looks something like the following:
9. Coming together eh? Here’s what would make this even better: a background image!
- Unsplash is a great place to find some free photographs to spruce up our page. Since this is a page about trucks, you can see in the starter repo that I left one image can use. The path is “standalone/static/seb-creativo-3jG-UM8IZ40-unsplash.jpg”.
- We want our background image to be for the whole page. So, let’s first nest everything under the
<body>
tag under a newdiv
element, with an id of “truck-bg”. Yourindex.html
should look like this:
<body>
<div class="truck-bg">
<!-- NAVBAR -->
...
<!-- HERO -->
...
<!-- CALL TO ACTION -->
<!-- FOOTER -->
</div>
</body>
- Now that we’ve added the
truck-bg
to your HTML, and now define that class in your CSS. See the following code snippet:
/* styles.css */
#custom-home-link {...}
/* The ruleset below is adapted from Chris Coyier's post on the following: https://css-tricks.com/perfect-full-page-background-image/ */
#truck-bg {
background: url("../static/seb-creativo-3jG-UM8IZ40-unsplash.jpg") no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
}
- If you save that and go back to your browser, the image will, admittedly, not look that great. It is currently set to cover all of the our page content, which is so far just 1 jumbotron. So it ends up looking like it was cropped vertically:
- Don’t worry about this for now — we’ll fix this as we go along, by adding more elements to the page that increase its vertical length. For now, what we can do is fix the text on our Hero, so it stands out again the dark-colored background. In Bootstrap, there’s a utility class called
"text-white"
you can use to help you do this. For example, you can do something like the following:
<!-- HERO -->
<div class="container text-white">
<div class="jumbotron">
<h1 class="row display-3">Command the Road</h1>
<div class="row">
<p class="ml-3 lead">Your new truck awaits.</p>
</div>
<hr class="my-2">
</div>
</div>
- Then, your page should look similar to this:
10. OK! Now that folks have been introduced to our service in the hero, usually the next thing to do is to tell them about the benefits of our product. We’re going to skip that step for the sake of brevity. Instead, we’re going to go straight into asking our users to take an action on the page. This is called a “Call To Action” or CTA. In this case we’ll link to another page, where they can learn more about our truck.
- We’ll use the
jumbotron
component again. And in order to make our link centered, we'll use Bootstrap's handytext-center
class. See the below code (I’ve also added other attributes, they’re not as important but please read the Bootstrap docs to clarify their function):
<!-- CALL TO ACTION -->
<div class="jumbotron text-center mt-5">
<button type="button"
class="btn btn-primary btn-lg"
data-toggle="button"
data-target="#ViewerButton">
<h4>
<a class="text-white" href="./viewer.html">See More</a>
</h4>
</button>
</div>
- Note: this page we’re linking to,
viewer.html,
hasn’t been created it yet; but don’t worry — that’s coming up in Step 2 when we’ll build our 3D viewer for the glTF models :) - Your page should look like the following now:
- Note 2: take a look back up at the
"mt-5"
class used in the first div in the code above. We use this utility to add a margin to the top of our CTA button, so that it’s not sitting flush with the Hero. - Lastly, if we want to fill this page out a little further, that can be done with a little extra CSS. Add an
id
to the div containing our CTA, with the value of something like"middle-section”
:
<!-- Call to Action (CTA) -->
<div id="middle-section" class="jumbotron text-center mt-5">
<button type="button" class="btn btn-primary btn-lg" data-toggle="button" data-target="#ViewerButton">
<h4>
<a class="text-white" href="./viewer.html">See More</a>
</h4>
</button>
</div>
- To define this class in your CSS, we can set the
height
property:
/* styles.css */
...
#middle-section {
height: 45em;
}
- Save your code, and verify your page looks something like below:
11. Lastly, we can polish off our page with a footer. Footers can be quite large (in fact, we’re a perfect example of this at Onshape), but we are just going to keep it simple for today. Using just a few more utility classes, we can make a nice footer. bg-secondary
gives us the secondary background color (dark grey), and p-5
gives the element a large amount of padding on all sides. text-white
should also be used here, for the same reasons we used it in the hero.
- Check this out:
<!-- FOOTER -->
<div class="mt-5 bg-secondary p-5 text-white">
<div class="row">
<p>© 2022 ACME Inc. <br>
</p>
</div>
</div>
- Note: that little
©
is HTML's way of writing the copyright symbol: ©. - If you save and scroll to the bottom of the page, you should see something like the following:
- How about that? It’s good to make footers a dark color because it kind of signals that the page is over. In this case, it also goes well with the color palette of our image.
- Speaking of which —let’s give credit to the photographer of our Unsplash photo. The footer is a great place to give acknowledgements and link to others’ work. Nest a new row under our copyright text — we can put a link to the source of our photo, and put it in a
<small>
tag to not be too overwhelming:
<!-- FOOTER -->
<div class="mt-5 bg-secondary p-5 text-white">
<div class="row">
<p>© 2022 ACME Inc. <br>
<div class="row ml-auto">
<small>
<p>
Photo Credits: "Truck" by <a href="https://unsplash.com/es/@sebcreativo?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Seb Creativo</a> on <a href="https://unsplash.com/s/photos/truck?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
</p>
</small>
</div>
</p>
</div>
</div>
- The new footer should look like this:
If it does, great work! Otherwise go back and troubleshoot until it is pixel perfect.
12. Now you have the baseline of your Launch Page, but nobody likes something so generic! Let’s spice things up.
- One of the easiest things you can do to add some uniqueness to your page is to use a Bootstrap Theme. Some themes are fancy and complex —but for this tutorial, we’re just going to use a free one that augments Bootstrap itself without adding any extra components.
- Navigate to Bootswatch.com — there’s a lot of themes here, which I encourage you to explore on your own at some point. For this tutorial, we’ll make use of the “Darkly” theme.
- You can use the URL to link it in your head BELOW where you added Bootstrap itself (but still above your custom CSS). Alternatively, the better approach is to actually download the
.min.css
file and add it to your project, (I would recommend placing it inside thecss/
folder). Here’s a screenshot of the button to download the file from Bootswatch:
- In either case, the
head
component ofindex.html
should now look similar to this:
<head>
...
<!-- Load in Bootstrap 5 CSS/JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<!-- Link in "Darkly" theme -->
<link rel="stylesheet" href="../css/bootstrap.min.css" type="text/css">
<!-- Custom CSS -->
<link rel="stylesheet" href="../css/styles.css" type="text/css">
<title>ACME Inc.: 3D Truck Viewer</title>
</head>
- Last thing: you’ll probably want to tweek your navbar (and the footer, for good measure) to be
navbar-dark
and change the background tobg-dark
(or play with colors like:bg-primary
,bg-secondary
, etc). And then don’t forget about the utility class"text-white"
to stand out from the dark background color. At the end of this, your page should look something like the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Load in Bootstrap 5 CSS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<!-- Link in "Darkly" theme: -->
<link rel="stylesheet" href="../css/bootstrap.min.css" type="text/css">
<link rel="stylesheet" href="../css/styles.css" type="text/css">
<title>ACME Inc.: 3D Truck Viewer</title>
</head>
<body>
<div id="truck-bg">
<!-- TOP NAVBAR -->
<nav class="navbar navbar-dark navbar-expand-md bg-dark">
<div class="row">
<a class="navbar-brand w-50 text-white mr-auto" href="/">
<p id="custom-home-link">ACME</p>
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbarContent">
<ul id="custom-home-link" class="list-group list-group-horizontal ml-auto">
</ul>
</div>
</div>
</nav>
<!-- HERO -->
<div class="container">
<div class="jumbotron justify-content-center">
<h1 class="row display-3">Command the Road</h1>
<div class="row">
<p class="ml-3 lead">Your new truck awaits.</p>
</div>
<hr class="my-2">
</div>
</div>
<!-- Call to Action (CTA) -->
<div id="middle-section" class="jumbotron text-center mt-5">
<button type="button" class="btn btn-primary btn-lg" data-toggle="button" data-target="#ViewerButton">
<h4>
<a class="text-white" href="./viewer.html">See More</a>
</h4>
</button>
</div>
<!-- FOOTER -->
<div class="mt-5 bg-secondary p-5 text-white">
<div class="row">
<p>© 2022 ACME Inc. <br>
<div class="row ml-auto">
<small>
<p>
Photo Credits: "Truck" by <a href="https://unsplash.com/es/@sebcreativo?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Seb Creativo</a> on <a href="https://unsplash.com/s/photos/truck?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
</p>
</small>
</div>
</p>
</div>
</div>
</div>
</body>
</html>
- And here’s the corresponding browser view. Note that because of the page height, I’ve zoomed out just to fit the page to the screenshot, otherwise it’s OK if your window is not large enough to see the footer at the bottom:
You now have a launch page. Before moving to the next section of this tutorial, make a git commit to mark your progress:
$ git add .
$ git commit -am "added boilerplate home page"
To compare your work to the solution code on GitHub, we have a branch for that as well: use git checkout boilerplate-solution
to see it.
Woohoo — congrats on the great work so far 🥳 !
Step 2: Adding Vaporware Functionality
In this section, our goal is to create the vaporware. That is to say, we’re going to have a glTF viewer by the end of this section — but instead of dealing with the Onshape REST API just yet, we’re just going to implement the front-end functionality first. This way it should be easier to debug, since you’ll just be using local glTF assets.
- Switch to the starter code for this section to begin:
$ git checkout vaporware-starter
2. Take a look around the repo — what are some of the changes you notice?
- Specifically, pay close attention the comments in the repo which begin with
"[Challenge X]"
— these are where I’ll ask you to focus on coding for this section. - Note: To simulate what goes on in industry, we’ll be implementing our viewer using a popular JavaScript library for 3D web-graphics, known as Three.js. It’s a huge library which is more than deserving of its own dedicated tutorial series — but for this blog, I’ve provided a lot more starter code, so it’ll hopefully be more manageable to follow along and you can just focus on the most crucial bits of making the 3D viewer.
3. One last thing before we resume coding — let’s get a glTF model file you can use locally! Here are your options:
- If you have an Onshape model in particular you would like to be able to visualize, then check out Cody’s article on how to export your documents to the glTF format.
- Otherwise, you are free to export a glTF from the sample truck model (shown in the video at the beginning). I’ve enabled link sharing for this model, so you can visit it here (credit goes to Poojan Shah for originally creating this truck in this public Onshape document over here)!
- Once you are on the document, it should look something like this:
- To export a glTF, start by clicking the arrow next to the download icon on the page.
- In the menu that shows up, you’ll want to select “Export tab…”.
- Now, there should be a modal on the screen titled “Export”. There are a lot of fields in this form, but don’t worry — the only one you really need to worry about is the “Format”. Open that dropdown, and make sure to select the “GLTF” option. See below for a screenshot of what this looks like:
- Once you have made that selection, go ahead and click the blue “Export” button in the modal. Your glTF file should download in a few moments!
- Last thing: once you have your new glTF (probably named “blueTruck.gltf”), make sure to move it to the
static/
folder in our project repo (so it’ll make future instructions easier to follow).
4. Alrighty, so this is probably the part where you’re thinking, “Zain, cool glTF — but how do I render it on the web page?”
- Easy. We will be focusing on how to do so in Challenge 1 of this section. Flip over to the starter code for
index.js
on this branch, and search for the stringChalllenge 1
in the file. It should look like this:
// (3) setup for loading glTF based on user selection
activateBtn.addEventListener('click', async (evt) => {
// retrieve form values + access the glTF
try {
document.body.style.cursor = 'progress';
/** [Challenge 1]: Displaying Our Local glTF File
* In order for this work, you must call loadGltf(),
* passing the relative file path to your specific .glb/.gltf file.
*
* Hint: please make sure to read the function body of loadGltf()
* above, in case you have any doubts!
*/
/** your code goes here */
} catch (err) {
displayError(`Error in displaying glTF: ${err}`);
}
});
- So, what is this saying? We can see above that what I’m asking you to do is fill in part of an event listener for a button, somewhere, on the viewer page. But, where is said button? In fact, what is the viewer page?
- To give some context here, go back to your browser, and visit the home page for our website. Remember that CTA button from before? Click on it! (or, if you’re using the Live Server Extension, feel free to just go to http://127.0.0.1:5500/standalone/html/viewer.html).
- Once you’re there — tada! This is what I meant by the viewer page. It should look something like this:
- Feel free to go look deeper into
viewer.html
if you’re curious to know more about how this page is marked up in HTML (it’s mostly more of the same as what we did forindex.html
). For right now though — do you see that blue button that says “Click Me!” on the page? - (Psst — that is the button our event listener is for!)
- Our intended UX here is for the user to click on that HTML button, so then our JavaScript knows to render our 3D model onto the viewport using Three.js.
- So, how do we solve this challenge? I would encourage you to pause here, read more into the code in
index.js
, and give this a shot on your own before I give the solution below. And don’t be afraid to dive into the Three.js documentation if you have questions — exploration is good! - Solution Time: ok! As the hint our
Challenge 1
comment mentions, the key to solving this piece is invoke theloadGltf()
function, passing in the relative file path to your glTF asset. In this case, assuming your asset is namedblueTruck.gltf
and is stored in thestatic/
folder, the code would look like this:
// ...
try {
// ...
loadGltf("../static/blueTruck.gltf");
}
// ...
- Not convinced? Go back to
viewer.html
, and test out that “Click Me!” button. You should see your truck load in, like shown below:
- Note: for more on how Three.js parses glTFs to load 3D graphics, I would definitely recommend reading up on the GLTFLoader class.
5. Next up — our viewer is working, but our initial viewpoint at the model is in kind of an awkward, close-up position. Can we use Three.js to fix that?
- Short answer — yes! Let’s see how in
Challenge 2
of this section. Just like before, start by finding the comment containing the challenge string inindex.js
. It should look something like this:
/**
* Sets the contents of the scene to the given GLTF data.
*
* @param {object} gltfScene The GLTF data to render.
*/
const setGltfContents = (gltfScene) => {
// ...
/** [Challenge 2]: Fixing the Camera Position
*
* Our viewer is almost complete! We've retrieved our glTF,
* and this function contains most of the code you need to
* build a standalone 3D model viewer using Three.js.
*
* BUT, the camera's position could be improved.
*
* Your task: let's get that camera positioned so that it looks
* directly at the center of the whatever glTF model we've retrieved
* from the API.
*
* a) To start, set the camera position to copy the coordinates in the
* "center" variable above.
* b) Next, use the size our our "box" variable to set the position
* of the camera along the X, Y, and Z axes.
*/
/** your code goes here */
// ...
};
- Take a moment to read the description in this comment. You’ll notice this is in a function called
setGltfContents()
, which is called once yourgltfLoader
object (seen earlier in Challenge 1) successfully loads your glTF file. Specifically, we’ll be focusing on how to use Three.js’ “camera” object to adjust the starting viewpoint into our 3D model. - Pause here, and please give this challenge a try on your own (feel free to read the docs on the
PerspectiveCamera
class if you need help!) - …ok, let’s review the solution here — following the comments, your code can be implemented using something similar to the following:
/**
* Sets the contents of the scene to the given GLTF data.
*
* @param {object} gltfScene The GLTF data to render.
*/
const setGltfContents = (gltfScene) => {
// ...
/** [Challenge 2]: Fixing the Camera Position */
camera.position.copy(center);
const boxSize = box.getSize(new Vector3());
camera.position.x = boxSize.x * 2;
camera.position.y = boxSize.y * 2;
camera.position.z = boxSize.z * 2;
// ...
};
- Note: the last 4 lines of the code above is what’s especially important here. This tells Three.js we want our camera 2X the size of the “bounding box” surrounding our 3D model, to make sure we can fix the whole model in our viewer element once it loads (but hopefully not be too far either).
- Test that “Click Me!” button once again. If you’re using the truck model, you should see it load, but now it should be in a much more natural starting position:
- Nicely done 🙌 ! You’ve now completed Challenge 2. Just as with Challenge 1, I’d encourage you to read up more on the various Three.js classes you’ve seen used here separately. Otherwise, let’s commit your progress and keep going:
$ git commit -am "completed 3D model viewer using local gltfs"
Step 3: Rendering glTFs from the Onshape REST API
Up to now, we’ve built a 3D viewer for glTF models. This is great — but we’re restricted only to rendering whatever glTF files are on our local machine. This is where Onshape’s REST API comes in: with it, we’ll be able to let users request to view 3D models which we have stored in your Onshape account. Let’s go set that up now!
- To begin, let’s switch to the starter code for this section:
$ git checkout custom-server-starter
2. Do you remember when I told you to ignore the package.json
file before? It’s going to be useful going forward. The main utility of this file will be to install all our Node dependencies — they’re all listed in the file, so we can install them using this npm
command (from the root directory):
$ npm install -i
3. Next, the Onshape REST API was designed to take web security seriously, so we require developers to sign up for API keys before using it.
- Go to the Onshape Developer Portal and log in with the credentials for your Onshape account.
- But Zain, what if I have multiple Onshape accounts? In this case, you’re going to want to choose the account that will have (at minimum) viewing permissions to whatever 3D models stored in Onshape that you want this glTF viewer to render.
- Note: that is to say, if you’re planning to visualize 3D models that are private to you, you should be using your private Onshape account credentials. Otherwise, don’t sweat this step and just use any Onshape account you have — in this tutorial we’ll just be working with publicly available models, so you will have no trouble accessing them.
- After logging into the Developer Portal: go to the “API keys” tab, and create a pair of new API keys to use.
- Create a new file in the root of our local git repo, name it something like
.env
, so that we have a safe place to keep these API keys:
$ touch .env
4. Next — let’s fill out your .env
file. There are six in particular you’ll need for this tutorial. I’ve listed them below, please see the comments to know how you should fill them in:
ONSHAPE_API_ACCESSKEY=... # from the Dev Portal
ONSHAPE_API_SECRETKEY=... # from the Dev Portal
PORT=3000 # or could be 5000, 8080, etc. - it's just where our web server will run
API_URL=https://cad.onshape.com/api
SESSION_SECRET=... # some long, hard to guess string with no spaces or quotes, e.g. "kdjsf3$q%%G_4&+22awgvAEQq"
WEBHOOK_CALLBACK_ROOT_URL=... # same as host name - use "http://localhost:<whatever-your-PORT-number-is>"
5. Ok! It’s time to start integrating with Onshape’s API. Again, the goal here is that instead of reading glTF files from our local storage, we’re going to tell our web server request them from one of Onshape’s servers, before rendering in the browser. As you might guess, we’re going to need to write custom server-side code to do so — that’s where Express.js comes in.
- Why are we using Express? Tbh, we’re using it here because I don’t want you to have to learn another programming language for this part of the tutorial. Express.js is an enormously popular web application framework for JavaScript. That basically means that if you can write JavaScript for the browser (and you can, since we did so previously), you can transfer that skill over to writing JavaScript for web servers (and of course, use the docs if you get stuck).
- With that as background, we now need to figure out how to actually start a server in Express— let’s do that now in
"Challenge 1"
of this git branch! Flip over to thewww
file now, and it should look like this:
#!/usr/bin/env node
/**
* [Challenge 1]: Starting the Server
*
* Starting Your Express Server
* Please do the following:
* a) load your config vars from the .env file,
* b) import the Node `app` object created in server.js,
* c) and set it up to listen on port 3000!
*/
/** your code goes here */
- Pause now, and please try Challenge 1 before moving ahead…
- …alrighty! Your solution to Challenge should look similar to this:
#!/usr/bin/env node
/** Challenge 1 Solution */
// part a)
require('dotenv').config()
const port = process.env.PORT || 3000;
// part b)
const app = require('../server');
// part c)
app.listen(port, () => {});
- Note: if you’re unclear on how the
require()
function works, I’d encourage you to read more about it here before going forward. - Now then — how is this file supposed to start our server? Observe the comment at the top of the file,
#!/usr/bin/env node
. This comment (commonly referred to as a shebang line) is what tells us this file is actually meant to be an executable. - As such, you can observe see that in
package.json
, we have a key named"scripts"
, which lists the following:
// package.json
// ...
"scripts": {
"start": "bin/www",
"dev": "nodemon bin/www",
// ...
- What does this mean? This part of
package.json
is where we can specify customnpm
commands, that others can execute when working with our project. Right now, we’re going to just focus on invoking the second script listed in this dictionary,dev.
This is just for quality-of-life purposes — just as how we didn’t need to restart our web app when using the “Live Server” extension,nodemon
will auto-reload our app after code changes (so that we don’t need to restart our server after each code change going forward, which can be cumbersome). - So, to run our
bin/www
executable usingnodemon
, please go back to your terminal and invoke ourdev
script:
$ npm run dev
- Now, your server is running! To test, go to the address https://localhost:3000/ (or whichever port you’ve configured the app to use). You will see the following response on the page:
6. Obviously, we’re not done. As you might guess from the error message on the page, we have a running server— but that server now needs to be told where to find all the HTML/CSS for our web app, before it can serve up the content we need on the UI. Let’s tackle that in Challenge 2
and Challenge 3.
- Flip over to the file named
server.js
. The comment forChallenge 2
should look like this:
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.set('trust proxy', 1); // To allow to run correctly behind Heroku
/**
* [Challenge 2]: Express Middleware
*
* Part a: Tell Express to serve the static files in our
* 'standalone' directory.
*
* Part b: Then, let's tell Express we want to
* use the bodyParser.json() function, so that we can
* parse the contents of our API requests via HTTP.
*
*/
/** your code goes here */
- At the top, this starter code imports some Node modules, and instantiates our web app as a variable (aptly named
app
) using Express.js. Now, the next step is to add middleware functions—these are essentially functions our app will be able to use as it goes through the request-response cycle, so it’s an ideal place for us to add code that will be useful across all of our app request handlers. Now, go ahead and add these 2 lines of code under the comment forChallenge 2
:
app.use(express.static(path.join(__dirname, 'standalone'))); // sets up a "static" folder
app.use(bodyParser.json()); // provides access the "body" of our HTTP requests
- OK, so what’s going on here? For background,
app.use()
is generally the method we’ll call when utilizing middleware functions with Express. In particular, when we useexpress.static()
, that is basically a way for us to tell Express where it can find all the static assets for our app, i.e. all the HTML/CSS/images, etc. You can test that this works by going to http://localhost:3000/html/index.html — if it did, we’ll be able to again to see our home page again from section 1! - So that’s
express.static()
what is thebodyParser
? The TLDR is this middleware will allow us to read the “body” of the HTTP requests coming to our server , by storing it in a variable calledreq.body
— this is something that is also useful for many kinds of server-side functions. - Alrighty, that takes care of
Challenge 2
— but what aboutChallenge 3
? Verify that you have the following comment at the bottom ofserver.js
:
/**
* [Challenge 3]: Controller functions
*
* Part a: Add a namespace for the controller functions in
* api.js, so our Express can route request towards them.
* (note: we will see more of api.js in just a sec!)
*
* Part b: using res.sendFile(), add route handlers so
* that our app can server our index.html and viewer.html pages.
*/
/** your code goes here */
- Please read the comment in full, then come back once you have given it a shot. Don’t worry, I’ll wait :)…
- …solution time!
/** [Challenge 3] */
// Part a)
app.use('/api', require('./api'));
// Part b)
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'standalone', 'html', 'index.html'));
});
app.get('/truck-viewer', (req, res) => {
res.sendFile(path.join(__dirname, 'standalone', 'html', 'viewer.html'));
});
A few points to note here:
- For part a), observe that we’re calling
app.use()
again, and just passing in a call to therequire()
function — so technically, you can consider this your first implementation of custom middleware! - For part b), the solution is basically just the Express.js-way of telling our server how to, well, serve the two HTML pages we worked on in the previous sections of this tutorial — the
index.html
and the viewer page. You might ask though, why are these lines needed, if I told you that’s what we use theexpress.static()
middleware for? - The answer is surprisingly simple — if we don’t, then we wouldn’t be able to use CSS on our server! (feel free to test this out by commenting out the line that includes
express.static()
and reloading the page). - For now — to verify our changes worked — please go to the home page route, “/”, by visiting http://localhost:3000/. You should see our familiar home page again!
7. Onward! So far we have added routes to serve all our HTML/CSS. Can you guess what we’ll need to add to our Express code, in order to grab data from the Onshape API?
- If you guessed “another route function”, you’d be correct! This is because a route is basically just a way for us to tell Express how to 1) take requests, 2) process them in some way, and 3) how to return the appropiate responses. So, just as we can setup routes where the request-response cycle is between users and our own server, the same can be done between our server, and other servers whose APIs we interact with (in this case, Onshape’s).
- Speaking of APIs, let’s flip over to
Challenge 4
to see how to make an API call to Onshape in Express. If you go toapi.js
, you should see the following starter code:
const { onshapeApiUrl } = require('./config'); // note: this is equal to whatever string you put for API_URL earlier!
const { forwardRequestToOnshape } = require('./utils');
const apiRouter = require('express').Router();
/**
* Retrieve glTF from a given Part Studio tab in an Onshape document.
*
* GET /api/get-gltf?documentId=...&inputId=...&idChoice=...&gltfElementId=...
* -> 200, { ... }
* -or-
* -> some error e.g. 400
*
* Read more/try out this endpoint in the docs: https://cad.onshape.com/glassworks/explorer#/PartStudio/exportPartStudioGltf
*/
apiRouter.get('/get-gltf/:did/:wvm/:wvmid/:eid', async (req, res) => {
// Extract the necessary IDs from the URL
const did = req.params.did,
wvm = req.params.wvm,
wvmid = req.params.wvmid,
eid = req.params.eid;
/** [Challenge 4]: Making a Request to the Onshape API
*
* First, let's pause and take a sec to read about the "Export glTF" endpoint we'll be using
* in our app: https://cad.onshape.com/glassworks/explorer#/PartStudio/exportPartStudioGltf.
* Then, use the `forwardRequestToOnshape()` function (defined in utils.js)
* to make a request to this endpoint!!
*
* Hint: the args you'll need to pass to forwardRequestToOnshape() are the following:
* 1) a template string that uses all the ^params above, to make a well-formed URI,
* 2) the request object, and
* 3) the response object found in our code...
*/
/** your code goes here */
});
Lots of code right? Let’s break it down a little:
- We’re using the “exportPartStudioGltf” endpoint from the Onshape REST API. Like the name suggests, it will retrieve a glTF model for you, provided the proper information about the Part Studio where that model is defined (more on that in a moment). For now, feel free to play around with this endpoint in our documentation so you’ll be more comfortable using it for this tutorial.
- Secondly, the comment above refers to a function called
forwardRequestToOnshape()
— what is this function? For your edification, I’ve pasted it below:
/**
* Send a request to the Onshape API, and proxy the response back to the caller.
*
* @param {string} apiPath The API path to be called. This can be absolute or a path fragment.
* @param {Request} req The request being proxied.
* @param {Response} res The response being proxied.
*/
forwardRequestToOnshape: async (apiPath, req, res) => {
try {
// API request authorization
const normalizedUrl = apiPath.indexOf(onshapeApiUrl) === 0 ? apiPath : `${onshapeApiUrl}/${apiPath}`;
const encodedString = Buffer.from(`${config.accessKey}:${config.secretKey}`).toString('base64');
const resp = await fetch(normalizedUrl, { headers: {
Authorization: `Basic ${encodedString}`,
}});
const data = await resp.text();
const contentType = resp.headers.get('Content-Type');
res.status(resp.status).contentType(contentType).send(data);
} catch (err) {
res.status(500).json({ error: err });
}
}
- I would encourage you to pause here and read more into the functions to understand it better. Essentially, this abstracts away a lot of the complexity in authorizing API requests we make to Onshape (i.e. by including those keys you got from the dev portal earlier when we call
fetch()
). Although this is mostly boilerplate, it’s super important from a security standpoint! - Ok, now, please give a go at
Challenge 4
… - …and afterwards, take a look at the solution!
apiRouter.get('/get-gltf/:did/:wvm/:wvmid/:eid', async (req, res) => {
// Extract the necessary IDs from the URL (included in the request)
// ...
/** [Challenge 4]: Making a Request to the Onshape API */
forwardRequestToOnshape(
// 1) a template string:
`${onshapeApiUrl}/partstudios/d/${did}/${wvm}/${wvmid}/e/${eid}/gltf`,
// 2) the request object, and 3) the response object passed to this function:
req, res
);
});
Ok, let’s review — first off, a word on the template string — what the heck do the parameters did
, wvm
, wvmid
, and eid
refer to?
- To answer this, let’s observe a parallel example: when you go to open a document in Onshape, the URL path you follow on our servers abides by the same naming scheme. For instance, in the document link https://cad.onshape.com/documents/c1c54c370fa5185f0a52ed15/w/1b249e369705d99ca986bec1/e/12bd0e929396835dadeaa83b?renderMode=0&uiState=63a76a21d12f2f36e4bebae5:
- the substring “…/documents/c1c54c370fa5185f0a52ed15/…” tells you what the document ID (abbreviated
did
in the API) is in our databases, - the “…/w/1b249e369705d99ca986bec1/…” tells you we’re about to use a workspace ID to refer to whatever we want to request (as opposed to using the ID of a version/microversion of the document); and then we provide that ID,
- and the substring “…/e/12bd0e929396835dadeaa83b/…” tells you the ID of the element we want to refer to, of that particular document.
- **When you see the term “element” used in our API, that is the exact same as what regular users think of as individual tabs in an Onshape document!**
With this knowledge, we’re now empowered to test out the API route you just added to our Express app. How so?
- Using the document link above (or one of your own), send a request to this API route using the proper path parameters! We can do this all in our terminal using the
curl
command— here is an example:
$ curl http://localhost:3000/get-gltf/c1c54c370fa5185f0a52ed15/w/1b249e369705d99ca986bec1/12bd0e929396835dadeaa83b/
- After you do this, it should take a second and then — voila! You’ll see something come through — that is a response coming back from the Onshape server. It should look something like this:
{"extensions":{"PTC_onshape_metadata":{"documentId":"c1c54c370fa5185f0a52ed15","elementId":"12bd0e929396835dadeaa83b"}},"extensionsUsed":["PTC_onshape_metadata"],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":1493,"type":"VEC3","max":[1.86,3.4003837,1.0279473],"min":[0.0,-2.011272,-1.0765781]},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":1493,"type":"VEC3"},{"bufferView":2,"byteOffset":0,"componentType":5123,"count":5442,"type":"SCALAR"}],"asset":{"version":"2.0"},"buffers":...
- Boom! If you see this, that means you’ve just successfully requested a glTF over our API!
8. At long last, we’ve seen how to programmatically request and receive glTF data from the Onshape API — but we have yet to integrate this functionality into our viewer on the UI. Let’s take care of that in Challenge 5
and Challenge 6
:
- First looking at any more code, let’s go visit the updated viewer page in the browser. Go to http://localhost:3000/truck-viewer, and you should see something similar to the following:
- Whoa, what is this form doing here? Let’s flip over to
viewer.html
— the form you see there can explain more. While you’re there, also take a look at whereChallenge 5
is described:
<!-- Form to "feed" API call to retrieve glTF -->
<div id="elem-selector" class="mb-5">
<div class="form-group">
<!--
[Challenge 5]: Collecting Data from Users
To be able to make requests to retrieve the specific
glTF our users ask for, we're collecting the parameters
we'll need to make API calls to Onshape
(i.e. the document ID, element ID, workspace ID, etc.) via this HTML form.
But in order to eventually pass the data from our HTML to the backend server,
we'll need to add a unique `id` string for each of the fields
in the form below, so we can tell JavaScript where to go access it
(we'll dive deeper into this in a future step, so don't worry if it
sounds a little confusing right now).
ACTION: go ahead and fill in the 'id' attributes for each form
field below, so that they are unique from each other.
The places you need to edit are marked 'YOUR ID GOES HERE', so you won't miss :)
-->
<label for="documentIdInput">Document ID</label>
<input name="documentId" type="text"
class="form-control" id="YOUR ID GOES HERE"
aria-describedby="documentIdHelp" value="f246b429ad653513d90defe2">
<small id="documentIdHelp" class="form-text text-muted">Please enter the ID of your Onshape document.</small>
</div>
<div class="form-group">
<label for="wvmSingleChoiceSelect">Select which "Type" of ID you have:</label>
<select class="form-control" id="YOUR ID GOES HERE" name="idChoice">
<option>w</option>
<option>v</option>
<option>m</option>
</select>
<small id="idChoiceHelp" class="form-text text-muted">
w = "workspace ID"; v = "version ID"; m = "microversion ID"
</small>
</div>
<div class="form-group">
<label for="wvmIdInput">WVM ID</label>
<input name="providedId" type="text"
class="form-control" id="YOUR ID GOES HERE"
aria-describedby="providedIdHelp" value="467dd42ecaa46be04cc2500a">
<small id="providedIdHelp" class="form-text text-muted">
Please enter the ID of your workspace, current version,
or microversion (must correspond to the "type" of ID chosen above).
</small>
</div>
<div class="form-group">
<label for="elementIdInput">Element ID</label>
<input name="elementId" type="text"
class="form-control" id="YOUR ID GOES HERE"
aria-describedby="eIdInputHelp" value="eff42e24b584233240dff36f">
<small id="elementIdHelp" class="form-text text-muted">
I think you can figure what this one means 😄
</small>
</div>
<button id="formSubmitButton" type="submit" class="btn btn-primary">Submit</button>
</div>
- What is this saying? We know that before our server request a glTF to the Onshape API, we need some way for a regular user to tell it which glTF to get, based on the parameters the API requires (discussed above).
- (Note: at this point, in the real world you would probably need to go to your Product Manager or UI/UX Designer to have a conversation about this — but for this tutorial, we’re just going to have our users submit the parameters through the form you saw above, which our server will parse in order to get the variables needed for the API request). So the form is not the only way we could implement this user journey, but it will help keep things simple for your learning :)
- Now, on to the challenge! We can see the starter code for the form comes with default values which you could use. But, we still need a way for our JavaScript to be able to parse the data submitted under particular form fields — that’s where you come in. Go ahead and fill in where it says
"YOUR ID GOES HERE"
in theviewer.html
. - Although you can use a variety of different ID strings, an easy option for doing this is just to reuse the string in the
for
attribute of each<label>
component placed in the starter code — here is an example solution:
<!-- Form to "fill in" API req params to retrieve glTF -->
<div id="elem-selector" class="mb-5">
<div class="form-group">
<!-- [Challenge 5]: Collecting Data from Users -->
<label for="documentIdInput">Document ID</label>
<input name="documentId" type="text"
class="form-control" id="documentIdInput"
aria-describedby="documentIdHelp" value="f246b429ad653513d90defe2">
<small id="documentIdHelp" class="form-text text-muted">Please enter the ID of your Onshape document.</small>
</div>
<div class="form-group">
<label for="wvmSingleChoiceSelect">Select which "Type" of ID you have:</label>
<select class="form-control" id="wvmSingleChoiceSelect" name="idChoice">
<option>w</option>
<option>v</option>
<option>m</option>
</select>
<small id="idChoiceHelp" class="form-text text-muted">
w = "workspace ID"; v = "version ID"; m = "microversion ID"
</small>
</div>
<div class="form-group">
<label for="wvmIdInput">WVM ID</label>
<input name="providedId" type="text"
class="form-control" id="wvmIdInput"
aria-describedby="providedIdHelp" value="467dd42ecaa46be04cc2500a">
<small id="providedIdHelp" class="form-text text-muted">
Please enter the ID of your workspace, current version,
or microversion (must correspond to the "type" of ID chosen above).
</small>
</div>
<div class="form-group">
<label for="elementIdInput">Element ID</label>
<input name="elementId" type="text"
class="form-control" id="elementIdInput"
aria-describedby="eIdInputHelp" value="eff42e24b584233240dff36f">
<small id="elementIdHelp" class="form-text text-muted">
I think you can figure what this one means 😄
</small>
</div>
<button id="formSubmitButton" type="submit" class="btn btn-primary">Submit</button>
</div>
<div id='gltf-viewport'></div>
</div>
- Good stuff! As you can see, we’ve just filled in the
id
attribute of the form fields. Please check to ensure your id strings are unique to their respective HTML elements — this is so that JavaScript will have no trouble locating them later (this statement will make more sense in a second, I promise). - Next, let’s go find
Challenge 6
. Do you remember when we implemented an event listener earlier in this tutorial? Well, let’s go revisitindex.js
— in it, you should see the following comment:
// (3) setup for loading glTF based on user selection
formSubmitBtn.addEventListener('click', async (evt) => {
// retrieve form values + access the glTF
try {
document.body.style.cursor = 'progress';
/**
* [Challenge 6]: Accessing Data from the Front-End:
*
* On the next four lines, we now access the data inputted
* by users into the form on our viewer.html page
* (so that we have all the parameters needed to
* complete that fancy-pants API request you made in Challenge 4).
*
* Now, do you remember what 'id' strings you gave
* to each field in that form?
*
* Using those same id strings for each of your form fields,
* go ahead and store the value of each field in a
* new JavaScript variable!
*
*/
const did = document.getElementById("YOUR ID GOES HERE").value,
wvm = document.getElementById("YOUR ID GOES HERE").value,
wvmid = document.getElementById("YOUR ID GOES HERE").value,
eid = document.getElementById("YOUR ID GOES HERE").value;
poll(5, () => fetch(`/api/get-gltf/${did}/${wvm}/${wvmid}/${eid}`),
(resp) => resp.status === 200, (respJson) => {
if (respJson.error) {
displayError('There was an error in parsing the glTF to a JSON string.');
} else {
console.log('Loading GLTF data...');
loadGltf(respJson);
}
});
} catch (err) {
displayError(`Error requesting GLTF data translation: ${err}`);
}
- Hmm… event listener, you look a little different. Did you get a new haircut? 💇♂️
- In all seriousness, do you see where the starter code calls
fetch(`/api/get-gltf/${did}/${wvm}/${wvmid}/${eid}`)
? This is in fact crucial, because this connects the front-end of our app to the API route we implemented earlier in this section using Express. Now, go ahead and complete this challenge, by replacing in the"YOUR ID GOES HERE"
strings with the corresponding IDs you placed in theviewer.html
form. If you’re following along with my example, the solution should look similar to this:
// (3) setup for loading glTF based on user selection
formSubmitBtn.addEventListener('click', async (evt) => {
// retrieve form values + access the glTF
try {
document.body.style.cursor = 'progress';
/** [Challenge 6]: Accessing Data from the Front-End */
const did = document.getElementById("documentIdInput").value,
wvm = document.getElementById("wvmSingleChoiceSelect").value,
wvmid = document.getElementById("wvmIdInput").value,
eid = document.getElementById("elementIdInput").value;
poll(5, () => fetch(`/api/get-gltf/${did}/${wvm}/${wvmid}/${eid}`),
(resp) => resp.status === 200, (respJson) => {
if (respJson.error) {
displayError('There was an error in parsing the glTF to a JSON string.');
} else {
console.log('Loading GLTF data...');
loadGltf(respJson);
}
});
} catch (err) {
displayError(`Error requesting GLTF data translation: ${err}`);
}
});
- Parfait. At this point, the form in the
viewer.html
should be able to request, receive, and render any multitude of 3D CAD models you have stored in Onshape (the only restriction at this point is they should be Part Studios which you have permissions to view regularly, just like when you go to https://cad.onshape.com). - Go ahead and try out the form now — you can either submit it using the default form values, or feel free to render any Part Studio models you have (using what we discussed earlier, related to how to get the required API parameters from an Onshape document URL). For an example of what a “successful” result should look like, see the demo video I included at the top of this blog.
Conclusion
Congratulations on making it to the end! 👏 Please take a moment to reflect on all that you’ve achieved today.
So, Why Do We Build for the Web?
To wrap it up in a sentence — successful collaboration is at the heart of successful product development. And collaboration does not just refer to your engineers and designers working together to develop the next version of your product. Whether you’re Jony Ive at Apple or a brand-spanking-new founder running your own hard-tech startup, the best companies don’t let their CAD systems create silos. Instead, they enable every department outside of R&D to work on top of the same 3D CAD data — this can save enormous amounts of time in sharing models, and can be especially instrumental when you need to build custom applications whose value isn’t strictly tied to R&D, such as landing pages for the product or ERP systems. But the question is: how do you design such a software system to enable this collaboration?
At Onshape, our answer can be summed up in two words: the web. We believe there’s numerous benefits to having your enterprise software run on the cloud. We can’t wait to see what kinds of new apps you build with our 3D CAD tools, and that includes our REST API. Definitely feel free to reach out to me if you have feedback on how this tutorial went for you also.
Now, go build!
Additional Resources
- Companion Repo on GitHub — please see the
main
branch for an example of what a finished project may look like. - The Product Launch Page Tutorial at the Dominican University of California — this was the source material that inspired Step 1 of this blog. Please read if you like to dive further into front-end development. Huge shout-out to my former instructor, Adam Braus, and his team for creating this resource!
- Three.js Fundamentals is a fantastic free resource to go deeper into the library for beginners.
- Traversy Media’s Crash Course on Express.js — offers a deeper look at the Node.js concepts covered in Step 3 of this tutorial.
- Last but not least, check out the official Onshape documentation for a deeper dive into our integration methodologies.