3D模型查看器开发实战【WebGL】

news2024/11/19 1:19:01

本文介绍如何从头开发一个包含3D 模型查看器的页面 - 尽管它非常简单,但你将学习的步骤也应该有助于构建其他类型的 Web 应用程序。

在自己的网站或博客里展示3D模型更简单的方式是使用NSDT 3DConvert提供的在线服务,无需任何开发工作,5分钟就可以完成(使用教程),效果如下:

NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割

1、环境设置

如果这是你第一次使用 git 或 Express.js,我需要您首先设置一些工具:

下载并安装版本控制工具Git。 请参阅此页面了解如何针对您的特定操作系统执行此操作。

立即登录/注册GitHub帐户,因为我们将使用它来帮助你练习推送/拉取更改。

下载并安装 Node.js 和节点包管理器 (npm)。 执行此操作的首选方法是首先安装称为节点版本管理器 (nvm) 的东西。 然后:

  • 如果你使用的是 macOS 或 Linux,直接访问此工具的 GitHub 存储库。 在系统上打开终端,然后粘贴以下命令:
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash

这实际上将下载 nvm。 现在,使用此命令来激活它:

$ 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
  • 一旦运行,我们应该验证它是否实际安装。 运行以下命令 - 如果它没有中断,那么你就可以开始了:
$ nvm --version # should output a version number, e.g. "0.38.0"
  • 好的! 现在,我们可以使用nvm通过命令行快速安装和使用不同版本的Node.js和npm。 在本教程中,我们将使用 Node 版本 14,以及 npm 的最新(兼容)版本:
$ nvm install 14
$ nvm use 14 --lts
$ nvm install-latest-npm
  • 最后,在继续之前,应该继续验证 npm -v 和 node -v 命令是否在终端中正常执行。 输出应该是 x.y.z 形式的字符串(就像 nvm -v 一样)。 这会让我们知道到目前为止一切正常。
  • 但是,如果我在 Windows 上怎么办? — 我明白了! 与 macOS/Linux 人群相比,只有 1 个细微差别:你将使用 GitHub 上的 nvm-windows 包,而不是普通的“ol nvm”。 使用从该存储库链接的安装程序在你的计算机上获取 nvm,而不是使用上面的终端命令。
  • 但之后,你应该仍然能够运行相同的命令来获取 Node.js 和 npm 的 LTS 版本。

对于本教程,我们非常欢迎你使用任何 IDE。 对于那些以前没有使用过的人,我强烈建议安装 Visual Studio Code(又名“VS Code”),因为它提供了很多扩展,使 Web 开发变得更容易(尤其是对于初学者)。

注意:如果你确实选择在本教程中使用 VS Code,我的下一个建议是安装“Live Server Extension”。 为此,请打开 VS Code,转到“首选项”>“扩展”,然后只需在搜索框中搜索它。 如果有兴趣,Microsoft 的 Sana Ajani 的这段视频可以提供更多背景知识。

2、添加样板代码

现在我们已经完成了设置,是时候通过构建每个产品所需的内容来启动我们的网络应用程序了:外观时尚的登陆页面!

现在前往 GitHub 存储库。 请使用此链接直接转到 README.md 中的说明。 立即完成该文件中的步骤 1-4,以便你为开发做好准备。

那么你现在有仓库了吗? 很好! 我们已将本教程的不同部分划分为多个 git 分支,因此不必担心同时跟踪太多移动部分。 现在,让我们转到分支来了解本教程的这一部分。 运行这些命令:

$ cd my-OnshapeExperiments.git
$ git checkout boilerplate-starter

这是在 IDE 中打开项目的特别好时机。 如果你使用 VS Code,也可以通过命令行完成此操作:

$ code .  # the "." is a file path, it should point to wherever the project root directory is located

好东西! 在 VS Code 内部,我们可以验证现在需要的所有文件。 如果你查看左侧窗格中的文件资源管理器,请验证它是否如下所示:

这里有一些观察结果:

README.md — 你已经看过这个了。

.eslintrc.json、.gitignore、package.json、package-lock.json、.env — 现在可以简单地忽略这些。

standalone/——这就是我们要关注的! 正如你所看到的,这里有 3 个子目录,分别名为 css 、 html 和 static 。

到目前为止,standalone 下不应该有任何文件,除了我在 static 文件夹中为您提供的 .jpg 图像(我们将在本节后面看到如何使用它)。

切换分支成功了吗? 好的。 现在,是时候开始构建我们的登陆页面了!

第一步是添加新的 HTML 页面。 我们首先在 VS Code 中打开一个终端来实现这一点。 使用 Ctrl + (反引号)或 Control + 显示集成终端(前者适用于 Windows,后者适用于 macOS/Linux)。

然后,继续在 html 文件夹下添加新的 HTML 文件。 使用 touch 命令,我们将其命名为index.html。

$ touch ./standalone/html/index.html  # exact file path will vary based on your local setup
  • 到目前为止,一切都很好? 获得该文件后,在编辑器中将其打开。 你可以在文件资源管理器中单击其名称,也可以调用之前的相同代码命令: code ./standalone/html/index.html

接下来,在 index.html 内部添加以下标记:

<h1>Hello World</h1>

不错。 现在,在进行任何进一步的更改之前,让我们先看看该 Web 应用程序在本地的样子。 是时候运行我们的网络服务器了!

对于那些选择使用“Live Server Extension”的人,你应该通过执行以下操作来完成此步骤:

将鼠标悬停在 VS Code 左侧窗格中的 index.html 文件上。

右键单击该文件。 然后你应该会看到一个菜单,其中有一个选项“使用 Live Server 打开”。 例如(在 macOS 上):

单击该按钮! 然后它应该将你带到浏览器。 如果一切正常,你应该在 http://127.0.0.1:5500/standalone/html/index.html 上看到如下页面:

你应该会在浏览器中看到呈现的文本“Hello World”。 干得好!

你认为这很神奇吗? 让我们证明它不是:返回index.html,并将“Hello World”更改为“Launch Page”。 使用 command + s(或 Ctrl + s)保存更改,然后返回浏览器。 现在页面有什么不同?

好吧 - 现在我们将采取下一步,并将其构建成一个真正的启动页面。 此时,将以下注释添加到index.html - 它应该如下所示:

<!-- NAVBAR -->


<!-- HERO -->

<h1>Launch Page</h1>

<!-- CALL TO ACTION -->


<!-- FOOTER -->

你可能还不明白所有这些组件是什么,没关系。 目前,这些评论至少为我们提供了本节其余部分的大纲或遵循的计划。

专业提示:快要开始构建 HTML 页面了。 不过,为了加快编写此代码的速度,我首先强烈建议安装一个名为 Emmet 的插件,它可以扩展 IDE 的自动完成功能(以便可以更快地编写代码!)。 如果使用 VS Code,Emmet 将立即为你工作。 如果不是,我建议你查看 Emmet 文档上的下载页面,了解如何为你的特定文本编辑器获取它。

现在已经安装了 Emmet,你可以通过键入 HTML 标签并按 Tab 键来快速编写 HTML。 尝试以下练习:

h1 => <h1></h1>
p => <p></p>

你可以使用 . 和 # 符号分别为:

p.lead => <p class="lead"></p>
.jumbotron => <div class="jumbotron"></div>
li.card => <li class="card"></li>

ul#comments => <ul id="comments"></ul>

一旦能够轻松浏览代码库,就可以回过头来为我们的应用程序添加 HTML 样板!样板文件是任何始终存在的标准代码。 Emmet 通过输入 html:5 并点击 Tab 为我们提供了 HTML 样板。继续这样做 - 然后,将我们的注释从前面移到  标记中。 之后,你的 index.html 应如下所示:

<!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>

警告:注意缩进! 始终保持所有内容正确缩进非常重要。 如果缩进不好,HTML 也可以工作,但你会更难发现错误和问题。 因此,请始终保持缩进一致。

好的! 我们已经开始了 HTML——但是我们的 CSS 呢? 虽然我们可以使用自己的 CSS 来设计整个网站的样式,但我更愿意向你展示如何做几乎每个人都会做的事情,从最小的顾问到最大的公司:使用 CSS 框架!

在本例中,我们将使用网络上最流行的 CSS 框架之一,Bootstrap 5。

首先,观看这个 20 分钟的 Bootstrap 演示。 它将让您对框架、如何使用它以及它背后的代码有一个直观的认识。 消化了这个心理模型后,你会更容易取得进展(如果你在之后的任何时候遇到困难,请确保将 Bootstrap 文档放在身边)。

现在,有几种方法可以将 Bootstrap 5 添加到我们的项目中,但我们将使用最简单的。 我们可以简单地在  标签中添加一个指向 Bootstrap 5(包括 CSS 和 JavaScript)的链接,如下所示:

<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>

如果保存,我们可以看出 Bootstrap 已添加。 我们的 h1 的字体应该已经改变,看起来更像 Bootstrap-py:

注意:这种字体的圆形外观更正式地被描述为 san-serif(我刚刚编造了“Bootstrap-py”这个词)。

接下来让我们开始向我们的网站添加导航栏(又名导航栏)。 在许多真实的发布网站上,这些都是非常常见的,因为它们可以帮助用户随时了解他们在哪里以及他们可以采取什么操作。

Bootstrap 附带了一个导航栏组件(链接到文档),用于将导航栏添加到您的页面。 导航栏组件最简单的实现是这样的:

  <!-- NAVBAR -->
  <nav class="navbar navbar-light bg-light">
    <a class="navbar-brand" href="#">Navbar</a>
  </nav>

它只有两部分,导航栏本身,然后是导航栏品牌,这是您的项目的名称。

当我们讨论这个主题时,现在让我们将导航栏中的文本更改为听起来更接近实际公司名称的内容。 进行以下更新:

  • 将  <a>标记内的文本更改为“ACME”。
  • 最后,在相关说明中,我们应该更改 <title>内的文本以使其更具描述性。 将其更改为“ACME Inc.:3D Truck Viewer”(有关 3D 查看器的更多信息将在步骤 2 中介绍)。

最后,你的index.html 现在应该如下所示:

<!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>

好的! 现在回到导航栏本身。 另一项改进是使屏幕尺寸较小的用户更容易访问此组件。 我们使用 Bootstrap 实现此目的的方法之一是添加“切换”按钮,并使用 CSS,以便在浏览器窗口不够宽时自动折叠导航栏。 看一下下一个代码片段,我在其中添加了这个  (以及一些 aria 属性,这些属性也是为了可访问性目的):

<!-- 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> 

注意:如果我们有此站点的其他页面,我们可以通过在上面的 <ul> 标记内插入 <li> 元素来添加到这些其他页面的链接。 只是想让你知道:)

好的,现在你的页面应该如下所示:

到目前为止,该导航栏看起来还不错,但是,你是否注意到品牌“ACME”看起来有多局促?

我们可以做些什么来改善其左侧的间距? 答案:自定义CSS!

此时,让我们回到index.html 中包含“ACME”文本的 <a> 标记。 我们的目标是将其稍微向右移动。

我们只想将样式应用于该文本本身 - 因此,让我们继续将文本嵌套在 <a> 元素内的 <p> 元素内。

最后,给它一个 id 属性,并将其命名为“custom-home-link”。 您的index.html 现在应该包含以下内容:

<!-- NAVBAR -->
...
<a class="navbar-brand w-50 mr-auto" href="/">
    <p id="custom-home-link">ACME</p>
</a>
...

现在我们有了将 CSS 应用于文本的方法,但我们还没有创建 CSS 样式。 通过返回终端并在 css/ 文件夹内创建一个新的 CSS 文件(称为 styles.css)来更改它:

$ touch standalone/css/styles.css

在编辑器中打开该文件。 要固定品牌标记的左边距,我们可以使用 margin-left 属性。 将以下内容添加到 styles.css:

#custom-home-link {
    margin-left: 1rem;
}

现在,要将其应用到 HTML,最后一步是在页面的  元素中嵌套一个链接标记。 返回到index.html,并从那里链接我们的CSS文件:

<head>
    <!-- Load in Bootstrap 5 CSS -->
    ... 
    <!-- Custom CSS -->
    <link rel="stylesheet" href="../css/styles.css" type="text/css">
    ...
</head>

注意:你的自定义 CSS 应始终位于链接 Bootstrap 的位置下方! 否则,Bootstrap 自己的 CSS 将覆盖你的类。

保存所有更改。 返回浏览器,见证神奇:

在导航栏之后,让我们重点添加网站的“英雄”部分。 英雄通常是产品的引人注目的图像和文字,
让我们网站的新访问者对我们公司的业务有一个良好的感觉。

让我们首先添加一个容器来容纳我们的英雄来开始这个过程(这将自动处理良好设计的一些小方面,例如边距)。

在代码中的 <!-- HERO --> 注释下,将现有的 <h1> 嵌套在具有实用程序类“container”的 div 中:

<!-- HERO -->
<div class="container">
    <h1>Launch Page</h1>
</div>

现在,为了在 Bootstrap 中真正制作一个漂亮的英雄,我们使用 jumbotron 组件。 观察下一个片段(这是更多样板代码):

<!-- HERO -->
<div class="container">
    <div class="jumbotron">
        <h1>Launch Page</h1>
        <p class="lead">A Fantastic Product!</p>
    </div>
</div>

你的页面现在应如下所示:

到目前为止工作顺利! 在继续之前,我认为我们可以更新该文本,使其比“启动页面”更生动一些。 继续将 HTML 文件中的 h1 文本更改为“指挥道路”。 在其下方,你还可以提前将 <p> 标记中的文本更改为“您的新卡车正在等待。”,以便它与上面视频中显示的示例站点相匹配。

你的代码应如下所示:

<!-- HERO -->
<div class="container">
    <div class="jumbotron">
        <h1>Command the Road</h1>
        <p class="lead">Your new truck awaits.</p>
    </div>
</div>

如果你现在看一下浏览器,它应该看起来像这样:

为了使标题更加突出,我们可以使用显示实用程序类。 继续看下一个代码片段(我还合并了更多行类,因此我们使用 Bootstrap 的默认网格系统):

<!-- 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>

最后,为了准备我们将添加到此页面的下一部分,将 hr 元素添加到 jumbotron div 的底部。 你的代码应如下所示:

<!-- 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>

现在仔细检查你的浏览器看起来如下所示:

一起来吧? 这会让这一切变得更好:背景图片!

Unsplash 是一个寻找一些免费照片来美化我们页面的好地方。 由于这是一个关于卡车的页面,你可以在入门存储库中看到我留下了一张可以使用的图像。 路径为“standalone/static/seb-creativo-3jG-UM8IZ40-unsplash.jpg”。

我们希望我们的背景图像适用于整个页面。 因此,我们首先将  标签下的所有内容嵌套在新的 div 元素下,id 为“truck-bg”。 你的index.html应该是这样的:

<body>
    <div class="truck-bg">
        <!-- NAVBAR -->
        ...

        <!-- HERO -->
        ...

        <!-- CALL TO ACTION -->

        <!-- FOOTER -->

    </div>
</body>

现在我们已经将 Truck-bg 添加到你的 HTML 中,现在在你的 CSS 中定义该类。 请参阅以下代码片段:

/* 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;
}

如果保存该图像并返回浏览器,无可否认,该图像看起来不会那么好。 它目前设置为覆盖我们的所有页面内容,到目前为止只有 1 个大屏幕。 所以它最终看起来就像是垂直裁剪的:

现在不用担心这个问题 - 我们将通过向页面添加更多元素来增加其垂直长度来解决这个问题。 现在,我们能做的就是修复英雄上的文本,使其再次在深色背景中脱颖而出。 在 Bootstrap 中,有一个名为“text-white”的实用程序类,可以使用它来帮助你做到这一点。 例如,可以执行如下操作:

<!-- 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>

然后,你的页面应类似于以下内容:

好的! 现在人们已经了解了我们的英雄服务,通常接下来要做的就是告诉他们我们产品的好处。 为了简洁起见,我们将跳过这一步。 相反,我们将直接要求用户在页面上执行操作。 这称为“号召性用语”或 CTA。 在这种情况下,我们将链接到另一个页面,他们可以在其中了解有关我们卡车的更多信息。

我们将再次使用大屏幕组件。 为了使我们的链接居中,我们将使用 Bootstrap 方便的文本中心类。 请参阅下面的代码(我还添加了其他属性,它们并不那么重要,但请阅读 Bootstrap 文档以阐明它们的功能):

<!-- 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>

注意:我们链接到的这个页面,viewer.html,尚未创建; 但别担心 - 这将在第 2 步中出现,届时我们将为 glTF 模型构建 3D 查看器:)

你的页面现在应如下所示:

注 2:回顾一下上面代码中第一个 div 中使用的“mt-5”类。 我们使用这个实用程序在 CTA 按钮的顶部添加边距,这样它就不会与英雄齐平。

最后,如果我们想进一步填充这个页面,可以使用一些额外的 CSS 来完成。 添加一个 id 到包含我们的 CTA 的 div,其值类似于“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>

要在 CSS 中定义此类,我们可以设置 height 属性:

/* styles.css */

...

#middle-section {
    height: 45em;
}

保存你的代码,并验证你的页面如下所示:

最后,我们可以用页脚来完善我们的页面。 页脚可能会很大(事实上,Onshape 就是一个完美的例子),但今天我们只是让它保持简单。 只需再使用几个实用程序类,我们就可以制作一个漂亮的页脚。 bg-secondary 为我们提供了辅助背景颜色(深灰色),p-5 为元素的所有侧面提供了大量的填充。 此处也应使用 text-white,原因与我们在英雄中使用它的原因相同。

看一下这个:

<!-- FOOTER -->
<div class="mt-5 bg-secondary p-5 text-white">
    <div class="row">
        <p>&copy 2022 ACME Inc. <br>
        </p>
    </div>
</div>

注意:那个 &copy是 HTML 的版权符号书写方式:©。

如果保存并滚动到页面底部,应该会看到类似以下内容的内容:

那个怎么样? 最好将页脚设置为深色,因为它表明页面已结束。 在这种情况下,它也与我们图像的调色板相匹配。

说到这里,我们要感谢 Unsplash 照片的摄影师。 页脚是致谢和链接到其他人的工作的好地方。

在我们的版权文本下嵌套一个新行 - 我们可以放置一个指向照片来源的链接,并将其放在 <small> 标签中,以免过于压倒:

<!-- FOOTER -->
<div class="mt-5 bg-secondary p-5 text-white">
    <div class="row">
        <p>&copy 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>

新的页脚应如下所示:

如果确实如此,那就太好了! 否则返回并排除故障,直到像素完美为止。

现在您已经有了启动页面的基线,但没有人喜欢如此通用的东西! 让我们来调味吧。

要为页面添加一些独特性,最简单的方法之一就是使用 Bootstrap 主题。 有些主题很奇特且复杂,但在本教程中,我们将使用一个免费的主题来增强 Bootstrap 本身,而无需添加任何额外的组件。

导航至 Bootswatch.com — 这里有很多主题,我鼓励你在某个时候自行探索。 在本教程中,我们将使用“Darkly”主题。

你可以使用 URL 将其链接到您添加 Bootstrap 本身的位置下方(但仍在自定义 CSS 上方)。 或者,更好的方法是实际下载 .min.css 文件并将其添加到你的项目中(我建议将其放置在 css/文件夹中)。 以下是从 Bootswatch 下载文件的按钮的屏幕截图:

无论哪种情况,index.html 的 head 组件现在应该类似于以下内容:

<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>

最后一件事:你可能需要将导航栏(以及页脚,为了更好的措施)调整为导航栏深色并将背景更改为深色(或使用以下颜色: bg-primary 、 bg-secondary 等) 。 然后不要忘记实用程序类“text-white”以从深色背景颜色中脱颖而出。 最后,页面应如下所示:

<!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>&copy 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>

这是相应的浏览器视图。 请注意,由于页面高度的原因,我缩小了页面以使页面适合屏幕截图,否则,如果你的窗口不够大,看不到底部的页脚,也没关系:

你现在有了一个启动页面。 在进入本教程的下一部分之前,请进行 git 提交来标记你的进度:

$ git add .
$ git commit -am "added boilerplate home page"

为了将你的工作与 GitHub 上的解决方案代码进行比较,我们也有一个分支:使用  git checkout boilerplate-solution来查看它。

哇哦 — 祝贺迄今为止取得的出色工作🥳!

3、添加 Vaporware 功能

在本节中,我们的目标是创建 Vaporware。 也就是说,在本节结束时我们将拥有一个 glTF 查看器 - 但我们暂时不处理 Onshape REST API,而是首先实现前端功能。 这样应该更容易调试,因为你只需使用本地 glTF 资源。

切换到本节的起始代码:

$ git checkout vaporware-starter

看一下存储库——你注意到了哪些变化?

具体来说,请密切注意存储库中以“[Challenge X]”开头的注释 - 这些是我要求你专注于本部分编码的地方。

注意:为了模拟行业中发生的情况,我们将使用流行的 3D Web 图形 JavaScript 库(称为 Three.js)来实现查看器。 这是一个巨大的库,值得拥有自己的专用教程系列 - 但对于这个博客,我提供了更多的入门代码,因此希望它更易于管理,您可以只关注最重要的内容 制作 3D 查看器的关键部分。

在我们继续编码之前最后一件事 - 让我们获取一个可以在本地使用的 glTF 模型文件! 以下是你的选择:

  • 如果特别希望能够可视化 Onshape 模型,请查看 Cody 的文章,了解如何将文档导出为 glTF 格式。
  • 否则,你可以自由地从示例卡车模型中导出 glTF(如开头的视频所示)。 我已启用此模型的链接共享,因此你可以在此处访问它(感谢 Poojan Shah 最初在此处的公共 Onshape 文档中创建了此卡车)!

进入文档后,它应该如下所示:

要导出 glTF,请首先单击页面上下载图标旁边的箭头。

在显示的菜单中,你需要选择“导出选项卡...”。

现在,屏幕上应该有一个标题为“导出”的模式。 此表单中有很多字段,但不用担心 - 你真正需要担心的唯一一个是“格式”。 打开该下拉列表,并确保选择“GLTF”选项。 请参阅下面的屏幕截图:

做出选择后,继续并单击模式中的蓝色“导出”按钮。 你的 glTF 文件应该很快就会下载完毕!

最后一件事:一旦你有了新的 glTF(可能名为“blueTruck.gltf”),请确保将其移动到我们项目存储库中的 static/ 文件夹(这样将来的说明将更容易遵循)。

好吧,这可能是你在想的部分,“Zain,很酷的 glTF - 但我如何在网页上呈现它?”

简单的。 我们将在本节的挑战 1 中重点讨论如何做到这一点。 翻到该分支上的index.js的起始代码,并在文件中搜索字符串Challenge 1。 它应该看起来像这样:

// (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}`);
    }
});

那么,这句话说的是什么? 我们可以在上面看到,我要求你做的是在查看器页面上的某处填充按钮的事件侦听器的一部分。 但是,所说的按钮在哪里? 事实上,什么是查看器页面?

要在此处提供一些背景信息,请返回您的浏览器,然后访问我们网站的主页。 还记得之前的 CTA 按钮吗? 点击它! 或者,如果你使用的是 Live Server Extension,请随时访问 http://127.0.0.1:5500/standalone/html/viewer.html 。

一旦你到了那里——tada! 这就是我所说的查看器页面的意思。 它应该看起来像这样:

如果你想了解有关此页面如何在 HTML 中标记的更多信息,请随意深入查看viewer.html(它与我们对index.html 所做的大部分相同)。 不过现在——你看到那个蓝色按钮了吗,上面写着“点击我!” 在页面上?

嘘——这就是我们的事件监听器的按钮!

我们的预期 UX 是让用户单击该 HTML 按钮,这样我们的 JavaScript 就知道使用 Three.js 将 3D 模型渲染到视口上。

那么,我们如何解决这个挑战呢? 我鼓励你在这里暂停一下,仔细阅读 index.js 中的代码,并在我给出下面的解决方案之前自行尝试一下。 如果有疑问,请不要害怕深入研究 Three.js 文档 - 探索是很好的!

解决时间:好的! 正如我们的挑战 1 评论中提到的,解决此问题的关键是调用 loadGltf() 函数,传入 glTF 资源的相对文件路径。 在本例中,假设你的资产名为 blueTruck.gltfand 存储在 static/ 文件夹中,则代码将如下所示:

// ...
try {
    // ...
    loadGltf("../static/blueTruck.gltf");
}
// ...

不相信? 返回viewer.html,并测试“Click Me!” 按钮。 你应该看到你的卡车装载情况,如下所示:

注意:有关 Three.js 如何解析 glTF 来加载 3D 图形的更多信息,我绝对建议阅读 GLTFLoader 类。

接下来——我们的观察者正在工作,但我们对模型的最初视角处于一种尴尬的特写位置。 我们可以使用 Three.js 来解决这个问题吗?

简短的回答——是的! 让我们看看本节的挑战 2 是如何实现的。 就像之前一样,首先在index.js 中找到包含challenge 字符串的注释。 它应该看起来像这样:

/**
 * 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 */
    // ...
};

花点时间阅读此评论中的描述。 你会注意到这是一个名为 setGltfContents() 的函数,一旦您的 gltfLoader 对象(如前面的挑战 1 中所示)成功加载您的 glTF 文件,就会调用该函数。 具体来说,我们将重点关注如何使用 Three.js 的“相机”对象将起始视点调整到我们的 3D 模型中。

在此暂停,请自行尝试此挑战(如果需要帮助,请随时阅读 PerspectiveCamera 类上的文档!)

…好吧,让我们回顾一下这里的解决方案 - 根据注释,你的代码可以使用类似于以下内容的内容来实现:

/**
 * 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;
    // ...
};

注意:上面代码的最后 4 行在这里特别重要。 这告诉 Three.js,我们希望相机的大小是 3D 模型周围“边界框”的 2 倍,以确保加载后我们可以在查看器元素中修复整个模型(但希望也不会太远)。

测试“点击我!” 再次按钮。 如果ni 使用的是卡车模型,应该会看到它正在加载,但现在它应该处于更自然的起始位置:

干得好🙌! 你现在已经完成了挑战 2。就像挑战 1 一样,我鼓励你阅读有关您在此处单独使用的各种 Three.js 类的更多信息。 否则,让我们承诺你的进度并继续前进:

$ git commit -am "completed 3D model viewer using local gltfs"

4、从 Onshape REST API 渲染 glTF

到目前为止,我们已经为 glTF 模型构建了一个 3D 查看器。 这很棒 - 但我们仅限于渲染本地计算机上的任何 glTF 文件。 这就是 Onshape 的 REST API 的用武之地:有了它,我们将能够让用户请求查看我们存储在你的 Onshape 帐户中的 3D 模型。 我们现在就去设置吧!

首先,让我们切换到本节的起始代码:

$ git checkout custom-server-starter

你还记得我之前告诉过你忽略 package.json 文件吗? 未来它将会很有用。 该文件的主要实用程序是安装所有 Node 依赖项 - 它们都在文件中列出,因此我们可以使用此 npm 命令(从根目录)安装它们:

$ npm install -i

接下来,Onshape REST API 的设计非常重视网络安全,因此我们要求开发人员在使用之前注册 API 密钥。

转至 Onshape 开发者门户并使用你的 Onshape 帐户凭据登录:

但是,如果我有多个 Onshape 帐户怎么办? 在这种情况下,你需要选择对存储在 Onshape 中你希望 glTF 查看器渲染的任何 3D 模型具有(至少)查看权限的帐户。
注意:也就是说,如果打算可视化你私有的 3D 模型,应该使用你的私人 Onshape 帐户凭据。 否则,不必费力执行此步骤,只需使用你拥有的任何 Onshape 帐户即可 - 在本教程中,我们将仅使用公开可用的模型,因此在访问它们时不会遇到任何问题。

登录开发者门户后:转到“API 密钥”选项卡,然后创建一对新的 API 密钥以供使用。

在本地 git 存储库的根目录中创建一个新文件,将其命名为 .env 之类的名称,以便我们有一个安全的位置来保存这些 API 密钥:

$ touch .env

接下来 — 让我们填写你的 .env 文件。 本教程特别需要六个。 我把它们列在下面,请参阅评论以了解如何填写:

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>"

好的! 是时候开始与 Onshape 的 API 集成了。 同样,这里的目标是,我们不是从本地存储中读取 glTF 文件,而是告诉我们的 Web 服务器在浏览器中渲染之前从 Onshape 的服务器之一请求它们。 正如你可能猜到的,我们需要编写自定义服务器端代码来执行此操作 - 这就是 Express.js 的用武之地。

我们为什么使用 Express? 老实说,我们在这里使用它是因为我不希望您在本教程的这一部分中学习另一种编程语言。 Express.js 是一个非常流行的 JavaScript Web 应用程序框架。 这基本上意味着,如果你可以为浏览器编写 JavaScript(而且您可以,因为我们之前就这样做过),那么可以将该技能转移到为 Web 服务器编写 JavaScript(当然,如果遇到困难,请使用文档)。

以此为背景,我们现在需要弄清楚如何在 Express 中实际启动服务器——现在让我们在这个 git 分支的“挑战 1”中这样做! 现在翻到 wwwfile,它应该如下所示:

#!/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 */

现在暂停,请先尝试挑战 1,然后再继续……

……好吧! 你的挑战解决方案应类似于以下内容:

#!/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, () => {});

注意:如果你不清楚 require() 函数是如何工作的,我建议你在继续之前相关内容。

那么现在——这个文件应该如何启动我们的服务器呢? 观察文件顶部的注释 #!/usr/bin/env node 。 该注释(通常称为 shebang 行)告诉我们该文件实际上是一个可执行文件。

因此,可以观察到在 package.json 中,我们有一个名为“scripts”的键,其中列出了以下内容:

// package.json
// ...
"scripts": {
    "start": "bin/www",
    "dev": "nodemon bin/www",
// ...

这是什么意思? package.json 的这一部分是我们可以指定自定义 npm 命令的地方,其他人可以在使用我们的项目时执行这些命令。 现在,我们将专注于调用本词典中列出的第二个脚本 dev。这只是为了提高生活质量 - 就像我们在使用“ Live Server”扩展,nodemon 将在代码更改后自动重新加载我们的应用程序(这样我们就不需要在每次代码更改后重新启动服务器,这可能很麻烦)。

因此,要使用 nodemon 运行 bin/www 可执行文件,请返回终端并调用我们的开发脚本:

$ npm run dev

现在,你的服务器正在运行! 要进行测试,请转到地址 https://localhost:3000/(或你配置应用程序使用的任何端口)。 将在页面上看到以下响应:

显然,我们还没有完成。 正如您可能从页面上的错误消息中猜到的那样,我们有一个正在运行的服务器,但现在需要告诉该服务器在哪里可以找到我们的 Web 应用程序的所有 HTML/CSS,然后才能在该服务器上提供我们所需的内容。 用户界面。 让我们在挑战 2 和挑战 3 中解决这个问题。

转到名为 server.js 的文件。 挑战 2 的评论应如下所示:

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 */

在顶部,此起始代码导入一些 Node 模块,并使用 Express.js 将我们的 Web 应用程序实例化为变量(恰当地命名为 app)。 现在,下一步是添加中间件函数 - 这些本质上是我们的应用程序在经历请求-响应周期时能够使用的函数,因此这是我们添加对我们所有应用程序有用的代码的理想位置。 应用程序请求处理程序。 现在,继续在挑战 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

好的,那么这是怎么回事呢? 作为背景,app.use() 通常是我们在 Express 中使用中间件函数时调用的方法。 特别是,当我们使用express.static()时,这基本上是我们告诉 Express 在哪里可以找到我们应用程序的所有静态资源的一种方式,即所有 HTML/CSS/图像等。可以测试一下 可以通过访问 http://localhost:3000/html/index.html 来实现——如果是的话,我们将能够再次从第 1 部分看到我们的主页!

那么这就是express.static(),bodyParser 是什么? TLDR 是这个中间件,它允许我们读取传入服务器的 HTTP 请求的“主体”,方法是将其存储在名为 req.body 的变量中——这对于多种服务器端功能也很有用。

好吧,挑战 2 已经解决了,但是挑战 3 呢? 验证 server.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 */

请完整阅读评论,然后在尝试后回来。 别担心,我会等:)...

/** [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) 部分,请注意我们再次调用 app.use(),并且只是传递对 require() 函数的调用 - 因此从技术上讲,您可以将其视为自定义中间件的第一个实现!

对于 b) 部分,解决方案基本上只是 Express.js 方式,告诉我们的服务器如何提供我们在本教程前面部分中处理的两个 HTML 页面 — index.html 和查看器页面。 你可能会问,如果我告诉你这就是我们使用express.static()中间件的目的,为什么需要这些行呢?

答案出奇的简单——如果我们不这样做,那么我们将无法在我们的服务器上使用 CSS! (可以通过注释掉包含express.static()的行并重新加载页面来测试这一点)。

现在,为了验证我们的更改是否有效,请通过访问 http://localhost:3000/ 转到主页路径“/”。 你应该再次看到我们熟悉的主页!

前进! 到目前为止,我们已经添加了为所有 HTML/CSS 提供服务的路由。 您能猜出我们需要在 Express 代码中添加什么才能从 Onshape API 获取数据吗?

如果你猜到“另一个路由功能”,那么您是对的! 这是因为路由基本上只是我们告诉 Express 如何 1) 接受请求,2) 以某种方式处理它们,以及 3) 如何返回适当响应的一种方式。 因此,就像我们可以在用户和我们自己的服务器之间设置请求-响应周期的路由一样,我们的服务器和我们与之交互的 API 的其他服务器(在本例中为 Onshape 的服务器)之间也可以这样做。

说到 API,让我们翻到挑战 4,看看如何在 Express 中对 Onshape 进行 API 调用。 如果访问 api.js,应该会看到以下起始代码:

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 */
});

很多代码对吗? 让我们稍微分解一下:

我们使用 Onshape REST API 中的“exportPartStudioGltf”端点。 顾名思义,它会为您检索 glTF 模型,并提供有关定义该模型的 Part Studio 的正确信息(稍后会详细介绍)。 现在,请随意在我们的文档中使用此端点,以便您在本教程中使用它会更加自如。

其次,上面的注释提到了一个名为forwardRequestToOnshape()的函数——这个函数是什么? 为了启发,我将其粘贴在下面:

/**
 * 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 });
    }
}

我鼓励您在这里暂停并功能以更好地理解它。 从本质上讲,这消除了我们向 Onshape 发出的 API 请求授权过程中的大量复杂性(即,通过包含你之前调用 fetch() 时从开发门户获得的密钥)。 尽管这主要是样板文件,但从安全角度来看,它非常重要!

好的,现在请尝试一下挑战 4……

……然后,看看解决方案!

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
    );
});

好吧,让我们回顾一下——首先,模板字符串上的一个词——参数 did 、 wvm 、 wvmid 和 eid 到底指的是什么?

为了回答这个问题,让我们观察一个并行的例子:当你在 Onshape 中打开文档时,您在我们的服务器上遵循的 URL 路径遵循相同的命名方案。 例如,如下文档链接:

https://cad.onshape.com/documents/c1c54c370fa5185f0a52ed15/w/1b249e369705d99ca986bec1/e/12bd0e929396835dadeaa83b?renderMode=0&uiState=63a76a21d12f2f36e4bebae5:

字符串 …/documents/c1c54c370fa5185f0a52ed15/… 表示文档ID。

字符串…/w/1b249e369705d99ca986bec1/… 表示我们将使用工作区 ID 来引用我们想要请求的任何内容(而不是使用文档的版本/微版本的 ID)。

字符串 …/e/12bd0e929396835dadeaa83b/… 表示我们要引用的特定文档的元素的 ID。

当看到我们的 API 中使用的术语“元素”时,这与普通用户所认为的 Onshape 文档中的单个选项卡完全相同!

有了这些知识,我们现在就可以测试您刚刚添加到 Express 应用程序中的 API 路由。 为何如此?

使用上面的文档链接(或自己的文档链接之一),使用正确的路径参数向此 API 路由发送请求! 我们可以使用curl命令在终端中完成这一切——这是一个示例:

$ curl http://localhost:3000/get-gltf/c1c54c370fa5185f0a52ed15/w/1b249e369705d99ca986bec1/12bd0e929396835dadeaa83b/

完成此操作后,应该需要一秒钟,然后 - 瞧! 您会看到一些信息——这是从 Onshape 服务器返回的响应。 它应该看起来像这样:

{"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":...

如果看到此内容,则意味着刚刚通过我们的 API 成功请求了 glTF!

最后,我们已经了解了如何以编程方式从 Onshape API 请求和接收 glTF 数据,但我们尚未将此功能集成到 UI 上的查看器中。 让我们在挑战 5 和挑战 6 中解决这个问题:

首先查看更多代码,让我们在浏览器中访问更新后的查看器页面。 转到 http://localhost:3000/truck-viewer,应该会看到类似以下内容的内容:

哇,这个表格在这里做什么? 让我们翻到viewer.html——你在那里看到的表格可以解释更多。 当在那里时,还请看一下挑战 5 的描述:

<!-- 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>

这句话说的是什么? 我们知道,在我们的服务器向 Onshape API 请求 glTF 之前,我们需要某种方式让普通用户根据 API 所需的参数(如上所述)告诉它要获取哪个 glTF。

注意:此时,在现实世界中,您可能需要与产品经理或 UI/UX 设计师讨论此问题 - 但在本教程中,我们将让用户提交参数 通过您在上面看到的表单,我们的服务器将解析该表单以获取 API 请求所需的变量)。 因此,该表单并不是我们实现此用户旅程的唯一方式,但它将有助于让您的学习变得简单:

现在,迎接挑战吧! 我们可以看到表单的起始代码带有您可以使用的默认值。 但是,我们仍然需要一种方法让 JavaScript 能够解析在特定表单字段下提交的数据 - 这就是您的用武之地。继续填写viewer.html 中“您的 ID 在这里”的位置。

尽管可以使用各种不同的 ID 字符串,但执行此操作的一个简单选择是重用起始代码中放置的每个 <label>  组件的 forattribute 中的字符串 - 以下是一个示例解决方案:

<!-- 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>

好东西! 如你所见,我们刚刚填写了表单字段的 idattribute。 请检查以确保你的 id 字符串对于其各自的 HTML 元素来说是唯一的 - 这样 JavaScript 以后就可以轻松找到它们(我保证,这个语句很快就会变得更有意义)。

接下来,让我们找到挑战 6。你还记得我们在本教程前面实现事件监听器时的情况吗? 好吧,让我们回顾一下 index.js — 在其中,应该看到以下注释:

// (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}`);
    }

嗯……事件监听者,你看起来有点不同。 你剪新发型了吗? 💇‍♂️

严肃地说,您是否看到起始代码在哪里调用 fetch(/api/get-gltf/${did}/${wvm}/${wvmid}/${eid}) ? 这实际上至关重要,因为这将我们应用程序的前端连接到我们在本节前面使用 Express 实现的 API 路由。 现在,继续完成这个挑战,将“YOUR ID GOES HERE”字符串替换为您在viewer.htmlform 中放置的相应ID。 如果遵循我的示例,解决方案应类似于以下内容:

// (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}`);
    }
});

此时,viewer.html 中的表单应该能够请求、接收和渲染你存储在 Onshape 中的任何大量 3D CAD 模型(此时唯一的限制是它们应该是你有权访问的 Part Studio) 定期查看,就像访问 https://cad.onshape.com 一样。

现在就继续尝试该表单 - 可以使用默认表单值提交它,也可以随意渲染您拥有的任何 Part Studio 模型(使用我们之前讨论的内容,涉及如何从 Onshape 获取所需的 API 参数) 文档网址)。 有关“成功”结果应该是什么样子的示例,请参阅我在本博客顶部包含的演示视频。

5、结束语

恭喜你走到了最后! 👏 请花点时间反思一下今天所取得的成果。


原文链接:3D模型查看器开发实战 - BimAnt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1610305.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

access多表关联提示:语法错误(操作符丢失)在查询表达式中

在access数据库中执行多表关联时提示了一个错误 select * from Patient a inner join BioMain b on a.BioIDb.BioID inner join BioResult c on b.BioIDc.BioID where len(a.PatientID)>12 and b.AddTime>#2024-04-17 05:53:23# and b.AddTime<#2024-04-17 17:53:23#…

基于Python 实现数据可视化大屏

数据本身是冰冷的数字&#xff0c;通过选择合适的图形或者图表来进行展示表达&#xff0c;使得传递给使用者的感受更加直观、更容易获得其中的价值。 数据可视化将技术与艺术完美结合&#xff0c;借助图形化的手段&#xff0c;清晰有效地传达与沟通信息。一方面&#xff0c;数…

Unity地形关联出错的解决办法以及地形深度拷贝

问题 最近发现unity地形系统的一个bug&#xff0c;导入的场景地形数据关联错乱了&#xff0c;关联到别的场景的地形数据了&#xff0c;meta替换了也没用&#xff0c;不清楚它具体是怎么关联的。 看下面的案例&#xff1a; 可以看到正常这个场景的地形数据应该关联的是Scene_E…

【GitBlit】Windows搭建Git服务器详细教程

前言 如果公司或个人想在 Windows 环境下搭建私有的 Git 服务器&#xff0c;那么这个开源的 GitBlit 是一个不错的选择。 Gitblit 是一个开源纯 Java 的用于管理、查看和服务 Git 存储库。它是一个小型的托管集中式存储库工具。支持 SSH、HTTP 和 GIT 协议&#xff0c;开箱即…

SpringBoot项目如何实现邮件发送

文章目录 1. 开启邮箱SMTP服务2. 导入pom依赖3. 在配置文件中添加邮箱配置3. 封装EmailTask类4. 写测试类 1. 开启邮箱SMTP服务 这里以163邮箱为例&#xff0c;点击设置——更多设置——POP3/SMTP/IMAP——开启服务 根据提示开启服务之后会得到一个授权码&#xff0c;只显示一…

线性代数基础3 行列式

行列式 行列式其实在机器学习中用的并不多&#xff0c;一个矩阵必须是方阵&#xff0c;才能计算它的行列式 行列式是把矩阵变成一个标量 import numpy as np A np.array([[1,3],[2,5]]) display(A) print(矩阵A的行列式是&#xff1a;\n,np.linalg.det(A))array([[1, 3],[2, …

04 JavaScript学习:输出

JavaScript 没有任何打印或者输出的函数。 JavaScript 显示数据 JavaScript 可以通过不同的方式来输出数据&#xff1a; 使用 window.alert() 弹出警告框。使用 document.write() 方法将内容写到 HTML 文档中。使用 innerHTML 写入到 HTML 元素。使用 console.log() 写入到浏…

用栈实现队列(力扣第232题)

#define _CRT_SECURE_NO_WARNINGS 1 #include "assert.h" #include "stdio.h" #include "stdbool.h" #include "stdlib.h" #include "string.h" #define N 10 typedef int STDataType; int data; //静态栈 //typedef struct…

面试算法题之暴力求解

这里写目录标题 1 回溯1.1 思路及模板1.2 例题1.2.1 全排列1.2.2 N 皇后1.2.3 N皇后问题 II 1 回溯 1.1 思路及模板 抽象地说&#xff0c;解决一个回溯问题&#xff0c;实际上就是遍历一棵决策树的过程&#xff0c;树的每个叶子节点存放着一个合法答案。你把整棵树遍历一遍&a…

数据链路层协议——以太网协议

目录 要解决的问题 以太网协议 以太网帧格式 MAC地址 MAC地址和IP地址 MTU MTU对IP协议的影响 MTU对UDP协议的影响 MTU对TCP协议的影响 ARP协议 ARP协议格式 ARP协议的工作流程 ARP缓存表 要解决的问题 上一篇我们也说了&#xff0c;数据从应用层一步步封装到了网…

沉思录 (梁实秋)

链接&#xff1a;https://pan.quark.cn/s/8e27564b02f5

Flutter 的 showDialog 和 showCupertinoDialog 有什么区别?

我将我的 App 里用的 Flutter 升级到了 3.19&#xff0c;没想到&#xff0c;以前我用 showDialog 和 AlertDialog 组合创建的二次确认框&#xff0c;变得无敌难看了&#xff0c;大幅度增加了整个框的圆角和里面默认按钮的圆角。不得已&#xff0c;我必须修改一下&#xff0c;以…

[笔试强训day02]

文章目录 BC64 牛牛的快递DP4 最小花费爬楼梯[编程题]数组中两个字符串的最小距离 BC64 牛牛的快递 BC64 牛牛的快递 #include<iostream> #include<cmath> using namespace std;double a; char b;int main() {cin>>a>>b;int ans0;if(a<1.0){ans20;…

【图解计算机网络】网络协议分层解析

网络协议分层解析 网络协议分层应用层传输层网络层数据链路层 TCP/IP分层模型通讯示例 网络协议分层 网络协议分层一共有OSI七层网络协议&#xff0c;TCP/IP四层网络网络协议&#xff0c;还有五层网络协议。 七层由于分层太多过于复杂&#xff0c;实际应用中并没有使用&#x…

Flutter 热修复(Shorebird)

Shorebird&#xff1a;https://docs.shorebird.dev/ 我们都知道安卓原生开发&#xff0c;热修复已经不是什么难题。阿里云&#xff0c;腾讯云已经都有现成的SDK可以接入。 然而Flutter开发还一直没有类似热修复的开发库&#xff0c;无意中看到了Shorebird这个平台&#xff0c…

云服务器需要多少流量?评估支持最大并发量?

一 需要购买多大的流量&#xff1f; 项目上线时&#xff0c;我们需要购买多大的流量的带宽&#xff1f;支持多少设备&#xff08;支持多少并发量&#xff0c;在设计阶段会计算&#xff09;&#xff1f;作为架构师我们必须清楚与明确。 二 清楚服务器的流量计算 常见的云服务主机…

win32 API 函数

目录 win32 API 的介绍控制台程序COORD结构体GetStdHandle函数GetConsoleCursorInfo函数SetConsoleCursorInfo函数SetConsoleCursorInfo函数GetAsyncKeyState函数 win32 API 的介绍 WIN32API就是Microsoft Windows32位平台的应⽤程序编程接⼝ win32 API 中有许多可以调用的函数…

【ZBrush】制作章鱼练习 02——足部

本篇效果 步骤 笔刷工具选择“Move” 按下X键激活对称&#xff0c;然后往外拉 这里拉出6条腿的基底 笔刷工具选择“CurveTube” 绘制腿&#xff0c;可以发现此时腿部起始点和终点的粗细一样&#xff0c;但是真实的章鱼腿部应该是根部较粗&#xff0c;脚部较细 因此我们先回撤一…

网络流问题详解

1. 网络最大流 1.1 容量网络和网络最大流 1.1.1 容量网络 设 G(V, E)是一个有向网络&#xff0c;在 V 中指定了一个顶点&#xff0c;称为源点&#xff08;记为 Vs&#xff09;&#xff0c;以及另一个顶点&#xff0c;称为汇点&#xff08;记为 Vt&#xff09;&#xff1b;对…

淘宝/天猫获取sku详细信息 API,item_sku-获取sku详细信息

淘宝/天猫获取sku详细信息 API,item_sku-获取sku详细信息 示例&#xff1a; {"seller_rate": true,"timeout_action_time": "2000-01-01 00:00:00","iid": "152e442aefe88dd41cb0879232c0dcb0","num": 10,"…